Coroutine 에서의 Error handling
코루틴에서의 예외처리를 한번 살펴봅니다.
최근에 회사를 옮기면서 코루틴을 다시 살펴볼 기회가 생겼는데, 에러 처리 부분을 한번 정리해봤습니다.
코루틴은 기본적으로 CoroutineScope
에서 사용한는 launch
, async
등의 코루틴 빌더 안에서 실행되므로, 일반적인 코드를 진행하는 스레드에서의 예외를 처리하는 방식과 다르게 구현해야 하는 경우가 있습니다.
launch 안의 코드에서 에러를 처리하는 경우
다만 코루틴이라고 특별한 예외 처리 문법를 사용하는 것은 아닙니다. launch
를 사용하고 안쪽에 있는 코드에서 에러를 처리하고 싶은 경우 단순하게 try-catch
를 사용하면 됩니다.
launch {
try {
// 예외가 발생한 경우
error("cancels coroutine")
} catch (e: Exception) {
// 코드가 여기로 진행된다.
print("launch failed")
}
}
error() 는 전달한 메시지로 IllegalStateException 을 발생시키는 함수입니다.(stdlib)
코루틴 자체에서 예외가 발생할 때
그런데 만약, 위와 같이 코드 로직 안쪽에서 예외를 처리하는것이 아니라, 코루틴 자체를 예외처리 할 수는 없을까요? 만약 아래처럼 launch
를 예외처리 블록으로 감싼다면 어떨까요?
// 코루틴 종료까지 프로세스를 유지해야 해서 runblocking을 사용합니다.
fun main(args: Array<String>): Unit = runBlocking {
try { // launch를 try-catch로 감싼다.
launch {
error("cancels coroutine")
}
} catch (e: Exception) {
println("catched $e")
}
print("main done")
}
예외를 잡을거 같지만, 그렇지 않습니다. 위 프로그램은 “main done” 을 출력하지 못하고 비정상 종료됩니다. 간단하게 이야기하면 코루틴은 기본적으로 예외가 나면 그 코루틴을 실행시킨 CoroutineScope에도 예외를 전파하기 때문입니다. 위 코드에서는 error()
을 실행하는 안쪽 launch
에서 예외가 발생하였고 이 예외가 runBlocking
으로 전달되어 프로세스 자체가 종료됩니다.
위 코드와 같은 예외를 받아서 처리하기 위해서는 다음과 같은 방법들을 사용 가능합니다.
- 새로운
CoroutineScope
를 만들어서 거기서 코루틴 로직을 실행 - 에러를 한 방향으로 전파하는
SupervisorScope
사용하기 CoroutineScope
자체에 처리되지 않은 예외를 받는 기능을 추가 (CoroutineExceptionHandler)
새로운 CoroutineScope를 실행하기
간단합니다. 위 코드에서 안쪽에서 실행하는 launch
는 사실 runBlocking
의 this
를 사용하는 this.launch
와 같습니다.
fun main(args: Array<String>): Unit = runBlocking {
try {
this.launch { // 해당 함수 블록의 this(coroutineScope 에서 호출)
error("cancels coroutine")
}
} catch (e: Exception) {
println("catched $e")
}
print("main done")
}
앞서 말했듯이 코루틴의 기본 동작은 해당 코루틴을 실행한 coroutineScope
로도 예외를 전달하기 때문에, this.launch
를 사용하지 않고 새로운 ScopeBuilder
를 사용하면 됩니다. builer
로는 coroutineScope
, GlobalScope
등이 있습니다.
fun main(args: Array<String>): Unit = runBlocking {
try {
coroutineScope { // 여기부터는 다른 scope
launch {
error("cancels coroutine")
}
}
} catch (e: Exception) {
println("catched $e")
}
delay(1000)
print("main done")
}
SupervisorScope
사용하기
이야기 했듯이 코루틴에서의 예외는 해당 코루틴을 실행한 코루틴, 해당 코루틴이 실행하는 코루틴 모두로 퍼집니다. 아마 기본 동작이 이렇게 된것은 코루틴에서 코루틴을 시행하는 경우 그 전체를 하나의 트랜젝션으로 보기 때문이 아닐까 싶습니다.
하지만 어떤 시나리오에서는 하위에서 진행되는 코루틴이 실패했다고 해서 그 코루틴 전체를 실패시킬 필요는 없습니다. 여러군데의 api를 호출하고 성공한 결과만 화면에 뿌려도 된다면, 해당 작업 하나만 실패처리 하고 다른 작업을 계속 실행시키는 것이 정상동작인 시나리오도 존재합니다.
이를 위해 예외를 child 로만 퍼트리는 특별한 Scope가 있는데, 그것이 바로 SupervisorScope
입니다.
fun main(args: Array<String>): Unit = runBlocking {
supervisorScope {
launch {
delay(500)
println("first scope done")
}
// 예외가 발생해도, 해당 launch만 실패하게 됩니다. (예외를 위로 전달하지 않음)
launch {
error("cancels coroutine")
println("second scope done")
}
delay(1000)
println("main scope done")
}
print("main function completed")
}
위 코드에서 두번째 있는 launch
에서 예외를 발생시키지만, 이를 실행한 supervisorScope
로 예외가 퍼지지 않고 해당 코루틴만 실패합니다.
참고: android 의 ViewModel 에서 사용하는
viewModelScope
는SuperVisorScope
로 설정되어있습니다.
CoroutineExceptionHandler 사용하기
코루틴이 실행 될 때 예외 처리가 되지 않은 경우, 이를 기본으로 처리하는 ` CoroutineExceptionHandler
를 추가 할 수 있습니다. 자바에서 사용되는 Thread.UncaughtExceptionHandler 와 같다고 생각하면 됩니다. 안드로이드의 경우 ViewModel에서 사용하는 viewModelScope
등에 coroutineExceptionHandler
를 추가해서 사용하면 해당 Scope
에서 발생하는 예외는 모두 처리 가능합니다.(물론 예외 발생 코드에서 예외처리를 해야하는 경우는 코드가 다를 수 있습니다.)
// SomeViewModel val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
// handle error
}
val scope = viewModelScoe + coroutineExceptionHandlerscope.launch {
// scope를 사용한 launch 에서 발생한 예외는 coroutineExceptionHandler로
}
PS. GlobalScope의 동작
이전에 적은 내용에서, CoroutineScope
내에서 예외가 발생한다면 해당 Scope
도 예외가 발생한다고 했습니다. 그렇다면, GlobalScope
는 앱 전체에서 하나의 인스턴스를 사용하는데 만약 아래와 같이 코드를 작성한다면 secondJob
이 예외를 발생시켰을 때 firstJob
도 실패할까요?
fun main(args: Array<String>) = runBlocking {
val firstJob = GlobalScope.launch {
delay(3000)
println("first job done.")
}
val secondJob = GlobalScope.launch {
delay(1000)
println("second job done")
error("throws error")
}
secondJob.join()
firstJob.join()
println("ended")
}
firstJob
은 실패하지 않습니다. GlobalScope
라는 object
를 공유하는 것은 맞으나 launch
메소드는 새로운 context
를 만들어서 실행하기때문에 GlobalScope
자체에 영향을 주지는 않습니다. 게다가 새로운 runblocking
과 다른 Scope
를 사용하므로 코루틴의 에러를 위쪽으로 전파시키지 않고, 따라서 firstJob
도 종료되지 않습니다.
위 내용 외에도 여러개의 예외가 발생시, 임의로 cancel()
사용시 어떻게 진행되는지 살펴보면 재미있습니다.
더 자세하게 알고싶다면 코루틴 공식 문서에서 예외처리 부분을 참고 바랍니다.