async와 Deferred(withContext)

동키·2025년 4월 10일

Kotlin

목록 보기
7/10

해당 내용은코틀린 코루틴의 정석6장 내용을 공부하며 정리한 내용입니다.

기존의 launch 코루틴 빌더는 결과를 반환하지 않습니다.

그렇다면 결과를 수신해야 할 때는 어떤 방법을 사용할가요?

async 코루틴 빌더를 통해 코루틴을 생성하면 생성한 코루틴으로부터 결괏값을 수신받을 수 있습니다.

launch 함수 사용시 결괏값이 없는 Job 객체를 반환했지만 async 함수를 사용하면 결괏값이 있는 코루틴 객체인 Deferred 객체가 반환됩니다.

Deferred 객체를 통해 코루틴으로부터 결괏값을 수신할 수 있습니다.

async 사용해 결괏값 수신하기

async 사용해 Deferred 만들기

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T>

async 함수 또한 launch와 마찬가지로 context를 통해 Dispatcher를 설정할 수 있고 start 인자로 LAZY 를 설정해 코루틴 지연을 설정할 수 있습니다. block 은 코루틴에서 실행할 코드를 작성하는 람다식 입니다.

Deferred 는 Job과 같이 코루틴을 추상화한 객체이지만 코루틴으로 부터 생성된 결괏값을 감싸는 기능을 추가로 가짐

바로 Deferred을 반환하는 객체를 만들어 보겠습니다.

fun main() = runBlocking<Unit> {
   val message: Deferred<String> = async {
       delay(1000L)
       "Hello World!!"
   }
    println(message.await())
}
// Hello World!!

명시적으로 타입 Deferred을 붙여주지 않아도 됩니다.

await를 사용한 결괏값 수신

Deferred 객체는 미래의 어느 시점에 결괏값이 반환될 수 있음을 표현하는 코루틴 객체

Deferred 객체는 결괏값 수신의 대기를 위해 await 함수를 제공한다.

await 함수는 await의 대상이 된 Deferred 코루틴이 실행 완료될 때까지 await 함수를 호출한 코루틴을 일시 중단하며, Deferred 코루틴이 실행 완료되면 결괏값을 반환하고 호출부의 코루틴을 재개한다.

바로 예제를 통해 알아보겠습니다.

fun main() = runBlocking<Unit> {
   val message: Deferred<String> = async {
       delay(1000L)
       return@async "Hello World!!"
   }
    println("메시지 언제오나??")
    val result: String = message.await()
    println(result)
}
메시지 언제오나??
Hello World!!

여기서 주의깊게 봐야할 것은 await 이다.

앞서 Deferred 객체는 미래의 어느 시점에 결괏값이 반환될 수 있음을 표현하는 객체라고 배웠다.

그래서 message의 타입은 async 빌더를 사용했기 때문에 Deferred<String> 이 되는 것이다.

이때 message.await 을 해버리는 순간 result의 type은 String 이 된다.

즉 결괏값이 반환되는 시점이기 때문에 String객체의 데이터(결괏값)를 얻게되는 것이다.


Deferred는 특수한 형태의 Job

Deferred 객체는 결괏값을 가지는 Job으로 비동기 작업의 결과를 미래에 받아올 수 있는 Promise 객체

public interface Deferred<out T> : Job 

위 코드를 보면 Deferred 객체는 Job 인터페이스의 서브타입으로 선언되어 있다.

즉, 결괏값 수신을 위해 Job 객체에서 몇 가지 기능이 추가됐을 뿐, 여전히 Job 객체의 일종이다.

그러므로 Job과 마찬가지로 join(), cancel(), isActive , isCancelled, isCompleted 등 프로퍼티들을 사용할 수 있다.

정리하자면 Deferred 객체는 결괏값을 반환받는 기능이 추가된 Job 객체이며,

Job 객체의 모든 함수와 변수를 사용할 수 있습니다.


복수의 코루틴으로부터 결괏값 수신하기

개발을 하다 보면 여러 비동기 작업으로부터 결괏값을 반환받아 병합해야 하는 경우가 자주 생긴다.

이때 우리는 복수의 코루틴을 생성하고 결괏값을 취합해야 한다.

await를 사용해 복수의 코루틴으로부터 결괏값 수신

fun main() = runBlocking<Unit> {
    val startTime = System.currentTimeMillis()
    val stringList = (1..10).map { value ->
        val result = async {
            delay(1000L)
            return@async "num is $value"
        }.await()
        result
    }
    stringList.forEach { print("$it ") }
    println(getElapsedTime(startTime))
}
num is 1 num is 2 num is 3 num is 4 num is 5
num is 6 num is 7 num is 8 num is 9 num is 10
지난 시간 : 10077ms

여기서 주의깊게 봐야할 점은 지난 시간이 10초가 걸렸다는 것이다.

위 코드는 아주 비효율적인 코드이다.

1부터 10까지 반복문을 돌면서 1일 때 1초쉬고 2일때 2초쉬고… 이런식으로 10초가 흘러간다.

우리가 원하는 것은 1초안에 작업이 끝나는 것이다, 어떻게 할 수 있을가?

awaitAll을 사용한 결괏값 수신

fun main() = runBlocking<Unit> {
    val startTime = System.currentTimeMillis()
    val stringList: List<Deferred<String>> = (1..10).map { value ->
        val result: Deferred<String> = async {
            delay(1000L)
            return@async "num is $value"
        }
        result
    }
    println(stringList.awaitAll())
    println(getElapsedTime(startTime))
}
[num is 1, num is 2, num is 3, num is 4, num is 5, num is 6, num is 7, num is 8, num is 9, num is 10]
지난 시간 : 1040ms

우리가 의도한대로 1초만에 작업이 완료된 것을 확인할 수 있다.

이를 위해 코루틴 라이브러리는 복수의 Deferred 객체로부터 결괏값을 수신하기 위한 awaitAll 함수를 제공합니다.

fun main() = runBlocking<Unit> {
    val startTime = System.currentTimeMillis()
    val participantDeferred1: Deferred<Array<String>> = async {
        delay(1000L)
        arrayOf("길동", "옥지")
    }
    val participantDeferred2: Deferred<Array<String>> = async {
        delay(1000L)
        arrayOf("옥순", "빵빵")
    }
    val result = awaitAll(participantDeferred1, participantDeferred2)
    println(result)
    println(getElapsedTime(startTime))
}

이런식으로도 가능하다

즉, awaitAll 함수에 값을 집어넣거나, Collection<Deferred<T>>.awaitAll() 컬렉션의 awaitAll을 사용할 수 있다.


withContext

일시적으로 CoroutineContext를 전환하는 suspend 함수

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T 

withContext로 async - await 대체하기

fun main() = runBlocking<Unit> {
    val networkDeferred: Deferred<String> = async(Dispatchers.IO) { 
        delay(1000L)
        return@async "Dummy Response"
    }
    val result = networkDeferred.await()
}

해당 코드를 withContext 로 대체해 보자

fun main() = runBlocking<Unit> {
    val result: String = withContext(Dispatchers.IO) {
        delay(1000L)
        return@withContext "Dummy Response"
    }
    println(result)
}

withContext의 동작 방식

async - await 는 새로운 코루틴을 생성해 작업을 처리하지만 withContext 함수는 실행 중이던 코루틴을 그대로 유지한 채로 코루틴의 실행 환경만 변경해 작업을 처리한다

fun main() = runBlocking<Unit> {
    println("[${Thread.currentThread().name} runBlocking]")
    withContext(Dispatchers.IO) {
        println("[${Thread.currentThread().name} withContext]")
    }
}
[main @coroutine#1 runBlocking]
[DefaultDispatcher-worker-2 @coroutine#1 withContext]

출력에서 같은 coroutine 에서 실행된 것을 확인할 수 있다.

즉, 실행되는 스레드는 다르지만 코루틴은 coroutine#1으로 같은 것을 볼 수 있다.

withContext 함수는 새로운 코루틴을 만드는 대신 기존의 코루틴에서 CoroutineContext 객체만 바꿔서 실행된다. 여기서 Dispatcher를 IO로 바꾸었기 때문에 백그라운드 스레드에서 실행되었다.

동작원리에 대해서 알아보겠습니다.

처음 메인 스레드에 coroutine#1 이 있습니다. withContext(Dispatchers.IO) 로 인해 IO 작업 대기열에 coroutine#1 이 옮겨지고 백그라운드 스레드에서 실행되게 됩니다.

이렇게 메인에서 실행 중인 코루틴의 실행 환경이 withContext 의 context 인자 값으로 변경돼 실행되며,

이를 컨텍스트 스위칭 (Context Switching)이라고 부릅니다.

이처럼 withContext 함수는 함수의 block 람다식이 실행되는 동안 코루틴의 실행 환경을 변경시킵니다.

withContext 사용 시 주의점

복수의 독립적인 작업이 병렬로 실행돼야 하는 상황에 withContext 를 사용할 경우 성능에 문제를 일으킬 수 있습니다.

fun main() = runBlocking<Unit> {
    val startTime = System.currentTimeMillis()
    val hello = withContext(Dispatchers.IO) {
        delay(1000L)
        "hello"
    }
    val dongkyung = withContext(Dispatchers.IO) {
        delay(1000L)
        "dongkyung"
    }
    val result = "$hello $dongkyung"
    println(result)
    println(getElapsedTime(startTime))
}
hello dongkyung
지난 시간 : 2052ms

해당 코드를 보면 1초만에 할 수 있는 작업을 withContext 를 사용해 순차적으로 처리돼 2초가 걸렸다.

이를 해결하기 위해선 withContext 를 지우고 코루틴을 생성하는 async - await 쌍으로 대체해야 한다.

즉, withContext 함수 사용시 코드가 깔끔해 보이는 효과를 내지만 잘못 사용하게 되면 코루틴을 동기적으로 실행하도록 만들어 코드 실행 시간이 배 이상으로 증가하여 효율성이 떨어지게 된다.

withContext 는 새로운 코루틴을 만들지 않는다!!

profile
오늘 하루도 화이팅

0개의 댓글