실무 프로젝트로 배우는 Kotlin & Spring/리액티브 프로그래밍

비동기-논블로킹 프로그래밍

webmaster 2022. 11. 28. 01:08
728x90

동기 프로그래밍

  • 작업의 실행 흐름이 순차적으로 동작한다.
  • 순차적으로 동작하는 프로그램은 코드를 파악하기 쉽고, 결과를 예측하기 쉬우므로 디버깅이 쉽다.
  • 특정 작업을 실행하는 동안 다른 작업을 할 수 없다는 단점이 존재한다.

비동기 프로그래밍

  • 작업의 실행흐름이 기본적으로 순차적이지 않다.
  • 비동기 처리 방식은 현재 실행 중인 작업이 끝나는 것을 기다리지 않고 다른 작업을 할 수 있다.
  • 서버, 클라이언트 등 모든 환경에서 유용하게 사용된다.
  • UI 애플리케이션의 경우 특정 이벤트가 발생할 경우에 반응하는 동작을 구현해야 하는데, 이럴 때 필수적으로 비동기 프로그래밍을 사용하게 된다.
  • 대부분 프로그래밍 언어들은 각 언어의 철학에 맞는 다양한 비동기 처리 방법을 지원한다.
    • 대표적으로 callback, promise, future, async-await, coroutine 등이 있다.

비동기 프로그래밍 구현

Thread

fun main() {
    for (i in 0..5) {
        val thread = Thread {
            println("current-thread-name: ${Thread.currentThread().name}")
        }
        thread.start()
    }
    println("current-thread-name: ${Thread.currentThread().name}") //main 스레드 출력 -> 프로그래밍 실행될 때 가장 기본으로 실행되는 스레드
}
  • 가장 기본이 되는 비동기 처리 방식이다.
  • 스레드는 Runnable 인터페이스를 사용해 비동기 동작을 수행한다.
  • 스레드가 1개인 경우 싱글 스레드라고 부르고, 하나 이상 존재하는 경우 멀티스레드라고 한다.
  • 멀티 스레드를 사용하면 애플리케이션에서 여러개의 작업을 동시에 할 수 있다.
    • 멀티 스레드에서, 스케쥴링 알고리즘에 의해 스레드가 전환되며, 작업을 처리하는데 이를 컨텍스트 스위칭이라 한다.
    • 하나의 프로세스에는 최소 하나 이상의 스레드가 존재, 프로세스 내의 스레드들은 동일한 메모리를 공유한다.
    • 스레드는 프로세스를 생성하는것 보다 가볍다.

ExecutorSerivce

fun main() {
    val pool = Executors.newFixedThreadPool(5) //Executor Service에서 스레드를 꺼내오는 펙토리 메서드, 파라미터 값은 스레드의 유지 갯수
    try {
        for (i in 0..5) {
            pool.execute {
                println("current-thread-name : ${Thread.currentThread().name}")
            }
        }
    }finally {
        pool.shutdown()
    }
    println("current-thread-name : ${Thread.currentThread().name}")
}
  • 스레드가 무한정 많아지면 OOM 이 발생할 수 있고, 높은 처리량을 요구하는 시스템에서는 스레드를 생성하는데 대기하는 시간 때문에 응답 지연이 발생할 수 있다 
  • 이를 해결하기 위해 스레드 풀을 사용하며, 애플리케이션 내의 스레드의 총개수를 제한할 수 있고, 기존에 생성된 스레드를 재사용하여 빠른 응답이 가능하다.
  • 검증된 라이브러리를 사용해 작성된 스레드 풀을 사용해야하며, java.util.concurrent 패키지의 ExecutorService를 사용하면 쉽고 안전하게 스레드 풀을 사용할 수 있다.
  • 출력 결과를 보면 스레드 풀에서 관리하는 스레드임을 알 수 있고, 동일한 스레드 이름을 보고 스레드 풀에 있는 스레드를 재사용한 것을 파악할 수 있다.

Future

fun sum(a: Int, b: Int) = a + b

fun main() {
    val pool = Executors.newSingleThreadExecutor()
    val future = pool.submit(Callable {
        sum(100, 200)
    })
    println("계산 시작")
    val futureResult = future.get() //비동기 작업을 결과를 기다린다.
    // get 함수를 사용하게 되면 스레드가 작업을 완료할 때까지 스레드가 블록킹 된다
    println(futureResult)
    println("계산 종료")
}
  • future는 비동기 작업에 대한 결과를 얻고 싶은 경우에 사용된다.
    • 수행 시간이 오래 걸리는 작업(db접근, API 호출)에 대 한 결과를 기다리면서 다른 작업을 병행해서 수행하고 싶은 경우 유용
  • 스레드는 Runnable을 사용해 비동기 처리를 하지만, future를 사용해 처리 결과를 얻기 위해선 callable을 사용한다.
  • future를 사용하면 비동기 작업을 쉽게 구현할 수 있지만, 단점도 있다.
    • get 함수는 비동기 작업의 처리가 완료될 때까지 다음 코드로 넘어가지 않고 무한정 대기/ 지정해둔 타임 아웃까지 블로킹된다.
    • future를 사용하면 동시에 실행되는 한 개 이상의 비동기 작업에 대한 결과를 하나로 조합하여 처리하거나, 수동으로 완료처리할 수 있는 방법을 지원하지 않는다.

Completable Future

//CompletableFuture를 사용해서 자바에서 비동기 논 블로킹 작업을 쉽게 구현할 수 있다.
fun main() {
    val completableFuture = CompletableFuture.supplyAsync {
        Thread.sleep(2000)
        sum(100, 200)
    }

    println("계산 시작")
    completableFuture.thenApplyAsync(::println) //논 블로킹으로 동작

    /*
    val result = completableFuture.get() //블로킹 동작
    println(result)
     */

    while (!completableFuture.isDone) { //종료 될때까지 대기
        //completableFuture.isCancelled //취소 여부
        //completableFuture.isCompletedExceptionally //오류 발생 여부
        Thread.sleep(500)
        println("계산 결과를 집계 중입니다.")
    }
    println("계산 종료")
}
  • JDK8 이상부터 future을 단점을 극복하기 위해 CompletableFuture를 제공한다.
  • 펙토리 함수인 supplyAsync 함수를 사용해 비동기 작업을 수행할 수 있다.
  • thenApplyAsync 함수를 사용해 논블로킹으로 동작하고 뒤에 Async가 붙은 함수들은 supplyAsync와 별도의 스레드 풀을 지정할 수 있다
  • isDone 으로 CompletableFuture가 수행 중인 비동기 작업이 완료된 상태인지를 알 수 있다
  • 취소 상태를 나타내는 isCancelled, 비동기 작업 도중에 에러가 발생한 상태를 나타 내는 isCompletedExceptionally 도 제공한다.
  • CompletableFuture를 사용하더라도 get 함수를 그대로 사용하면 코드가 블록킹 된다
728x90

'실무 프로젝트로 배우는 Kotlin & Spring > 리액티브 프로그래밍' 카테고리의 다른 글

리액티브 프로그래밍  (0) 2022.12.01
Iterator 패턴  (0) 2022.12.01
Observer 패턴  (0) 2022.11.28