디지안의 개발일지
[TIL] Exception handling in Kotlin Coroutines 본문
이 문서는 Kt.Academy를 보고 배운 내용을 작성하였습니다.
코루틴을 다루는데 있어서 예외처리는 매우 중요한 부분이다. 코루틴에서도 예외가 발생하면 코루틴 자체가 중단된다. 코루틴 빌더는 예외에 의해 코루틴이 취소되면 상위 코루틴도 취소하고 각 상위 코루틴의 하위 코루틴을 삭제한다.
다음 예제를 보자. 아래와 같이 에러가 발생하기 전에 실행된 코루틴은 실행되지만 하위 코루틴에서 예외가 발생하면 모든 코루틴이 취소된다.
fun main(): Unit = runBlocking {
launch {
launch {
delay(1000)
throw Error("Some error")
}
launch {
delay(2000)
println("Will not be printed")
}
launch {
delay(500) // faster than the exception
println("Will be printed")
}
}
launch {
delay(2000)
println("Will not be printed")
}
}
// Will be printed
// Exception in thread "main" java.lang.Error: Some error...
결과적으로 이미 실행된 코루틴이 아니라면 계층 구조 내에 있는 모든 코루틴이 취소될 수 있다.
코루틴이 취소 되지 않게 하려면
아래와 같이 코루틴이 실행된 밖에 try-catch
문을 감싼다고 해서 코루틴의 취소되는 것을 방지할 수 없다.
fun main(): Unit = runBlocking {
// Don't wrap in a try-catch here. It will be ignored.
try {
launch {
delay(1000)
throw Error("Some error")
}
} catch (e: Throwable) { // nope, does not help here
println("Will not be printed")
}
launch {
delay(2000)
println("Will not be printed")
}
}
// Exception in thread "main" java.lang.Error: Some error...
SupervisorJob
코루틴의 취소를 막는 가장 중요한 방법은 SupervisorJob
을 활용하는 것이다. SupervisorJob
은 하위 코루틴의 모든 예외를 무시하는 특별한 종류의 작업이다.
일반적으로 여러 개의 코루틴 scope를 사용할 때 주로 사용한다.
fun main(): Unit = runBlocking {
val scope = CoroutineScope(SupervisorJob())
scope.launch {
delay(1000)
throw Error("Some error")
}
scope.launch {
delay(2000)
println("Will be printed")
}
delay(3000)
}
// Exception...
// Will be printed
SupervisorJob
을 사용하는데 일반적인 실수는 SupervisorJob
을 상위 코루틴에 대한 인수로 사용하는 것이다. 이는 예외를 처리하는데 도움이 되지 않는다. 이러한 경우 SupervisorJob
에 1개의 직계 하위 코루틴만 생성된 것으로 보기 때문이다. 예외가 발생하면 전체적으로 멈추게 된다.
fun main(): Unit = runBlocking {
// Don't do that, SupervisorJob with one children
// and no parent works similar to just Job
launch(SupervisorJob()) { // 1
launch {
delay(1000)
throw Error("Some error")
}
launch {
delay(2000)
println("Will not be printed")
}
}
delay(3000)
}
// Exception...
그래서 아래와 같이 동일한 작업을 여러 코루틴 빌더의 컨텍스트로 사용하는 것이 더 합리적이다. 그러면 각각이 취소되지만 서로 취소되지는 않는다.
fun main(): Unit = runBlocking {
val job = SupervisorJob()
launch(job) {
delay(1000)
throw Error("Some error")
}
launch(job) {
delay(2000)
println("Will be printed")
}
job.join()
}
// (1 sec)
// Exception...
// (1 sec)
// Will be printed
supervisorScope
다른 방법은 supervisorScope
로 묶는 것이다. supervisorScope
는 상위 - 하위 관계는 유지하지만 예외에 대해서는 연쇄 취소를 하지 않기 때문에 편리하다.
fun main(): Unit = runBlocking {
supervisorScope {
launch {
delay(1000)
throw Error("Some error")
}
launch {
delay(2000)
println("Will be printed")
}
}
delay(1000)
println("Done")
}
// Exception...
// Will be printed
// (1 sec)
// Done
supervisorScope
는 suspending 함수고 suspending 함수 본문을 래핑하는데 사용할 수 있다. 예를 들어, 아래와 같이 여러 독립작업을 시작하도록 사용할 수 있다.
suspend fun notifyAnalytics(actions: List<UserAction>) =
supervisorScope {
actions.forEach { action ->
launch {
notifyAnalytics(action)
}
}
}
Await
예외 전파를 중지하는 방법을 알고 있지만 때로는 충분하지 않을 수도 있다. 코루틴 빌더 async
는 다른 코루틴 빌더와 마찬가지로 상위 코루틴을 모두 중단시킨다. 만약 아래와 같이 async
로 만든 코루틴을 예외 방지하면 어떻게 될까?
class MyException : Throwable()
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
코루틴이 예외로 끝나서 반환할 값이 없기 때문에 await의 결과는 MyException이 된다. 다른 async는 supervisorScope
를 사용한다는 사실 덕분에 중단 없이 완료된다.
CancellationException은 상위 코루틴에 전파되지 않는다.
CancellationException
의 하위 클래스인 예외는 상위 코루틴으로 전파되지 않는다. 단지 현재 코루틴을 취소할 뿐이다. CancellationException
은 open class이기 때문에 자신의 클래스나 객체로 확장할 수 있다.
import kotlinx.coroutines.*
object MyNonPropagatingException : CancellationException()
suspend fun main(): Unit = coroutineScope {
launch { // 1
launch { // 2
delay(2000)
println("Will not be printed")
}
throw MyNonPropagatingException // 3
}
launch { // 4
delay(2000)
println("Will be printed")
}
}
// (2 sec)
// Will be printed
위의 1코루틴이 취소 됬기 때문에 2는 상위 코루틴이 취소 됬기 때문에 취소가 되고 4는 상관 없이 실행된다.
Coroutine exception handler
코루틴 예외를 다룰 때 모든 예외에 대한 기본 동작을 정의하는 것이 유용하다. 이 때 CoroutineExceptionHandler
를 사용하면 유용하다. 예외 전파를 막지는 않지만 예외의 경웨 발생해야 하는 상황을 설정하는데 사용할 수 있다.
fun main(): Unit = runBlocking {
val handler =
CoroutineExceptionHandler { ctx, exception ->
println("Caught $exception")
}
val scope = CoroutineScope(SupervisorJob() + handler)
scope.launch {
delay(1000)
throw Error("Some error")
}
scope.launch {
delay(2000)
println("Will be printed")
}
delay(3000)
}
// Caught java.lang.Error: Some error
// Will be printed
이 컨텍스트는 많은 플랫폼에서 예외를 처리하는 기본 방법을 추가하는데 유용하다.
'Kotlin' 카테고리의 다른 글
[TIL] Effective Kotlin을 읽어보자 - 2 장 (0) | 2022.03.07 |
---|---|
[TIL] Effective Kotlin을 읽어보자 - 책 소개편 (0) | 2022.03.04 |
[TIL] High performance with idiomatic Kotlin (0) | 2022.02.16 |
[TIL] Kotlin Coroutine Cancellation (0) | 2022.02.15 |
[TIL] 코틀린 코루틴에서 어떻게 지연 - 재개가 이루어지는가? (0) | 2022.02.12 |