티스토리 뷰

동기 프로그래밍

동기(Synchronous)방식의 프로그램에서 작업의 실행 흐름은 순차적으로 동작한다.

fun main() {
  // 실시간 주식 정보를 가져온다.
  val stock: StockDto = getRealtimeStockData("APPLE")

  println("주식 심볼 : ${stock.symbol}")
  println("시가 : ${stock.price.open}")
  println("종가 : ${stock.price.close}")
  println("시가총액 : ${stock.price.marketCap}")
} 
  • 순차적으로 동작하는 프로그램은 코드를 파악하기 쉽고 결과를 예측하기 쉬우므로 디버깅이 쉽다.
  • 특정 작업을 실행하는 동안에는 다른 작업을 할 수 없다는 단점이 존재한다.

 

비동기 프로그래밍

비동기(Asynchronous)방식의 프로그램에서 작업의 실행 흐름은 기본적으로 순차적이지 않다.
이러한 특징으로 인해 비동기 처리 방식은 현재 실행 중인 작업이 끝나는 것을 기다리지 않고 다른 작업을 할 수 있어 서버, 클라이언트 등 모든 환경에서 유용하게 사용된다. UI 애플리케이션의 경우 특정 이벤트가 발생한 경우에 반응하는 동작을 구현해야 하는데 이럴 때 필수적으로 비동기 프로그래밍을 사용하게 된다.

// 버튼을 누를때마다 카운트가 증가하는 예제
const button = document.querySelector('button');

// 'click' 옆의 두 번째 인자가 비동기 콜백
button.addEventListener('click', event => {
    button.innerHTML = ` : ${event.detail}`;
});
  • 대부분의 프로그래밍 언어들은 각 언어의 철학에 맞는 다양한 비동기 처리 방법들을 지원한다.
  • 대표적으로 Callback, Promise, Future, Async-await, Coroutine 등이 있고 각각의 방법들은 장점과 단점이 존재한다.

 

비동기 프로그래밍 구현

Thread

가장 기본이 되는 비동기 처리방식으로 Runable 인터페이스를 사용해 비동기 동작을 수행한다. 스레드가 1개인 경우 싱글 스레드(Single Thread), 하나 이상 존재하는 경우 멀티 스레드(Multi 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}")
}
// 호출할때마다 출력 결과가 달라진다.
  • 멀티 스레드를 사용하면 스케쥴링 알고리즘에 의해 스레드가 전환되면서 작업을 처리하는데 이를 컨텍스트 스위칭이라 한다.
  • 하나의 프로세스에는 최소한 하나 이상의 스레드가 존재하고 프로세스 내의 스레드들은 동일한 메모리를 공유한다.
  • 스레드는 프로세스를 생성하는 것보다 가볍다.
  • 하지만 스레드가 무한정 많아지면 메모리 사용량이 높아져서 OOME(OutOfMemoryError)가 발생할 수 있고, 높은 동시 처리량을 요구하는 시스템에서는 스레드를 생성하면서 발생하는 대기 시간 때문에 응답 지연이 발생한다.
  • 이런 문제를 해결하기 위해선 스레드 풀(Thread Pool)을 사용해야 한다. 스레드 풀을 사용하면 애플리케이션 내에서 사용할 총 스레드 수를 제한할 수 있고, 기존에 생성된 스레드를 재사용하므로 빠른 응답이 가능하다.
  • java.util.concurrent 패키지의 ExecutorService를 사용하면 안전하게 스레드 풀을 사용할 수 있다.
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

fun main() {
    val pool: ExecutorService = Executors.newFixedThreadPool(5)
    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}")
}

  • 출력 결과를 보면 스레드 이름이 pool-1-thread-{스레드번호} 형태이므로 스레드 풀에서 관리하는 스레드라는 것을 알 수 있다.
  • 스레드 풀에 있는 스레드를 재사용했기 때문에 동일한 스레드 이름으로 여러번 수행된 것을 확인할 수 있다.

 

Future

퓨처(Future)는 비동기 작업에 대한 결과를 얻고 싶은 경우에 사용된다. 예를 들어 수행 시간이 오래 걸리는 작업이나 작업에 대한 결과를 기다리면서 다른 작업을 병행해서 수행하고 싶은 경우에 유용하다. 스레드는 Runable을 사용해 비동기 처리를 하지만 퓨처를 사용해 처리 결과를 얻기 위해선 Callable을 사용한다.

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(1000L, TimeUnit.MILLISECONDS) // 비동기 작업의 결과를 기다린다.
    println(futureResult)
    println("계산 종료")
}

  • 퓨처를 사용하면 비동기 작업을 쉽게 구현할 수 있지만, 몇 가지 단점을 가지고 있다.
  • get 함수는 비동기 작업의 처리가 완료될 때까지 다음 코드로 넘어가지 않고, 무한정 대기하거나 지정해둔 타임아웃 시간까지 블로킹 된다.
  • 또한 퓨처를 사용하면 동시에 실행되는 한 개 이상의 비동기 작업에 대한 결과를 하나로 조합하여 처리하거나 수동으로 완료처리 할 수 있는 방법을 지원하지 않는다.

 

Completable Future

JDK8부터 퓨처의 단점을 극복하기 위해 컴플리터블 퓨처(Completable Future)를 제공한다.

fun main() {
    val completableFuture = CompletableFuture.supplyAsync {
        Thread.sleep(2000)
        sum(100, 200)
    }

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

//    val futureResult = completableFuture.get() // 블로킹 동작

    while (!completableFuture.isDone) {
        Thread.sleep(500)
        println("계산 결과를 집계 중입니다.")
    }
    println("계산 종료")
}

  • thenApplyAsync 함수를 사용해 논블로킹으로 동작하고 뒤에 Async가 붙은 함수들은 supplyAsync와 별도의 스레드 풀을 지정할 수 있다.
  • isDone은 말그대로 컴플리터블 퓨처가 수행 중인 비동기 작업이 완료된 상태인지를 체크한다.
  • 취소상태를 나타내는 isCancelled, 비동기 작업 도중에 에러가 발생한 상태를 나타내는 isCompletedExceptionally도 제공.

 

정리

  • 컴플리터블 퓨처(Completable Future)는 기존의 비동기 처리 방법에 비해 편리하다.
  • 컴플리터블 퓨처(Completable Future)가 만능은 아니지만, 대다수의 비동기 처리 시나리오에서 유용하게 사용할 수 있다. 예를 들어 서버에서 외부의 여러 API 서버를 호출하여 응답을 받아서 결과를 결합하고 처리해야 하는 상황에서 매우 유용할 수 있다.

'Kotlin' 카테고리의 다른 글

[Kotlin] 리액티브 프로그래밍 & 리액티브 스트림  (0) 2022.09.16
[Kotlin] scope function  (0) 2022.06.28
[Kotlin] 람다(lambda)  (0) 2022.06.22
[Kotlin] Collection  (0) 2022.06.19
[Kotlin] 클래스  (0) 2022.06.17
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함