[Coroutine] 코루틴 학습 - 8 (Exception handling)
Exception handling
- 코루틴은 익셉션이 발생하면 해당 코루틴을 cancel 시키고, 해당 익셉션을 부모 코루틴으로 전파한다.
- 익셉션을 전파받은 부모 코루틴은 자신과 자신의 자식 코루틴들을 cancel 시키고, 또 다시 자신의 부모 코루틴으로 익셉션을 전파한다.
- 위 과정을 반복해서 최상위 코루틴까지 예외가 전파된다.
fun main(): Unit = runBlocking {
launch {
launch {
delay(1000)
throw Error("Some error")
}
launch {
delay(2000)
println("출력되지 않는다.")
}
launch {
delay(300)
println("출력된다.")
}
}
launch {
delay(2000)
println("출력되지 않는다.")
}
}
- 위 코드에서 예외가 발생하는 코루틴을 try-catch로 래핑하여 감싸는 방법은 Job을 통해서 서로 커뮤니케이션이 일어나는 코루틴의 특성 때문에 cancel 이 전파되는 것을 막을 수 없다.
fun main(): Unit = runBlocking {
launch {
try {
launch {
delay(1000)
throw Error("Some error")
}
} catch (e: Throwable) {
println("출력되지 않는다.")
}
launch {
delay(2000)
println("출력되지 않는다.")
}
}
}
SupervisorJob
SupervisorJob은 자식 코루틴에게서 발생하는 cancel 을 무시하도록 하는 Job이다.
fun main(): Unit = runBlocking {
launch {
val supervisorJob = SupervisorJob()
launch(supervisorJob) {
delay(1000)
throw Error("Some error")
}
launch(supervisorJob) {
delay(2000)
println("Inner Coroutine")
}
supervisorJob.join()
}
launch {
delay(3000)
println("Outer Coroutine")
}
}
// Exception in thread "main" java.lang.Error: Some error
// Innter Coroutine
// Outer Coroutine
일반적으로 부모 코루틴을 argument로 넘기는 경우에 발생하는 실수는 아래의 코드와 같다.
fun main(): Unit = runBlocking {
// Bad practice
launch(SupervisorJob()) {
launch {
delay(1000)
throw Error("Some error")
}
launch {
delay(2000)
println("Inner Coroutine")
}
}
delay(3000)
}
위 코드는 SupervisorJob이 오직 하나의 자식 코루틴만 있기 때문에 예외 처리에 도움이 되지 않는다. 아래와 같이 여러 coroutine builder의 컨텍스트로 작업을 수행하는 것이 더 좋다. 이는 자식 코루틴들이 각자 취소될 수 있지만, 서로 영향을 주어 취소시키진 않는다.
fun main(): Unit = runBlocking {
val job = SupervisorJob()
launch(job) {
delay(1000)
throw Error("Some error")
}
launch(job) {
delay(2000)
println("출력된다")
}
job.join()
}
// (1 sec)
// Exception..
// (1 sec)
// 출력된다
supervisorScope
예외가 전파되는 것을 멈추는 또다른 방법은 coroutine builder를 supervisorScope로 래핑하는 것이다. 이는 부모 코루틴과 연결을 유지하기 때문에 편리한 방법이다. 그리고 코루틴으로부터의 예외는 묵음처리 된다.
fun main(): Unit = runBlocking {
supervisorScope {
launch {
delay(1000)
throw Error("Some error")
}
launch {
delay(2000)
println("Inner 코루틴")
}
}
delay(1000)
println("Done")
}
// Exception
// Inner 코루틴
// (1 sec)
// Done
supervisorScope는 일시중단 함수이며, 일시중단 함수 본문을 래핑하는데 사용할 수 있다. 이는 일반적으로 서로 독립적인 여러 작업들을 시작할 때 많이 사용한다.
Await
예외가 발생하는 케이스에서 코루틴 빌더 async는 launch와 와 다른 코루틴 빌더들과 마찬가지로 그와 관련된 부모 코루틴을 중단시킨다. 하지만 supervisorScope를 이용하면 마찬가지로 cancel이 전파되는 것을 막을 수 있다.
아래의 코드에서는 supervisorScope 내에서 async 가 바깥으로 예외를 던지는 상황인데, 다른 async는 중단되지 않고 실행이 완료된다.
suspend fun main() = supervisorScope {
val str1 = async<String> {
delay(1000)
throw MyException()
}
val str2 = async {
delay(2000)
"Text2"
}
try {
println(str1.await())
} catch (e: MyException) {
println(e)
}
println(str2.await())
}
// MyException
// Text2
CancellationException
발생한 예외가 CancellationException의 서브클래스인 경우에는 부모 클래스로 예외를 전파하지 않고, 현재 코루틴 내에서만 예외가 발생한다.
suspend fun main(): Unit = coroutineScope {
launch {
launch {
delay(2000)
println("출력되지 않는다.")
}
throw CustomNonPropagatingException
}
launch {
delay(3000)
println("출력된다.")
}
}
Coroutine exception handler
- CoroutineExceptionHandler를 이용하면 익셉션을 핸들링할 때 기본적으로 수행해야 하는 로직들을 처리하기 편리하다.
- 핸들러는 예외가 전파되는 것을 막지는 않지만, 예외가 발생했을 때 수행해야 할 로직들을 설정할 수 있다. (로깅 등..)
- 아래 코드는 CoroutineExceptionHandler와 예외의 전파를 막을 수 있는 SupervisorJob()을 이용한 예시 코드이다.
- SupervisorJob으로 인해 예외가 전파되지 않기 때문에 두 번째 자식 코루틴이 중지되지 않고 계속 실행된다.
fun main(): Unit = runBlocking {
val handler = CoroutineExceptionHandler { ctx, exception ->
println("exception - $exception")
}
val scope = CoroutineScope(SupervisorJob() + handler)
scope.launch {
delay(1000)
throw Error("Some error")
}
scope.launch {
delay(2000)
println("출력된다.")
}
scope.coroutineContext.job.children.forEach { it.join() }
}
참고자료
https://www.amazon.com/Kotlin-Coroutines-Deep-Marcin-Moskala/dp/8396395837