launch 코루틴 빌더를 통해 생성되는 코루틴은 기본적으로 작업 실행 후 결과를 반환하지 않는다. 그러나 네트워크 통신을 실행하고 응답을 받아 처리해야 할 경우 네트워크 통신을 실행하는 코루틴으로부터 결과를 수신받아야 한다.
async 코루틴 빌더는 코루틴으로부터 결과값을 수신받을 수 있도록 한다.
launch 함수를 사용하면 결과값이 없는 코루틴 객체인 Job이 반환되지만, async 함수를 사용하면 결과값이 있는 코루틴 객체인 Deferred가 반환되며, Deferred 객체를 통해 코루틴으로부터 결과값을 수신할 수 있다.
launch 코루틴 빌더와 async 코루틴 빌더는 매우 비슷하다.
async는 launch와 달리 코루틴에서 결과값을 반환받기 때문에 Deferred<T> 타입의 객체를 반환한다.
Deferred의 제네릭 타입을 지정하기 위해서는 Deferred에 명시적으로 타입을 설정하거나 async 블록의 반환값으로 반환할 결과값을 설정하면 된다.
val networkDeferred: Deferred<String> = async(Dispatchers.IO) {
delay(1000L)
return@async "Dummy Response" // 결과값 반환
이 코드에서 async 코루틴 빌더는 "Dummy Response" 라는 String 타입의 결과값을 반환하기 때문에 명시적으로 Deferred<String>을 설정했다.
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() // networkDeferred로부터 결과값이 반환돨 때까지 runBlocking 일시 중단
}
위 코드에서는 await 함수의 대상인 networkDeferred 코루틴의 결과값을 반환(실행 완료)할 때까지 await 함수를 호출한 코루틴(여기서는 runBlocking)을 일시 중단한다.
networkDeferred 코루틴의 결과값을 수신한 경우 result 변수에 저장되고 runBlocking 코루틴이 재개한다.
Deferred 객체는 Job 객체의 특수한 형태로 Deferred 인터페이스는 Job 인터페이스의 서브타입으로 선언된 인터페이스이다. Deferred 객체는 코루틴으로부터 결과값 수신을 위해 Job 객체에서 몇 가지 기능이 추가됐을 뿐 여전히 Job 객체의 일종이다.
public interface Deferred<out T>: Job {
public suspend fun await(): T
...
}
따라서 Deferred 객체는 Job 객체와 동일하게 코루틴을 취소하는 cancel 함수, 코루티의 상태를 나타내는 isActive, isCancelled, isCompleted 프로퍼티를 제공한다.
정리하면 Deferred 객체는 결과값을 반환받는 기능이 추가된 Job 객체이며, Job 객체의 모든 함수와 변수를 사용할 수 있다.
10개의 콘서트 관람객을 등록받는 사이트에서 등록된 모든 사용자의 정보를 받아와야하는 경우 await를 10번 계속 사용하는 것은 가독성이 좋지 않다.
이 문제를 해결하기 위해 코루틴 라이브러리는 복수의 Deferred 객체로부터 결과값을 수신하기 위한 awaitAll 함수를 제공한다.
awaitAll 함수는 가변 인자로 Deferred 타입의 객체를 받아 인자로 받은 모든 Deferred 코루틴으로부터 결과가 수신될 때까지 호출부의 코루틴을 일시 중단한다. 결과가 모두 수신되면 Deferred 코루틴들로부터 수신한 결과값들을 List로 만들어 반환하고 호출부의 코루틴을 재개한다.
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) // 요청이 끝날 때까지 대기
/*
// 컬렉션으로 awaitAll 사용하기
// 가변 인자를 사용한 awaitAll과 내부적으로 동일하게 동작한다.
val results:List<Array<String>> = listOf(participantDeferred1, participantDeferred2).awaitAll()
*/
}
participantDeferred1 코루틴과 participantDeferred2 코루틴이 모두 실행되고 두 코루틴의 결과값을 모두 수신하기 전까지 호출부인 runBlocking 코루틴이 일시 중단된다.
participantDeferred1 코루틴과 participantDeferred2 코루틴이 동시에 수행되기 때문에 약 1초 이후에 결과값을 모두 수신하게 되고 호출부인 runBlocking 코루틴이 재개한다.
코루틴 라이브러리에서 제공되는 withContext 함수를 사용하면 async-await 작업을 대체할 수 있다.
async-await 코드와 withContext 사용한 코드를 비교해 보자.
// async-await 사용
fun main() = runBlocking<Unit> {
val networkDefferd: Deferred<String> = async(Dispatchers.IO) {
delay(1000L)
return@async "Dummy Response" // 문자열 반환
}
val result = networkDefferd.await()
}
// withContext 사용
fun main() = runBlocking<Unit> {
val result: String = withContext(Dispatchers.IO) {
delay(1000L)
return@withContext "Dummy Response" // 문자열 반환
}
}
async-await 쌍이 withContext 함수로 대체되면 중간에 Deferred 객체가 생성되는 부분이 없어지고 "Dummy Response" 가 결과로 바로 반환된다.
이처럼 withContext를 이용해서 async-await 쌍을 깔끔하게 만들 수 있다.
withContext는 겉보기에 async와 await를 연속적으로 호출하는 것과 비슷하게 동작하지만 내부적으로 보면 다르게 동작한다.
async-await는 내부적으로 코루틴을 새로 생성해 작업을 처리하지만 withContext 함수는 실행 중이던 코루틴을 그대로 유지한 채로 코루틴의 실행 환경만 변경해 작업을 처리한다.
withContext의 context인자로 Dispatcher.IO 객체를 넣었다고 가정해보자. withContext에서의 작업은 Dispatcher.IO의 작업 대기열로 이동한 후 Dispatcher.IO가 관리하는 스레드 풀에서 실행 가능한 스레드로 옮겨진 후 실행하게 된다. -> 이것은 컨텍스트 스위칭(Context Switching)이라고 한다.
여기서 주의할 점은 새로운 코루틴을 생성하고 생성된 코루틴을 CoroutineDispatcher가 관리하는 스레드로 옮기는 것이 아니다. 아래의 예시를 살펴보자.
fun main() = runBlocking<Unit> {
println("[${Thread.currentThread().name}] runBlocking 블록 실행")
withContext(Dispatchers.IO) {
println("[${Thread.currentThread().name}] withContext 블록 실행")
}
async(Dispatchers.IO) {
println("[${Thread.currentThread().name}] async 블록 실행")
}.await()
/*
// 결과:
[main @coroutine#1] runBlocking 블록 실행
[DefaultDispatcher-worker-1 @coroutine#1] withContext 블록 실행
[DefaultDispatcher-worker-1 @coroutine#2] async 블록 실행
*/
}
runBlocking 코루틴인 coroutine#1은 기본적으로 main 스레드에서 작업하다가 withContext 블록에서만 작업을 Dispatchers.IO가 관리하는 스레드중 하나인 DefaultDispatcher-worker-1으로 옮겨서 작업하는 것을 볼 수 있다. 즉, 새로운 코루틴을 만드는 것이 아니고 기존 작업을 코루틴의 실행 환경만 변경하는 것을 확인할 수 있다.
❗️ withContext은 기존의 코루틴으로 컨텍스트 스위칭만 하기 때문에 병렬처리가 아닌 순차적으로 처리된다.