코루틴 관련 포스팅은 아래의 포스팅들을 같이 순차적으로 참고하시면 좋습니다. 클릭하면 해당 포스팅으로 이동합니다.
1. 코루틴 기본 개념
2. 코루틴 빌더
3. Job과 코루틴의 라이프 사이클
4. 코루틴 컨텍스트와 스코프
5. Flow 기본 개념
6. Flow 핵심 API 정복하기
7. Flow 제어하기 (예외, 완료 등)
이전까지 코루틴의 개념에 대해서 살펴보았다. 이제 실제 코루틴을 사용해보기 위해, 새로운 코루틴을 만드는 방법을 알아보자.
새로운 코루틴은 `runBlocking`, `launch`, `async` 총 3가지 주요 코루틴 빌더를 사용해서 만들 수 있다. 이 외에 스코프 빌더(혹은 scope function)을 통해서도 코루틴을 생성할 수 있긴 한데, 코루틴에 대해 더 상세하게 알아본 후 코루틴 스코프와 관련된 내용을 다룰 때 기술할 예정이다.
runBlocking
`runBlocking`은 새로운 코루틴을 실행하는 빌더 중 하나이다. 중요한 특성은 블록 내부에 있는 작업이 완료될 때까지 현재 스레드를 블로킹한다는 점이다. 이러한 특성 때문에 `runBlocking`은 코루틴, Reactive, Future 등의 비동기 라이브러리와 일반적인 블로킹 스타일의 코드를 연결하기 위해 설계했다고 설명하고 있으며, 주로 main함수 혹은 테스트 함수에 한번만 사용하는 것을 권장한다. 좀 더 풀어서 설명하면, 일반적인 동기식 코드와 코루틴을 연결하는 브릿지 용도로 사용하는 것이 적합하다.
위에서 설명한 `runBlocking`의 특성 때문에, 이를 용도에 맞게 사용하는 것이 중요하다. 만약 잘못 사용할 경우, 스레드 고갈 문제로 이어질 수도 있다. 말로 설명하기엔 어려우니, 소요 시간이 1초가 걸리는 API를 2번 호출해야하는데 시간을 단축시키고 싶어서 코루틴을 사용해서 구현하기로 했다고 예시를 들어보자.
suspend fun doSomethingWithRunBlocking() = coroutineScope {
runBlocking { callApi() }
callApi()
}
suspend fun doSomethingWithLaunch() = coroutineScope {
launch { callApi() }
callApi()
}
suspend fun callApi() {
delay(1000)
log("api call success")
}
두 함수 모두 코루틴을 생성한 후 내부에 코루틴을 하나 더 생성해서 총 2개의 코루틴으로 API 요청을 한번씩 보내는 코드이다. (`corountineScope`, `launch`는 단순하게 코루틴을 생성해준다고 생각하면 된다)
fun main() {
println(
measureTimeMillis {
runBlocking { doSomethingWithRunBlocking() }
}
)
println(
measureTimeMillis {
runBlocking { doSomethingWithLaunch() }
}
)
}
[main @coroutine#2] api call success
[main @coroutine#1] api call success
2053
[main @coroutine#3] api call success
[main @coroutine#4] api call success
1013
두 함수를 모두 실행시켜 결과를 비교해보았다. 동기식 코드라면 1초가 걸리는 API를 2개 호출해서 총 2초가 걸리는 것이 맞지만, 우리는 코루틴을 사용해서 이를 1초로 만들려고 했다. 하지만 `doSomethingWithRunBlocking()`는 기대한대로 결과가 나오지 않았다.
그 이유는 맨 처음에 설명했던 블록 내부에 있는 작업이 완료될 때까지 현재 스레드를 블로킹한다는 `runBlocking`의 특징 때문이다.
그림으로 보면 이해가 더 쉬울 것이다. 이 상황에서, 만약 스레드를 여러 개 사용하는 상황이라고 가정한다면 blocked된 시간만큼 해당 스레드는 못 쓰는 상태가 되기 때문에 스레드 고갈의 위험이 있다고 말하는 것이다.
launch
launch는 현재 쓰레드를 블로킹하지 않고 새로운 코루틴을 만든다. `Job`이라는 객체를 반환하는데, `Job`을 통해 코루틴을 제어할 수 있게 된다.
fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
함수 선언은 다음과 같다. `CoroutineScope`의 확장 함수이기 때문에 `CoroutineScope`를 수신객체로 가지는 람다 내부 혹은 `CoroutineScope` 객체가 있어야만 호출할 수 있다.
아직 자세히 다루지는 않았지만, 코루틴은 코루틴마다 실행되는 컨텍스트라는 것이 있고, 이 컨텍스트는 `CoroutineContext`를 통해 표현한다. 쉽게 그냥 해당 코루틴에 대한 정보라고 생각하면 된다. `launch` 함수를 통해 새로운 코루틴을 만들게 되면, 이 코루틴은 `launch`함수를 호출한 코루틴의 컨텍스트를 상속받게 된다. 단, 새롭게 추가해야 할 컨텍스트 요소들이 있다면 `launch` 함수의 파라미터로 선언된 `context`에 인자로 넣어주면 된다. 보통 이 파라미터로 추후에 기술할 `Dispatchers`라는 객체를 넣어주는데, 만약 인자로 넣어주지 않는다면 `Despatchers.Default`가 사용된다.
두번째 파라미터로는 `CoroutineStart` 타입을 받는데, 이는 코루틴의 시작을 어떻게 할 것인지 지정하게 된다. 생략하는 경우 즉시 시작하는 코루틴이 만들어지게 된다. `CoroutineStart`에 따라 어떻게 코루틴이 다뤄지는지는 이 문서를 참조하도록 하자.
`launch`함수는 `Job`을 반환한다. 이는 코루틴을 제어하는 객체인데 추후에 코루틴의 라이프사이클과 함께 자세히 기술할 예정이다. 지금은 코루틴을 제어한다고만 알고 있자.
async
`async`는 `launch` 처럼 쓰레드를 블로킹하지 않고 새로운 코루틴을 생성하되, 블록 내부의 결과를 `Deferred` 구현체를 통해서 반환하는 함수이다.
fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>
`async` 함수의 선언이다. 파라미터에 대한 설명은 `launch`에서 설명한 것과 동일하다.
다른 점은 반환 값인데, `Deferred`는 `Job`을 상속한 인터페이스이며 `Job`의 기능에 `await`이라는 실행 결과를 가져오는 함수가 추가된 인터페이스이다. 결론적으로 `launch`와 `async`의 큰 차이점은 블록 내부의 반환값을 블록 외부에서 사용할 수 있는지 없는지라고 생각하면 쉽다.
`launch` 함수는 `Job`을 반환함으로, 블록 내부에서 값을 반환할 수 없다. 반면 `async`는 `Deffered`를 반환하기 때문에 `await`을 통해서 블록 내부에서 반환한 값에 접근할 수 있게 된다.
예외와 관련해서도 차이점이 있는데, 만약 `async`를 루트 코루틴으로 사용한다면 `await`을 통해 결과를 받지 않는 이상 예외가 발생하지 않는다.
fun main(): Unit = runBlocking {
val deferred = CoroutineScope(Dispatchers.Default).async {
throw IllegalStateException("async exception")
}
val job = CoroutineScope(Dispatchers.Default).launch {
throw IllegalStateException("launch exception")
}
delay(1_000L)
log("end")
}
루트 코루틴이란 코루틴 스코프 내의 가장 최상위 코루틴을 의미한다. 아직 설명하지 않았지만 팩토리 함수를 통해 새로운 `CoroutineScope` 객체를 만들어 새로운 코루틴 영역을 만들 수 있고, 해당 영역에서 각각 `async`, `launch`를 통해 코루틴을 생성했다. 따라서 위 코드에서는 총 `runBlocking` 으로 생성된 코루틴을 포함하여 3개의 루트 코루틴이 존재한다.
보통 `runBlocking` 내에서 `launch`, `async`을 통해 새로운 코루틴을 생성하면 `runBlocking`의 코루틴이 루트, 하위의 `launch`, `async` 코루틴이 자식 코루틴이 된다. (아래에 코루틴의 라이프 사이클과 관련해서 자세하게 기술될 예정이다) 하지만 위 코드는 코루틴 영역을 생성한 후 `launch`, `async` 을 통해 코루틴을 생성했기 때문에 3개의 코루틴간에는 부모 자식 관계가 없다.
Exception in thread "DefaultDispatcher-worker-2 @coroutine#3" java.lang.IllegalStateException: launch exception
at coroutine.CoroutineExamKt$main$1$job$1.invokeSuspend(CoroutineExam.kt:41)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [CoroutineId(3), "coroutine#3":StandaloneCoroutine{Cancelling}@75eaeb90, Dispatchers.Default]
[main @coroutine#1] end
메인 함수의 실행 결과이다. launch exception만 스택트레이스로 남는 걸 확인할 수 있다. 그렇다면 `await`을 통해 결과를 얻어보자
fun main(): Unit = runBlocking {
val deferred = CoroutineScope(Dispatchers.Default).async {
throw IllegalStateException("async exception")
}
deferred.await()
delay(1_000L)
log("end")
}
바로 `async` 블록 내부의 예외가 발생하는 것을 볼 수 있다. (위 코드에서 end 로그는 출력되지 않는 것이 맞다. `runBlocking` 내부에서 `await`을 호출했기 때문에, `runBlocking` 코루틴에서 예외가 발생해버린 것이 때문에 `delay`, `log` 함수는 실행되지 못하고 코루틴이 종료된다)
다만, 루트 코루틴이 아닌 경우 `async`은 `launch`와 동일하게 예외를 즉시 발생시킨다.
fun main(): Unit = runBlocking {
async { throw IllegalStateException() }
delay(1000)
log("end")
}
이 코드로 확인해보면 예외가 발생해서 `runBlocking`으로 생성된 루트 코루틴 역시 취소되기 때문에 end 로그는 출력되지 않는다.
만약 자식 코루틴일 때도 `async`이 예외를 전파하지 않도록 할려면, `SupervisorJob`을 사용하면 된다.
fun main(): Unit = runBlocking {
async(SupervisorJob()) { throw IllegalStateException() }
delay(1000)
log("end")
}
위 코드는 `await`을 실행시키지 않았기 때문에 예외가 전파되지 않는다. 따라서 end 로그가 출력되는 것을 확인할 수 있다.
지금까지 `async`과 예외에 대한 내용은 사실 코루틴의 예외와 관련한 내용을 먼저 알고난 이후에 이해가 쉬울 것이다. 코루틴의 예외와 관련해서는 코루틴의 라이프 사이클과 관련하여 추가로 포스팅할 예정이니 이 부분이 이해가 안된다면 이후 작성할 포스팅을 한번 읽고 이 포스팅을 다시 읽어보는 것을 권장한다.