목적
코루틴을 이용하여 비동기 작업을 처리하던 중 'async'와 'launch' 내부에서 예외가 발생하면 어떻게 처리될지 궁금해졌다
그래서 여러 글을 찾아보던 중 async 내부에서 예외가 발생하면, await을 호출하기 전까지는 예외가 발생하지 않는다는 글을 봤다
async 내부에서 예외가 발생하면 그 즉시 자식, 부모 코루틴에 예외 및 취소 신호를 보내야 하지 않나?라는 의문점이 들었고 이 의문점을 해결하고자 글을 작성하게 됐다
문제상황 재현
먼저 async 내부에서 예외를 발생시키고 await 은 호출하지 않는 코드를 실행시켜 봤다. 아래 코드가 그 예이다
fun withoutAwait() {
val deferred = CoroutineScope(Dispatchers.IO).async{
delay(100)
throw RuntimeException("exception1")
}
Thread.sleep(1000)
println("finished")
}
/*
결과
finished
*/
위 코드를 실행시키면 결과로 'finished' 를 출력할 뿐 어떤 예외도 발생시키지 않는다. 위의 코드에서 async를 launch로 바꾸면 어떤 일이 발생할까?
fun launchException() {
CoroutineScope(Dispatchers.IO).launch{
delay(100)
throw RuntimeException("exception1")
}
Thread.sleep(1000)
println("finished")
}
/*
Exception in thread "DefaultDispatcher-worker-1 @coroutine#1" java.lang.RuntimeException: exception1
at com.pjh.application.AsyncService$launchException$1.invokeSuspend(AsyncService.kt:37)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104)
at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:111)
at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:99)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:811)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:715)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:702)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [CoroutineId(1), "coroutine#1":StandaloneCoroutine{Cancelling}@ef78271, Dispatchers.IO]
finished
*/
예외와 'finished' 가 모두 출력되는 걸 확인할 수 있다
async를 사용해서 비동기 처리할 때 await을 호출해주지 않으면, async 내부에서 예외가 발생해도 예외를 감지할 수 없는 문제가 발생할 수 있는 상황이다. 그렇다면 async내부의 예외는 어디로 간 것일까?
async와 launch 의 차이
async와 launch의 어떤 차이 때문에 내부에서 예외가 발생했을 때 차이가 발생하는 것일까? 그것은 바로 'handleJobException' 구현의 차이이다. 'handleJobException'에 적힌 주석을 정리하면 아래와 같다
- 코루틴이 실행되는 동안 발생한 예외를 핸들링하는 함수이다
- 예외가 정상적으로 처리되거나, 더 이상 처리할 필요가 없으면 true를 반환한다
- 주로 launch 계열의 코루틴이 오버라이딩한다. launch 계열이란 결과를 반환하지 않는 코루틴을 의미한다. async와 같이 결과를 반환하는 코루틴은 await 할 때 자체적으로 예외를 처리해 줄 수 있기 때문이다
- 자신의 코루틴과 모든 자식 코루틴이 종료된 상태에서 단 한 번 호출된다
async를 통해 만들어진 'DeferredCoroutine'을 보면 'handleJobException' 따로 오버라이딩하지 않고 있으며, 때문에 기본 구현체를 사용할 것이다. 기본 구현체는 아래 코드에서 확인할 수 있듯이 어떤 동작도 하지 않고 false만 반환하기 때문에 어떤 에러 로깅도 되지 않을 것이다
async를 통해 만들어진 코루틴은 내부에서 에러가 발생할 경우 에러를 잠시 저장해 뒀다가, await 이 호출되는 시점에 예외를 발생시킨다
protected open fun handleJobException(exception: Throwable): Boolean = false
launch 를 통해 만들어진 'StandaloneCoroutine'을 보면 'handleJobException' 을 오버라이딩한 걸 확인할 수 있다. 아래 구현을 간단히 설명하면 현재 코루틴 스코프에 등록된 exception handler 가 있는지 확인하고 있다면 그 핸들러로 핸들링하고, 없다면 기본 핸들러를 사용한다. 이 글에서는 따로 exception handler를 등록하지 않았기 때문에 기본 핸들러에 의해 에러 로깅이 남았을 걸로 추측된다
override fun handleJobException(exception: Throwable): Boolean {
handleCoroutineException(context, exception)
return true
}
async와 exception handler
async에 의해 만들어진 코루틴 로직을 살펴보면 코루틴 스코프에 exception handler를 등록해도, 핸들러가 동작하지 않을 것 같다. 정말 그럴지 아래 코드를 통해 확인해 보겠다
fun withoutAwait() {
val handler = CoroutineExceptionHandler {_, e ->
println(e.message)
}
val deferred = CoroutineScope(Dispatchers.IO + handler).async{
delay(100)
throw RuntimeException("exception1")
}
Thread.sleep(1000)
println("finished")
}
/*
finished
*/
결과를 보면 핸들러의 'println' 로직이 동작하지 않을 걸 확인할 수 있다(에러 메시지가 출력되지 않았다)
정리
- 코루틴의 근본적인 예외처리에 대한 글
- async와 launch를 사용해서 코루틴을 만들 때 내부에서 예외가 발생했을 때 어떻게 처리할지 미리 고민해야 한다
- launch는 코루틴 스코프에 exception handler를 등록해 준다
- async는 반드시 try-catch로 감싸진 구문에서 await를 호출하고, 예외를 핸들링해야 한다
- 코루틴을 사용할 때 예외가 발생할 경우 취소가 부모 및 자식에 전파될 수 있음을 고려해야 한다
- runBlocking 내부에서 사용할 때 동작이 달리질 수 있으므로 주의해야 한다
'WEB개발 > 백엔드' 카테고리의 다른 글
Spring MVC & WebClient 에서 MDC 로깅하기 2 (0) | 2025.03.22 |
---|---|
코루틴 내부에서 예외가 발생했을 때 (0) | 2025.03.08 |
TransactionalEventListener 정리 (0) | 2025.02.23 |
플링크 공식문서 정리-Stateful Stream Processing (0) | 2025.02.16 |
Spring MVC & WebClient 에서 MDC 로깅하기 1 (1) | 2025.02.09 |