이 섹션에서는 예외 처리와 예외 발생 시 취소에 대해 다룬다. 우리는 취소된 Coroutine이 일시중단 지점에서 CancellationException을 발생시키고 이것이 Coroutine의 동작원리에 의해서 무시되는 것을 알고 있다. 이 장에서는 취소 도중 예외가 발생되거나 같은 Coroutine에서 복수의 자식 Coroutine이 예외를 발생시킬 경우 어떤 일이 일어나는지 살펴볼 것이다.
Exception 전파
Coroutine 빌더는 자동으로 예외를 전파(launch와 actor)하거나 사용자에게 예외를 노출(async와 produce)한다. 이 빌더들이 다른 Coroutine의 자식이 아닌 root Coroutine을 만드는데 사용될 때, 전자(launch와 actor)는 Java의 Thread.uncaughtExceptionHandler와 비슷하게 앞의 빌더들은 예외를 잡히지 않은 예외*1로 다룬다. 반면 후자(async와 produce)는 await이나 recieve를 통해 사용자가 마지막 예외를 소비하는지에 의존한다.
*produce와 receive는 Channel 섹션에서 다룬다
이는 GlobalScope를 사용해 root Coroutine을 만드는 간단한 예제로 설명될 수 있다.
📖 GlobalScope는 사소하지 않은 역효과를 만들 수 있는 섬세하게 다뤄져야 하는 API이다. 모든 어플리케이션에 대해 root Coroutine을 만드는 것은 GlobalScope의 드문 적합한 사용 방법 중 하나이다. 따라서 @OptIn(DelicateCoroutinesApi::class)을 사용해 GlobalScope를 명시적으로 opt-in*2 시켜야 한다.
@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
val job = GlobalScope.launch { // root coroutine with launch
println("Throwing exception from launch")
throw IndexOutOfBoundsException() // Will be printed to the console by Thread.defaultUncaughtExceptionHandler
}
job.join()
println("Joined failed job")
val deferred = GlobalScope.async { // root coroutine with async
println("Throwing exception from async")
throw ArithmeticException() // Nothing is printed, relying on user to call await
}
try {
deferred.await()
println("Unreached")
} catch (e: ArithmeticException) {
println("Caught ArithmeticException")
}
}
📌 전체 코드는 이곳에서 확인할 수 있습니다.
이 코드의 출력은 다음과 같다(디버그 옵션을 켜놓았음*3) :
Throwing exception from launch
Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.IndexOutOfBoundsException
Joined failed job
Throwing exception from async
Caught ArithmeticException
📖 아래 내용은 독자의 이해를 위해 번역자가 추가한 글입니다.
*1. uncaught exceptions를 잡히지 않은 예외로 번역했다.
*2. Opt-in 이란 명시적으로 사용을 동의하는 것을 뜻한다. Kotlin 안전하게 사용하기 위해 Opt-in을 요구하는 경우가 많다.
*3. JVM option에 -Dkotlinx.coroutines.debug을 추가하면 Coroutine 디버깅이 가능하다.
이 글은 Coroutines 공식 문서를 번역한 글입니다.
원문 : Coroutine exceptions handling - Exception propagation
원문 최종 수정 : 2022년 6월 27일
CoroutineExceptionHandler 사용해 전파된 예외 처리하기
잡히지 않은 예외를 콘솔에 출력하도록 기본 동작을 커스터마이징 할 수 있다. root Coroutine 상의 Context의 요소인 CoroutineExceptionHandler는, root Coroutine과 모든 자식 Coroutine들에 대해 커스텀한 예외 처리가 필요한 경우, 일반 catch 블록으로 사용될 수 있다. 이는 Thread.uncaughtExceptionHandler와 비슷하다. CoroutineExceptionHandler 을 사용해서 예외를 복구 하지는 못한다*1. Coroutine은 Handler가 호출되었을 때 이미 해당 Exception에 대한 처리를 완료했기 때문이다. 일반적으로 CoroutineExceptionHandler는 오류를 로깅하거나, 애러 메세지를 보여주거나, 어플리케이션을 종료하거나 다시 시작하기 위해 사용된다.
CoroutineExceptionHandler는 잡히지 않은 예외에 대해서만 실행된다 - 다른 어떠한 방식으로도 처리되지 않은 예외. 특히, 모든 자식 Coroutine들(다른 Job의 Context로 만들어진 Coroutines)은 그들의 예외를 부모 Coroutine에서 처리하도록 위임하는데, 그 부모 또한 부모에게 위임해서 root Coroutine까지 올라간다. 따라서 그들의 Context에 추가된 CoroutineExceptionHandler는 절대 사용되지 않는다. 추가적으로 async 빌더는 모든 예외를 잡아 Deferred 객체에 나타내므로, CoroutineExceptionHandler가 아무런 효과가 없음은 마찬가지이다.
📖 Supervision Scope 상에서 실행되는 Coroutine은 예외를 그들의 부모로 전파하지 않으며, 이 규칙으로부터 제외된다. 이 문서에서 이후 다룰 Supervision 섹션에서 더 자세히 알려줄 것이다.
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
val job = GlobalScope.launch(handler) { // root coroutine, running in GlobalScope
throw AssertionError()
}
val deferred = GlobalScope.async(handler) { // also root, but async instead of launch
throw ArithmeticException() // Nothing will be printed, relying on user to call deferred.await()
}
joinAll(job, deferred)
📌 전체 코드는 이곳에서 확인할 수 있습니다.
이 코드의 출력은 다음과 같다 :
CoroutineExceptionHandler got java.lang.AssertionError
📖 아래 내용은 독자의 이해를 위해 번역자가 추가한 글입니다.
*1. try { ... } catch { ... } 를 쓰면 예외가 발생해도 다음 블록이 실행되는데, CoroutineExceptionHandler를 사용한다고 다음 블록이 실행되는 것은 아니라는 뜻이다.
이 글은 Coroutines 공식 문서를 번역한 글입니다.
원문 : Coroutine exceptions handling - CoroutineExceptionHandler
원문 최종 수정 : 2022년 6월 27일
목차로 돌아가기