들어가며
해당 글은 도서 <코틀린 코루틴의 정석>을 기반으로 코루틴에 대해 학습하는 시리즈의 글입니다.
코틀린 코루틴 시리즈의 이전 글
[Kotlin] 멀티 스레드와 코루틴
들어가며최근 코루틴을 오랜만에 다시, 그러나 제대로 공부해보았습니다.사실 공부를 제대로 시작하기 전까지는 코루틴 사용 방법과 주요 특징만 제대로 알고 있었고, 정확한 내부 동작은 잘
walnut-dev.tistory.com
2. [Kotlin] CoroutineDispatcher란?
[Kotlin] CoroutineDispatcher란?
들어가며해당 글은 도서 을 기반으로 코루틴에 대해 학습하는 시리즈의 글입니다. 코틀린 코루틴 시리즈의 이전 글[Kotlin] 멀티 스레드와 코루틴 [Kotlin] 멀티 스레드와 코루틴들어가며최근 코루
walnut-dev.tistory.com
[Kotlin] 코루틴 빌더와 Job
들어가며해당 글은 도서 을 기반으로 코루틴에 대해 학습하는 시리즈의 글입니다. 코틀린 코루틴 시리즈의 이전 글 1. [Kotlin] 멀티 스레드와 코루틴 [Kotlin] 멀티 스레드와 코루틴들어가며최근 코
walnut-dev.tistory.com
이전 글에서는 launch 코루틴 빌더와 Job에 대해 살펴보았습니다.
launch를 통해 생성한 코루틴은 작업의 실행과 생명주기를 제어하는 데에는 적합하지만, 작업의 결괏값을 직접 반환하지는 않습니다.
하지만 실제 개발 환경에서는 비동기 작업의 결괏값을 필요로 하는 경우가 매우 빈번합니다.
예를 들어 네트워크 요청을 수행한 뒤 서버로부터 응답을 받아 화면에 반영하거나, 후속 로직에서 해당 값을 활용해야 하는 상황이 이에 해당합니다.
이처럼 코루틴의 실행 결과를 비동기적으로 수신해야 하는 경우를 위해, 코루틴 라이브러리는 async 코루틴 빌더를 제공합니다.
async를 사용하면 코루틴의 실행 결과를 감싸는 객체인 Deferred를 통해 결괏값을 안전하게 전달받을 수 있습니다.
이번 글에서는 async 코루틴 빌더와 Deferred가 무엇이며, 어떻게 코루틴의 결괏값을 수신할 수 있는지에 대해 자세히 살펴보겠습니다.
async와 Deferred
저희가 지금까지 사용해온 launch 함수는 결괏값이 없는 코루틴 객체인 Job을 반환합니다. 이번에 살펴볼 async 함수는 코루틴 작업에 대한 결괏값을 반환받을 수 있는데, 이 때 결괏값은 코루틴 객체 Deferred에 감싸져 반환됩니다.
async 사용해 결과값 수신하기
async 살펴보기
async 코루틴 빌더는 launch와 매우 유사한 형태를 가지고 있습니다.
먼저 async 함수의 시그니처를 살펴보겠습니다.
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
함수 시그니처만 보아도 launch와 많은 부분이 닮아 있다는 것을 알 수 있습니다.
launch와 async의 공통점
- context 인자를 통해 CoroutineDispatcher를 설정할 수 있습니다.
- start 인자로 CoroutineStart.LAZY를 지정하여 코루틴을 지연 시작할 수 있습니다.
- 코루틴에서 실행할 로직을 block 람다식으로 전달합니다.
- 둘 다 코루틴을 추상화한 객체를 생성합니다.
launch와 async의 차이점
두 코루틴 빌더의 가장 큰 차이점은 결괏값의 존재 여부입니다.
- launch
- 코루틴이 결괏값을 직접 반환할 수 없습니다.
- Job 객체를 반환합니다.
- async
- 코루틴이 결괏값을 직접 반환할 수 있습니다.
- Deferred<T> 객체를 반환합니다.
Deferred도 Job처럼 코루틴을 추상화한 객체이며 코루틴으로부터 생성된 결괏값을 감싸는 역할을 수행합니다.
코루틴 결괏값의 타입은 제네릭 타입인 T로 표현됩니다.
Deferred의 제네릭 타입은 명시적으로 지정할 수도 있고, async 블록 내부에서 반환하는 값에 의해 자동으로 추론되기도 합니다.
아래 코드에서 async 블록의 실행 결과로 networkDeferred는 String 타입의 결괏값을 감싼 Deferred 인스턴스를 반환받습니다.
val networkDeferred: Deferred<String> = async(Dispatchers.IO) {
delay(1000L) // 네트워크 요청(1000ms 걸린다고 가정)
return "Dummy Response" // 결괏값 반환
}
위 코드에서 async 블록으로 만든 코루틴은 문자열 "Dummy Response"를 반환하며, 이 값은 Deferred 객체에 의해 감싸져 외부(networkDeferred)로 전달됩니다.
await()을 활용해 결괏값 수신하기
Deferred 객체는 미래의 어느 시점에 결괏값이 반환될 수 있음을 표현하는 코루틴 객체입니다.
async로 생성된 코루틴은 실행이 완료되는 시점에 결괏값을 반환하지만, 비동기 작업의 특성상 그 시점을 정확히 예측할 수는 없습니다.
따라서 코루틴의 결괏값이 필요한 시점에서는, 해당 결괏값이 준비될 때까지 대기하는 과정이 필요합니다.
이를 위해 Deferred 객체는 결괏값 수신을 위한 await() 함수를 제공합니다.
await() 함수의 동작 방식은 다음과 같습니다.
- await()은 대상이 된 Deferred 코루틴이 실행을 완료할 때까지 await()을 호출한 코루틴을 일시 중단합니다.
- Deferred 코루틴의 실행이 완료되면 결괏값을 반환하고 일시 중단되었던 호출부 코루틴을 다시 재개합니다.
이러한 동작 방식은 Job 객체의 join() 함수와 매우 유사합니다.
즉, join()이 코루틴의 완료 시점을 기다린다면, await()은 코루틴의 완료 시점과 결괏값을 함께 기다린다고 이해할 수 있습니다.
아래는 async를 사용해 Deferred 객체를 생성하고 결괏값을 반환하는 예시 코드입니다.
fun main() = runBlocking<Unit> {
val networkDeferred: Deferred<String> = async(Dispatchers.IO) {
delay(1000L)
return@async "Dummy Response"
}
val result = networkDeferred.await()
// networkDeferred로부터 결괏값이 반환될 때까지 runBlocking 코루틴을 일시 중단
println(result)
}
위 코드에서 await()이 호출되면 runBlocking 코루틴은 networkDeferred 코루틴이 실행을 완료하고 결괏값을 반환할 때까지 일시 중단됩니다. 이후 결괏값이 준비되면 해당 값을 반환받아 다음 로직을 이어서 실행하게 됩니다.
이처럼 await()을 사용하면 코루틴의 결괏값을 전달받을 수 있습니다.
Deferred는 특수한 형태의 Job이다
Deferred 객체는 Job 객체의 특수한 형태라고 볼 수 있습니다. 실제로 Deferred 인터페이스는 Job 인터페이스의 서브타입으로 선언되어 있습니다.
public interface Deferred<out T> : Job {
public suspend fun await(): T
// ...
}
위 정의를 보면 알 수 있듯, Deferred는 Job이 제공하는 모든 기능을 그대로 상속받고 있으며 여기에 코루틴의 결괏값을 수신하기 위한 기능만이 추가된 형태입니다. 즉, Deferred 객체는 결괏값을 가질 수 있는 Job이라고 이해하셔도 무방합니다.
이러한 구조로 인해 Deferred 객체는 Job 객체가 제공하는 모든 함수와 프로퍼티를 동일하게 사용할 수 있습니다.
- join()
- Deferred 코루틴이 완료될 때까지 호출부의 코루틴을 일시 중단합니다.
- cancel()
- 코루틴의 실행을 취소해야 할 경우 호출할 수 있습니다.
- 상태 확인 프로퍼티
- isActive
- isCancelled
- isCompleted
따라서 Deferred는 단순히 결괏값을 반환하기 위한 객체가 아니라, Job과 동일하게 코루틴의 생명주기와 상태를 제어할 수 있는 코루틴 추상화 객체입니다.
복수의 코루틴으로부터 결괏값 수신하기
실제 서비스에서는 여러 비동기 작업으로부터 결괏값을 수신한 뒤 이를 병합해야 하는 경우도 자주 발생합니다. 이러한 상황에서는 복수의 코루틴을 생성하고 각 코루틴의 결괏값을 취합하는 방식이 필요합니다.
await()을 사용해 복수의 코루틴으로부터 결괏값 수신
예를 들어 콘서트를 개최하면서 관람객을 두 개의 플랫폼에서 모집한다고 가정해보겠습니다. 각 플랫폼에 등록된 관람객 목록을 각각 조회한 뒤, 최종적으로 하나의 목록으로 병합해야 합니다. 다음은 이러한 요구사항을 단순하게 구현한 코드입니다.
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)}]")
println("참여자 목록: ${listOf(*participants1, *participants2)}")
}
/* 실행 결과
걸린 시간: [2019ms]
참여자 목록: [James, Jason, Jenny]
*/
겉보기에는 두 개의 코루틴을 사용하고 있으므로 비동기 처리가 이루어지고 있는 것처럼 보이지만, 이 코드에는 명확한 문제점이 존재합니다.
- participantDeferred1.await()가 호출되면서, 한 플랫폼 서버로부터 결괏값이 반환될 때까지 호출부 코루틴이 일시 중단됩니다.
- 이 대기 시간 동안 participantDeferred2 코루틴은 아직 생성조차 되지 않았으므로 실행될 수 없습니다.
- 결국 첫 번째 요청이 완료된 이후에야 두 번째 코루틴이 실행되며, 전체 흐름은 순차 실행과 동일하게 동작합니다.
즉, 서로 독립적인 두 작업을 동시에 처리할 수 있음에도 불구하고 불필요하게 순차적으로 처리하고 있어 매우 비효율적인 코드가 됩니다.
await() 호출 시점 조정하기
이 문제는 await()를 호출하는 위치를 조정하는 것만으로도 쉽게 개선할 수 있습니다. 두 코루틴을 모두 먼저 실행한 뒤, 이후에 각각의 결괏값을 수신하도록 변경해보겠습니다.
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)}]")
println("참여자 목록: ${listOf(*participants1, *participants2)}")
}
/* 실행 결과
걸린 시간: [1021ms]
참여자 목록: [James, Jason, Jenny]
*/
위 코드에서는 participantDeferred1.await()가 호출되기 전에 participantDeferred2 코루틴이 이미 생성되어 실행됩니다.
그 결과 두 코루틴은 거의 동시에 실행되며, 전체 실행 시간은 약 1초 수준으로 줄어듭니다.
실행 흐름을 정리하면 다음과 같습니다.
- 두 개의 async 코루틴이 먼저 생성되어 동시에 실행됩니다.
- runBlocking 코루틴이 participantDeferred1.await()을 호출하면서 일시 중단됩니다.
- 첫 번째 코루틴이 완료되면 재개되어 participantDeferred2.await()을 호출하고 다시 일시 중단됩니다.
- 두 번째 코루틴이 완료되면 재개되어 결괏값을 출력하고 종료됩니다.
이처럼 await()의 호출 시점에 따라 코루틴이 병렬로 실행될 수도, 순차적으로 실행될 수도 있습니다. 복수의 비동기 작업을 다룰 때에는 결괏값을 언제 수신할 것인지를 명확히 설계하는 것이 중요합니다.
awaitAll을 사용한 결괏값 수신
앞선 예제에서는 두 개의 Deferred 객체에 대해 각각 await()를 호출했습니다. 하지만 만약 관람객을 모집하는 플랫폼이 두 개가 아니라 열 개라면 어떨까요?
모든 Deferred 객체에 대해 일일이 await()를 호출하는 코드는 가독성도 떨어지고, 유지보수 측면에서도 좋지 않습니다.
이러한 상황을 위해 코루틴 라이브러리는 복수의 Deferred 객체로부터 결괏값을 한 번에 수신할 수 있는 awaitAll() 함수를 제공합니다.
먼저 awaitAll() 함수의 시그니처를 살펴보겠습니다.
public suspend fun <T> awaitAll(vararg deferreds: Deferred<T>): List<T>
awaitAll() 함수의 특징은 다음과 같습니다.
- 가변 인자로 Deferred 타입의 객체들을 전달받습니다.
- 인자로 전달된 모든 Deferred 코루틴의 실행이 완료될 때까지 호출부 코루틴을 일시 중단합니다.
- 모든 결괏값이 수신되면 각 Deferred 코루틴으로부터 반환된 결괏값을 List<T> 형태로 반환하고 호출부 코루틴을 재개합니다.
앞선 예제를 awaitAll()을 사용하도록 변경하면 다음과 같이 작성할 수 있습니다.
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)}]")
println("참여자 목록: ${listOf(*results[0], *results[1])}")
}
/* 실행 결과
걸린 시간: [1013ms]
참여자 목록: [James, Jason, Jenny]
*/
위 코드에서도 두 개의 코루틴은 동시에 실행되며, awaitAll()은 두 코루틴 모두로부터 결괏값이 반환될 때까지 호출부 코루틴을 일시 중단합니다. 그리고 모든 결괏값이 준비되면 이를 하나의 List로 묶어 반환합니다.
awaitAll()을 사용하면 복수의 비동기 작업 결과를 다룰 때 코드를 훨씬 간결하게 작성할 수 있습니다.
또한 awaitAll() 함수는 가변 인자를 받는 형태뿐만 아니라 Collection 인터페이스에 대한 확장 함수로도 제공됩니다.
public suspend fun <T> Collection<Deferred<T>>.awaitAll(): List<T>
이 확장 함수를 사용하면, List<Deferred<T>>와 같이 컬렉션 형태로 관리되는 Deferred 객체들에 대해서도 awaitAll()을 자연스럽게 호출할 수 있습니다.
플랫폼의 개수가 동적으로 변하거나, 반복문을 통해 여러 개의 코루틴을 생성하는 경우에는 이 확장 함수 형태의 awaitAll()이 특히 유용하게 사용됩니다.
withContext로 async–await 대체하기
코루틴에서는 async와 await()을 조합해 비동기 작업의 결괏값을 수신할 수 있습니다. 하지만 모든 경우에 async–await 쌍을 사용하는 것이 가장 적절한 선택은 아닙니다.
단일 비동기 작업을 수행하고 그 결과를 바로 사용해야 하는 경우라면, withContext() 함수를 사용해 async–await 패턴을 대체할 수 있습니다.
먼저 withContext() 함수의 시그니처를 살펴보겠습니다.
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T,
): T
withContext 함수의 동작 방식은 다음과 같습니다.
- withContext()가 호출되면, 인자로 전달된 CoroutineContext를 사용해 block 람다식을 실행합니다.
- block 람다식의 실행이 완료되면, 해당 람다식의 결괏값을 반환합니다.
- 이후 코루틴은 다시 기존의 CoroutineContext로 돌아와 실행을 재개합니다.
이러한 흐름은 async로 코루틴을 생성한 뒤 await()로 결괏값을 수신하는 동작과 매우 유사합니다.
차이점은 withContext는 별도의 코루틴 객체를 생성하지 않고, 하나의 일시 중단 함수 호출로 컨텍스트 전환과 결괏값 반환을 모두 처리한다는 점입니다.
async–await 사용 예제
fun main() = runBlocking<Unit> {
val networkDeferred: Deferred<String> = async(Dispatchers.IO) {
delay(1000L)
return@async "Dummy Response"
}
val result = networkDeferred.await()
// networkDeferred로부터 결괏값이 반환될 때까지 runBlocking 코루틴을 일시 중단
println(result)
}
위 코드에서는 async를 통해 새로운 코루틴을 생성하고, await()를 호출해 해당 코루틴의 결괏값을 수신합니다.
withContext 사용 예제
같은 동작을 withContext로 작성하면 다음과 같습니다.
fun main() = runBlocking<Unit> {
val result: String = withContext(Dispatchers.IO) {
delay(1000L)
return@withContext "Dummy Response"
}
println(result)
}
이 예제에서는 withContext가 호출되면서 Dispatchers.IO 컨텍스트에서 작업을 수행하고, 작업이 완료되면 즉시 결괏값을 반환받아 이후 로직을 이어서 실행합니다.
단일 작업의 결과를 바로 사용해야 하는 경우라면, async–await보다 withContext가 더 간결하고 의도가 명확한 선택이 될 수 있습니다.
withContext의 동작 방식
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 블록을 실행하는 스레드는 main입니다.
- withContext 블록을 실행하는 스레드는 DefaultDispatcher-worker-1 입니다.
- 스레드는 다르지만 코루틴 ID는 동일합니다.
즉, withContext는 새로운 코루틴을 생성하지 않고, 기존 코루틴을 유지한 상태에서 실행 컨텍스트만 변경하여 코드를 실행합니다.
이 예제에서는 Dispatchers.IO로 컨텍스트가 변경되었기 때문에 백그라운드 스레드에서 코드가 실행됩니다.
저희는 아직 CoroutineContext에 대해서는 자세히 다루지 않았습니다.
우선은 “CoroutineContext가 변경된다”는 것을 “코루틴을 실행시키는 CoroutineDispatcher가 변경되어 실행 스레드가 바뀐다”라고 이해하시면 됩니다.
이처럼 withContext를 통해 실행 중인 코루틴의 실행 환경이 변경되는 것을 컨텍스트 스위칭(Context Switching)이라고 합니다.
withContext의 block 람다식이 실행되는 동안에는 전달된 CoroutineContext가 적용되고, 람다식을 벗어나면 다시 원래의 CoroutineContext로 돌아와 코루틴이 재개됩니다.
async–await와 withContext의 내부 동작 비교
이번에는 동일한 작업을 async–await로 수행했을 때의 동작을 살펴보겠습니다.
fun main() = runBlocking<Unit> {
println("[${Thread.currentThread().name}] runBlocking 블록 실행")
async(Dispatchers.IO) {
println("[${Thread.currentThread().name}] async 블록 실행")
}.await()
}
/* 실행 결과
[main @coroutine#1] runBlocking 블록 실행
[DefaultDispatcher-worker-1 @coroutine#2] async 블록 실행
*/
이 경우에는 다음과 같은 차이가 발생합니다.
- runBlocking 내부에서 async(Dispatchers.IO)가 호출되면서 백그라운드 스레드에서 실행되는 새로운 코루틴이 생성
- 새로 생성된 코루틴은 기존 코루틴과 다른 코루틴 ID를 가짐
withContext 사용 시 주의점
앞서 살펴본 것처럼 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
*/
위 코드에서는 다음과 같은 흐름으로 실행됩니다.
- 첫 번째 withContext(Dispatchers.IO)가 실행되어 1초간 대기한 뒤 "Hello"를 반환
- 첫 번째 블록이 완전히 종료된 이후에 두 번째 withContext(Dispatchers.IO)가 실행되어 다시 1초간 대기한 뒤 "World"를 반환
- 결과적으로 두 작업은 순차적으로 실행되어 전체 실행 시간은 약 2초 소요됨
이 경우 두 작업은 서로 의존성이 없으며 동시에 실행될 수 있는 작업입니다.
따라서 withContext 대신 새로운 코루틴을 생성하는 async–await 패턴을 사용하는 것이 적절합니다.
위 코드를 async와 awaitAll()을 사용해 개선해보면 아래와 같습니다.
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]}")
}
/* 실행 결과
[지난 시간: 1014ms] Hello World
*/
위 코드에서는 helloDeferred와 worldDeferred 코루틴이 각각 생성되어 백그라운드 스레드에서 동시에 실행됩니다.
이후 awaitAll()이 호출되면서 두 코루틴이 모두 완료될 때까지 대기하고 결괏값을 한 번에 수신하게 됩니다.
정리하면 다음과 같습니다.
- withContext
- 새로운 코루틴을 생성하지 않습니다.
- 여러 번 호출하면 순차적으로 실행됩니다.
- 단일 작업의 컨텍스트 전환에 적합합니다.
- async–await
- 새로운 코루틴을 생성합니다.
- 여러 작업을 병렬로 실행할 수 있습니다.
- 복수의 독립적인 비동기 작업을 처리할 때 적합합니다.
이처럼 withContext의 특성을 이해하지 못한 채 사용하면 의도치 않게 코루틴을 동기적으로 실행하게 될 수 있으므로, 사용 목적에 맞는 코루틴 빌더를 선택하는 것이 중요합니다.
마치며
지금까지 async와 Deferred, 그리고 withContext를 활용해 코루틴에서 비동기 작업을 처리하는 방법을 알아보았습니다.
효율적인 비동기 처리를 위해서는 독립적인 작업인지, 결괏값이 다른 값에 의존적인지 등 여러 상황을 고려해서 호출 시점과 수신 시점을 적절히 설정하는 것이 중요합니다.
다음 시리즈에서는 CoroutineContext에 대해서 알아보겠습니다.