서론
- launc 코루틴 빌더를 통해 생성되는 코루틴은 기본적으로 작업 실행 후 결과를 반환하지 않지만
- async 코루틴 빌더를 통해 생성된 코루틴으로부터 결괏값을 수신받을 수 있음
- async 함수를 사용하면 결괏값이 있는 코루틴 객체인 Deferred가 반환됨
- Deferred 객체를 통해 코루틴으로부터 결괏값을 수신할 수 있음
async 사용해 결괏값 수신하기
1. async 사용해 Deferred 생성하기
- async 함수는 launch 함수와 유사하지만 다음과 같은 차이점이 존재함
- launch는 코루틴이 결괏값을 직접 반환할 수 없기 때문에 Job 객체를 반환
- async는 코루틴의 결괏값을 직접 반환하고 결괏값을 담아 반환하기 위해 Deferred<T> 타입의 객체를 반환
* Deferred는 Job과 같이 코루틴을 추상화한 객체이지만 코루틴으로부터 생성된 결괏값을 감싸는 기능을 추가로 가지며 결괏값의 타입은 제네릭 타입 T로 표현
2. await를 사용한 결괏값 수신
- Deferred 객체는 미래의 어느 시점에 결괏값이 반환될 수 있음을 표현하는 코루틴 객체
- 코루틴이 실행 완료될 때 결괏값이 반환됨
- 만약 결괏값이 필요하다면 결괏값이 수신될 때까지 대기해야 함
- Deferred 객체는 결괏값 수신의 대기를 위해 await 함수 제공
- await 함수는 await의 대상이 된 Deferred 코루틴이 실행 완료될 때까지 await 함수를 호출한 코루틴을 일시 중단 시킴
- Deferred 코루틴이 실행 완료되면 결괏값을 반환하고 호출부의 코루틴을 재개함
- Job 객체의 join 함수와 매우 유사하게 동작
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun main() = runBlocking<Unit> { | |
val networkDeferred: Deferred<String> = async(Dispatchers.IO) { | |
delay(1000L) | |
return@async "Dummy Response" | |
} | |
val result = networkDeferred.await() | |
println(result) | |
} |

Deferred는 특수한 형태의 Job
- Deferred 객체는 Job 객체의 특수한 형태로 Deferred 인터페이스는 Job 인터페이스의 서브타입으로 선언된 인터페이스
- 따라서 `모든 코루틴 빌더는 Job 객체를 생성한다`라는 명제는 참
- Deferred 객체는 코루틴으로부터 결괏값 수신을 위해 Job 객체에 몇 가지 기능이 추가되었을 뿐, 여전히 Job 객체의 일종
- 이런 특성 때문에 Deferred 객체는 Job 객체의 모든 함수와 프로퍼티를 사용할 수 있음

복수의 코루틴으로부터 결괏값 수신하기
- 개발할 때 여러 비동기 작업으로부터 결괏값을 반환받아 병합해야 하는 케이스가 생김
- 위와 같은 경우에는 복수의 코루틴을 생성해 결괏값을 취합해야 함
1. await를 사용해 복수의 코루틴으로부터 결괏값 수신하기
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun main() = runBlocking<Unit> { | |
val startTime = System.currentTimeMillis() | |
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)}") | |
} | |
fun getElapsedTime(startTime: Long): String = "지난 시간: ${System.currentTimeMillis() - startTime}ms" |

부연 설명
- 앞서 언급했다시피 await를 호출하면 결괏값이 반환될 때까지 호출부의 코루틴이 일시 중단됨
- Dispatchers.IO를 사용해 백그라운드 쓰레드에서 코루틴을 실행하더라도 await를 호출하면 코루틴이 실행 완료될 때까지 runBlocking 코루틴이 일시 중단돼 대기함
- 따라서 participantDeferred1.await()가 participantDeferred2 코루틴이 생성되기 전에 호출되면 participantDeferred1 코루틴과 participantDeferred2 코루틴은 순차적으로 처리됨
- 서로 간에 독립적인 작업을 동시에 처리할 수 있음에도 불구하고 순차적으로 처리하게 되면 매우 비효율적
- 따라서 participantDeferred1 코루틴의 await를 호출하는 위치를 participantDeferred2 코루틴이 실행된 이후로 만들어 동시에 처리하도록 처리
2. awaitAll을 사용한 결괏값 수신하기
- await 함수를 N번 호출해야하는 경우 await 함수를 N 줄에 걸쳐 호출해야 하기 때문에 가독성이 안 좋음
- 위 문제를 해결하기 위해 코루틴 라이브러리는 복수의 Deferred 객체로부터 결괏값을 수신하기 위한 awaitAll 함수를 제공
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun main() = runBlocking<Unit> { | |
val startTime = System.currentTimeMillis() | |
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 results: List<Array<String>> = awaitAll(participantDeferred1, participantDeferred2) | |
println("[${getElapsedTime(startTime)}] 참여자 목록: ${listOf(*results[0], *results[1])}") | |
} |

3. 컬렉션에 대해 awaitAll 사용하기
- Collection<Deferred<T>>에 대해 awaitAll 함수를 호출하면 컬렉션에 속한 Deferred들이 모두 완료돼 결괏값을 반환할 때까지 대기
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun main() = runBlocking<Unit> { | |
val startTime = System.currentTimeMillis() | |
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 results: List<Array<String>> = listOf(participantDeferred1, participantDeferred2).awaitAll() | |
println("[${getElapsedTime(startTime)}] 참여자 목록: ${listOf(*results[0], *results[1])}") | |
} |

withContext
1. withContext로 async-await 대체하기
- 코루틴 라이브러리에서 제공되는 withContext 함수를 사용하면 async-await 작업 대체 가능
- withContext 함수가 호출되면 함수의 인자로 설정된 CoroutineContext 객체를 사용해 block 람다식을 실행하고 완료되면 해당 결과를 반환
- withContext 함수를 호출한 코루틴은 인자로 받은 CoroutineContext 객체를 사용해 block 람다식을 실행하며, block 람다식을 모두 실행하면 다시 기존의 CoroutineContext 객체를 사용해 코루틴이 재개됨
- async-await 쌍을 연속적으로 실행했을 때의 동작과 매우 유사함
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun main() = runBlocking<Unit> { | |
val networkDeferred: Deferred<String> = async(Dispatchers.IO) { | |
delay(1000L) | |
return@async "Dummy Response" | |
} | |
val result = networkDeferred.await() | |
println(result) | |
} | |
// 위 코드와 동일하게 동작 | |
fun main() = runBlocking<Unit> { | |
val result: String = withContext(Dispatchers.IO) { | |
delay(1000L) | |
return@withContext "Dummy Response" | |
} | |
println(result) | |
} |
2. withContext의 동작 방식
- withContext 함수는 겉보기에는 async와 await를 연속적으로 호출하는 것과 비슷하게 동작하지만 내부적으로 보면 다르게 동작함
- async-await 쌍은 새로운 코루틴을 생성해 작업을 처리
- withContext 함수는 실행 중이던 코루틴을 그대로 유지한 채로 코루틴의 실행 환경만 변경해 작업을 처리
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun main() = runBlocking<Unit> { | |
println("[${Thread.currentThread().name}] runBlocking 블록 실행") | |
withContext(Dispatchers.IO) { | |
println("[${Thread.currentThread().name}] withContext 블록 실행") | |
} | |
} |

부연 설명
- runBlocking 함수와 block 람다식을 실행하는 쓰레드와 withContext 함수의 block 람다식을 실행하는 쓰레드는 main과 DefaultDispatcher-worker-1으로 다르지만 코루틴은 coroutine$1으로 같은 것을 확인 가능
- withContext 함수는 새로운 코루틴을 생성하는 대신 기존의 코루틴에서 CoroutineContext 객체만 바꿔서 실행
- withContext 함수가 호출되면 실행 중인 코루틴의 실행 환경이 withContext 함수의 context 인자 값으로 변경돼 실행되며 이를 컨텍스트 스위칭이라고 칭함
- 만약 context 인자로 CoroutineDispatcher 객체가 넘어온다면 코루틴은 해당 CoroutineDispatcher 객체를 사용해 다시 실행됨
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun main() = runBlocking<Unit> { | |
println("[${Thread.currentThread().name}] runBlocking 블록 실행") | |
async(Dispatchers.IO) { | |
println("[${Thread.currentThread().name}] withContext 블록 실행") | |
}.await() | |
} |

부연 설명
- withContext를 호출하면 코루틴이 유지된 채로 코루틴을 실행하는 실행 쓰레드만 변경되기 때문에 동기적으로 실행됨
- async-await 쌍을 사용하면 새로운 코루틴을 만들지만 await 함수를 통해 순차 처리가 돼 동기적으로 실행됨
3. withContext 사용 시 주의점
- 복수의 독립적인 작업이 병렬로 실행돼야 하는 상황에 withContext를 사용할 경우 성능 문제 야기 가능
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}") | |
} |

부연 설명
- runBlocking을 통해 실행된 코루틴은 처음에는 메인 쓰레드에서 실행되는데 withContext(Dispatchers.IO)를 사용하면 코루틴을 유지한 채로 실행 쓰레드만 바뀜
- 따라서 1초 간 대기 후 `Hello`를 반환받고, 다시 1초간 대기 후 `World`를 반환받음
- 각 withContext 블록의 코드를 실행하는 데는 1초 밖에 안 걸리지만 순차적으로 처리돼 2초의 시간이 걸리게 됨
- withContext 함수가 새로운 코루틴을 생성하지 않기 때문에 생기는 문제
- 이 문제를 해결하기 위해서는 withContext를 제거하고, 코루틴을 생성하는 async-await 쌍으로 대체하면서 Deferred 객체에 대한 await 함수 호출을 만든 코루틴이 실행된 뒤에 해야 함
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
fun main() = runBlocking<Unit> { | |
val startTime = System.currentTimeMillis() | |
val helloString = async(Dispatchers.IO) { | |
delay(1000L) | |
return@async "Hello" | |
} | |
val worldString = async(Dispatchers.IO) { | |
delay(1000L) | |
return@async "World" | |
} | |
val results = awaitAll(helloString, worldString) | |
println("[${getElapsedTime(startTime)}] ${results[0]} ${results[1]}") | |
} |

4. 정리
- withContext 함수를 사용하면 코드가 깔끔해 보이는 효과를 내지만 잘 못 사용하게 될 경우 코루틴을 동기적으로 실행하도록 만들어 코드 실행 시간이 배 이상으로 증가할 수 있음
- withContext 함수가 새로운 코루틴을 생성하지 않는다는 것을 명심해야 함
참고
코틀린 코루틴의 정석 (조세영 저)
반응형
'Kotlin > 코틀린 코루틴의 정석' 카테고리의 다른 글
[Kotlin 코루틴] 예외 처리 (0) | 2024.11.22 |
---|---|
[Kotlin 코루틴] 구조화된 동시성 (0) | 2024.11.21 |
[Kotlin 코루틴] CoroutineContext 정리 (0) | 2024.11.19 |
[Kotlin 코루틴] 코루틴 빌더와 Job 정리 (0) | 2024.11.08 |
[Kotlin 코루틴] CoroutineDispatcher 정리 (1) | 2024.11.05 |