launch 코루틴 빌더를 통해 생성되는 코루틴 → 작업 실행 후 결과를 반환하지 않음
하지만 네트워크 통신과 같이 코루틴을 통해 비동기로 실행하는 작업의 경우 결과를 수신받아야 하는 경우가 많음
코루틴 라이브러리는 비동기 작업으로 부터 결과를 수신해야 하는 경우를 위해 async 코루틴 빌더를 제공함
launch 코루틴 빌더 함수 → 걀괏값이 없는 코루틴 객체인 Job을 반환
async 코루틴 빌더 함수 → 결괏값이 있는 코루틴 객체인 Deferred를 반환
5장에서 다루는 내용
- async-await을 사용해 코루틴으로부터 결괏값 수신하기
- awaitAll 함수를 사용해 복수의 코루틴으로부터 결괏값 수신
- withContext를 사용해 실행 중인 코루틴의 CoroutineContext 변경하기
// async 빌더 함수 선언부
public fun <T> CoroutineScope.asycn(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>
launch 코루틴 빌더는 코루틴에서 결괏값이 반환되지 않기 때문에 Job 객체를 반환함
async 코루틴 빌더는 코루틴에서 결괏값을 담아 반환하기 위해 Deferred 타입의 객체를 반환함
Deferred 객체는 Job 객체와 마찬가지로 코루틴을 추상화한 객체이지만 코루틴으로부터 생성된 결괏값을 감싸는 기능을 추가로 가지며, 이 결괏값의 타입은 제네릭 타입인 T로 표현됨
Deferred 객체의 반환값 타입을 지정하기 위해선 Deferred에 명시적으로 타입을 설정하거나 async 블록의 반환값으로 반환할 결괏값을 설정하면 됨
val networkDeferred: Deferred<String> = async(Dispatchers.IO) {
delay(1000L)
return@async "Dummy Response"
}
Deferred 객체 → 미래의 어느 시점에 결괏값이 반환될 수 있음을 표현하는 코루틴 객체임
코루틴이 실행 완료될 때 결괏값이 발생하므로 언제 결괏값이 반환될지 정확히 알 수 없으며, 결괏값이 필요한 경우 결괏값이 수신될 때까지 대기해야 함
Deferred 객체 → 결괏값의 수신을 위해 await 함수를 제공
await 함수는 await의 대상이 된 Deferred 코루틴이 실행 완료될 때까지 await 함수를 호출한 코루틴을 일시 중단, Deferred 코루틴이 실행 완료되면 결괏값을 반환하고 호출부의 코루틴을 재개함
코루틴의 실행이 완료될 때까지 대기한다는 점에서 Job 객체의 Join함수와 매우 유사하게 동작함
fun main() = runBlocking<Unit> {
val networkDeferred: Deferred<String> = async(Dispatchers.IO) {
delay(1000L)
return@async "Dummy Response"
}
val result = networkDeferred.await()
println(result)
}

networkDeferred.await()를 호출하면 networkDeferred 코루틴이 완료될 때까지 runBlocking 코루틴이 일시 중단됨
이후 networkDeferred 코루틴으로부터 “Dummy Response”가 반환되면 runBlocking 코루틴이 재개되며, “Dummy Response”는 result 변수에 할당됨
println(result)를 통해 “Dummy Response”가 출력됨
4장에서 모든 코루틴 빌더는 Job 객체를 생성한다고 언급했었다.
하지만 await 코루틴 빌더는 Deferred 객체를 반환한다.
그렇다고 await 코루틴 빌더가 특별한 코루틴 객체를 반환하는 것은 아니다.
Deferred 객체는 Job 객체의 특수한 형태로 Deferred 인터페이스는 Job 인터페이스의 서브타입으로 선언된 인터페이스다.
Deferred 객체는 코루틴으로부터 결괏값 수신을 위해 Job 객체에 몇 가지 기능.이 추가되었을 뿐, 여전히 Job 객체의 일종이다.
// Deferred 인터페이스의 선언부
public interface Deferred<out T>: Job {
public suspend fun await(): T
...
}
Deferred 인터페이스가 Job 인터페이스의 서브 타입으로 선언되었기 때문에 Deferred 객체는 Job 객체의 모든 프로퍼티를 사용할 수 있다.
Join을 사용해 Deferred 객체가 완료될 때까지 호출부의 코루틴을 일시 중단할 수도 있도, Deferred 객체가 취소돼야 할 경우 cancel 함수를 호출해 취소할 수 도 있다.
isActive, isCancelled, isCompleted와 같은 상태 프로퍼티도 사용할 수 있다.
정리하자면 Deferred 객체는 결괏값을 받는 기능이 추가된 Job 객체이며, Job 객체의 모든 함수와 변수를 사용할 수 있다. (특수한 형태의 Job)
프로그램을 만들 때 여러 비동기 작업으로부터 결괏값을 반환받아 병합해야 하는 경우가 자주 생긴다.
콘서트 개최 시 관람객을 2개의 플래폼에서 모집하는 상황 → 각 플랫폼에 등록된 관람객을 조화한 후 병합해야 함
fun main() = runBlocking<Unit> {
val startTime = System.currentTimeMillis()
val participantDeferred1: Deferred<Array<String>> = async(Dispatchers.IO) {
delay(1000L)
return@async arrayOf("James", "Jason")
}
val participants1 = participantDeferred1.await()
val participantDeferred2: Deferred<Array<String>> = async(Dispatchers.IO) {
delay(1000L)
return@async arrayOf("Jenny")
}
val participants2 = participantDeferred2.await()
println("[${getElapsedTime(startTime)}] 참여자 목록: ${listOf(*participants1, *participants2)}")
}
// [지난 시간" 2018ms] 참여자 목록: [Jamse, Jason, Jenny]
vararg 인자로 펼칠 때 필요코드의 실행결과를 통해 참여자 목록이 병합되고 각 서버를 호출하는데 1초씩 걸려 총 2초의 시간이 걸리는 것을 확인할 수 있음

서버 호출이 총 2초가 걸린 이유 → await을 호출하면 결괏값이 반환될 때까지 호출부의 코루틴이 일시 중단되기 때문
Dispatchers.IO를 사용해 백그라운드 스레드에서 코루틴을 실행하더라도 await를 호출하면 코루틴이 실행 완료될 때까지 runBlocking 코루틴이 일시 중단돼 대기하게 됨
따라서 participantDeferred1.await()가 participantDeferred2 코루틴이 생성되기 전에 호출되면 participantDeferred2 코루틴은 순차적으로 처리됨
두 작업은 동시에 독립적으로 실행될 수 있는 작업임에도 순차적으로 처리되는 건 비효율적임
이 문제를 해결하려면 await()의 호출 시점을 두 코루틴이 모두 생성된 이후로 변경하면 된다.
fun main() = runBlocking<Unit> {
val participantDeferred1: Deferred<Array<String>> = async(Dispatchers.IO) {
delay(1000L)
return@async arrayOf("James", "Jason")
}
val participantDeferred2: Deferred<Array<String>> = async(Dispatchers.IO) {
delay(1000L)
return@async arrayOf("Jenny")
}
val participants1 = participantDeferred1.await()
val participants2 = participantDeferred2.await()
println("[${getElapsedTime(startTime)}] 참여자 목록: ${listOf(*participants1, *participants2)}")
}
// [지난 시간" 1018ms] 참여자 목록: [Jamse, Jason, Jenny]
participantDeferred1.await()가 호출되기 전에 participantDeferred2 코루틴이 실행되므로 두 코루틴이 동시에 실행됨

두 코루틴이 동시에 실행될 수 있도록하는 것은 코루틴의 성능 측면에서 매우 중요함
await의 호출 시점에 따라 코루틴이 순차적으로 처리될 수도 있고 동시에 처리될 수 있을을 유의해야 한다.
앞의 콘서트 관람객 예시에서는 관람객을 등록받는 플랫폼의 개수가 적었다. 만약 플랫폼의 개수가 100개로 늘어난다면 어떨까?
각 코루틴의 결과를 반환받기 위해 100개의 await()를 호출해야할까? 다행히도 그렇지 않다.
코루틴 라이브러리는 복수의 Deferred 객체로부터 결괏값을 수신하기 위한 awaitAll() 함수를 제공한다.
public suspend fun <T> awaitAll(vararg deferreds: Deferred<T>): List<T>
awaitAll 함수의 구현부를 보면 가변 인자로 Deferred 타입의 객체를 받아 인자로 받은 모든 Deferred 코루틴으로부터 결과가 수신될 때까지 호출부의 코루틴을 일시 중단한 후 결과가 모두 수신되면 Deferred 코루틴들로부터 수신한 결괏값들로 List를 만들어 반환하고 호출부의 코루틴을 재개한다.
participantDeferred1과 participantDeferred2의 예시에 awaitAll() 함수를 적용해보자
fun main() = runBlocking<Unit> {
val startTime = System.currentTimeMillis()
val participantDeferred1: Deferred<Array<String>> = async(Dispatchers.IO) {
delay(1000L)
arrayOf("James", "Jason")
}
val participantDeferred2: Deferred<Array<String>> = async(Dispatchers.IO) {
delay(1000L)
arrayOf("Jenny")
}
val results: List<Array<String>> = awaitAll(participantDeferred1, participantDeferred2)
println("[${getElapsedTime(startTime)}] 참여자 목록: ${listOf(*results[0], *results[1])}")
}
// [지난 시간: 1013ms] 참여자 목록: [Jamse, Jason, Jenny]

runBlocking 코루틴에서 awaitAll() 함수가 호출되면 awaitAll() 함수의 인자로 전달된 코루틴들의 실행이 모두 완료될 때까지 runBlocking 코루틴이 일시 중단된다.
이후 인자로 전달된 코루틴들의 실행이 완료되면 결과들이 리스트로 만들어져 반화되고 runBlocking의 코루틴이 재개된다.
awaitAll 함수 → Collection 인터페이스에 대한 확장 함수로도 제공함
public suspend fun <T> Collection<Deferred<T>>.awaitAll(): List<T>
Collection<Deferred>에 대해 awaitAll() 함수를 호출하면 컬렉션에 속한 Deferred 객체들의 코루틴이 모두 완료돼 결괏값을 반환할 때까지 대기한다.
fun main() = runBlocking<Unit> {
val startTime = System.CurrentTimeMillis()
val participantDeferred1: Deferred<Array<String>> = async(Dispatchers.IO) {
delay(1000L)
arrayOf("James", "Jason")
}
val participantDeferred2: Deferred<Array<String>> = async(Dispatchers.IO) {
delay(1000L)
arrayOf("Jenny")
}
val results: List<Array<String>> = listof(participantDeferred1, participantDeferred2).awaitAll()
println("[${getElapsedTime(startTime)}] 참여자 목록: ${listOf(*results[0], *results[1])}")
}
// [지난 시간: 1015ms] 참여자 목록: [Jamse, Jason, Jenny]
가변 인자를 받는 awaitAll() 함수를 사용한 것과 완전히 같게 동작한다.
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T
withContext 함수가 호출되면 함수의 인자로 설정된 CoroutineContext 객체를 사용해 block 람다식을 실행하고, 완료되면 그 결과를 반환한다.
withContext 함수를 호출한 코루틴은 인자로 받은 CoroutineContext 객체를 사용해 block 람다식을 실행하며, block 람다식을 모두 실행하면 다시 기존의 CoroutineContext 객체를 사용해 코루틴이 재개된다.
이 동작은 async-await 쌍을 연속적으로 실행했을 때의 동작과 매우 유사하다.
fun main() = runBlocking<Unit> {
val networkDeferred: Deferred<String> = async(Dispatchers.IO) {
delay(1000L)
"Dummy Response"
}
val result = networkDeferred.await()
println(result)
}
// Dummy Response
이 코드에서는 async 함수를 호출해 Deferred 객체를 만들고, 곧바로 Deferred 객체에 대해 await 함수를 호출한다.
이런 async 함수를 호출한 후 연속적으로 await 함수를 호출해 연속적으로 결괏값 수신을 대기하는 코드는 withContext 함수로 대체할 수 있다.
fun main() = runBlocking<Unit> {
val result: String = withContext(Dispatchers.IO) {
delay(1000L)
return@withContext "Dummy Response"
}
println(result)
}
// Dummy Response
async-await 쌍이 withContext 함수로 대체되면 Deferred 객체가 생성되는 부분이 없어지고 “Dummy Response”가 결과로 바로 반환된다.
withContext 함수는 async-await 쌍과 비슷하게 동작하는 것처럼 보이지만 내부적으로 보면 다르게 동작한다.
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-1 @coroutine#1] withContext 블록 실행
*/
코드의 실행 결과를 보면 runBlocking 함수의 block 람다를 실행하는 스레드와 withContext 함수의 block 람다식을 실행하는 스레드는 main과 DefaultDispatcher-worker-1 로 다르지만 코루틴은 coroutine#1으로 같은 것을 확인할 수 있다.
withContext 함수는 새로운 코루틴을 만드는 개신 기존의 코루틴에서 CoroutineContext 객체만 바꿔서 실행하는 것을 알 수 있다.
위의 예시에서는 CoroutineContext 객체가 Dispatchers.IO로 바뀌었기 때문에 백그라운드 스레드에서 실행됐다.
다만 CoroutineContext 객체에 대해 아직 다루지 않았기 때문에 “CoroutineContext가 변경됐다.”는 표현을 “코루틴을 실행시키는 CoroutineDispatcher 객체가 변경돼 코루틴의 실행 스레드가 변화한다.”라고 이해하는 것이 좋다.
CoroutineContext 객체에 대해서는 추후에 다룰 예정이다.
withContext 함수가 호출되면 실행 중인 코루틴의 실행 환경이 withContext 함수의 context 인자 값으로 변경돼 실행되며, 이를 컨텍스트 스위칭(context switching)이라고 부른다.
context 인자로 coroutineDispatcher 객체가 넘어오면 코루틴은 해당 CoroutineDispatcher 객체를 사용해 다시 실행된다.

withContext 함수는 함수의 block 람다식이 실행되는 동안 코루틴의 실행 환경을 변경시킨다.
withContext 함수가 block 람다식을 벗어나면 다시 원래의 CoroutineContext 객체를 사용해 실행된다.
fun main() = runBlocking<Unit> {
println("[${Thread.currentThread().name]] runBlocking 블록 실행")
async(Dispatchers.IO) {
println("[${Thread.currentThread().name]] withContext 블록 실행")
}.await()
}
/*
[main @coroutine#1] runBlocking 블록 실행
[DefaultDispatcher-worker-1 @coroutine#2] withContext 블록 실행
*/
이 코드의 실행 결과를 보면 async 블록을 실행하는 코루틴은 coroutine#2로 runBlocking 블록을 실행하는 coroutine#1과 다른 것을 확인할 수 있다.
async-await 쌍을 사용하면 새로운 코루틴을 만들지만 await 함수가 호출돼 순차 처리 되기 때문에 동기적으로 실행된다.

withContext 함수 → 새로운 코루틴을 만들지 않기 때문에 하나의 코루틴에서 withContext 함수가 여러 번 호출되면 순차적으로 실행됨 (withContext 여러개를 한 코루틴에서 호출하면 성능 문제가 발생할 수 있음)
fun main() = runBlocking<Unit> {
val startTime = System.currentTimeMillis()
val helloString = withContext(Dispatchers.IO) {
delay(1000L)
return@withContext "Hello"
}
val worldString = withContext(Dispatchers.IO) {
delay(1000L)
return@withContext "World"
}
println("[${getElapsedTime(startTime)} ${helloString} ${worldString}")
}
// [지난 시간: 2018ms] Hello World
“Hello” 문자열과 “World”문자열을 반환하는 두 코루틴이 순차 처리되어 총 2초의 실행 시간이 걸린 것을 확인할 수 있다.

위의 코드에선 runBlocking 함수에 의해 하나의 코루틴만 생성된다.
이후 withContext 함수를 사용해 코루틴을 유지한 채로 실행 스레드풀만 Dispatchers.IO로 변경된다.
따라서 1초 동안 대기한 후 문자열을 반환받는 두 작업이 한 코루틴에서 순차적으로 처리되어 총 2초 정도의 시간이 걸리는 것이다.
이는 withContext 함수가 새로운 코루틴을 생성하지 않기 때문에 생기는 문제다.
이 문제를 async-await 쌍으로 해결해보자
fun main() = runBlocking<Unit> {
val startTime = System.currentTimeMillis()
val helloDeferred = async(Dispatchers.IO) {
delay(1000L)
return@async "Hello"
}
val worldDeferred = async(Dispatchers.IO) {
delay(1000L)
return@async "World"
}
val results = awaitAll(helloDeferred, worldDeferred)
println("[${getElapsedTime(startTime)}] ${results[0]} ${results[1]}")
}
// [지난 시간: 1013ms] Hello World
HelloDeferred와 WorldDeferred가 모두 실행된 뒤에 awaitAll 함수를 호출해 여러 async 코루틴의 결괏값을 반환받는다.
두 코루틴이 병렬로 처리된 후 한번에 결괏값을 반환받기 때문에 총 1초 정도의 시간이 걸린 것을 확인할 수 있다.

그림에서는 백그라운드 스레드가 2개 사용되었지만 코루틴은 스레드를 사용하지 않을 때는 스레드를 양보하기 때문에 백그라운드 스레드를 하나만 사용할 수도 있다.
withContext 함수를 사용하면 코드가 깔끔해지는 효과를 낼 수 있지만 잘못 사용하면 코루틴이 동기적으로 실행될 수 있어 사용시 주의가 필요하다.
private val myDispatcher1 = newSingleThreadContext("MyThread1")
private val myDispatcher2 = newSingleThreadContext("MyThread2")
fun main() = runBlocking<Unit> {
println("[${Thread.currentThread().name}] 코루틴 실행")
withContext(myDispatcher1) {
println("[${Thread.currentThread().name}] 코루틴 실행")
withContext(myDispatcher2) {
println("[${Thread.currentThread().name}] 코루틴 실행")
}
println("[${Thread.currentThread().name}] 코루틴 실행")
}
println("[${Thread.currentThread().name}] 코루틴 실행")
}
/*
[main @coroutine#1] 코루틴 실행
[MyThread1 @coroutine#1] 코루틴 실행
[MyThread2 @coroutine#1] 코루틴 실행
[MyThread1 @coroutine#1] 코루틴 실행
[main @coroutine#1] 코루틴 실행
*/
코드의 실행 결과를 보면 모든 코루틴이 @coroutine#1인데 스레드 풀만 Main, MyThread1, MyThread2로 변경되는 것을 볼 수 있다.
withContext 함수를 사용하면 CoroutineDispatcher 객체를 통해 코루틴이 실행되는데 사용되는 CoroutineDispatcher 객체를 자유롭게 바꿀 수 있다.
withContext 함수를 통해 변경된 CoroutineDispatcher 객체는 withContext 함수 블록 냅에서만 유효하다. withContext 블록을 벗어나면 다시 이전의 CoroutineDispatcher 객체를 사용하게 되며 스레드가 다시 전환된다.