💡Coroutine 사용 중에 발생한 에러를 CoroutineExceptionHandler를 활용하여 핸들링 하는 방법에 대하여 기록하였습니다.
개요
코루틴 내부에서 예외가 발생했을 때 try - catch 블럭이나 runcatching을 활용하여 예외를 감싸 처리하는 방법이 있습니다. 다른 방법으로는 CoroutineExceptionHandler를 활용하는 방안이 있는데, 코루틴에서 발생한 예외를 처리하고 앱의 비정상 종료를 방지할 수 있습니다.
CoroutineExceptionHandler
- CoroutineExceptionHandler는 Kotlin의 코루틴에서 발생한 예외를 처리하기 위한 방법으로 주로 비동기 작업에 활용됩니다.
- 전역적으로 예외 처리를 할 수 있는 방법을 제공하며, 취소되지 않은 코루틴에서 발생한 예외만을 처리합니다.
- 취소 예외(CancellationException)는 CoroutineExceptionHandler로 처리되지 않으며, 이는 코루틴의 취소 흐름에서 따로 처리됩니다.
private val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
// 예외가 발생했을 때 즉각적으로 출력 됩니다.
Log.e("CartViewModel", "An error occurred: $throwable")
}
fun getAllCartProducts() {
// 예외가 발생하면 코루틴 자체는 종료되지만, 애플리케이션은 중단되지 않습니다.
viewModelScope.launch(coroutineExceptionHandler) {
val cartProducts = cartRepository
.getAllCartProducts()
.map { it.toModel() }
_cartProducts.value = CartUiState.Success(cartProducts)
}
}
async에서의 예외처리
- launch는 예외 발생 즉시 연결 된 coroutineExceptionHandler로 예외를 던지지만, async는 Deferred 객체를 반환하여 결과를 사용할 때 예외가 발생합니다.
- 이러한 특성 때문에 async에서는 예외 처리를 별도로 진행해야 합니다.
import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught exception: ${exception.message}")
}
// async는 직접 try-catch로 처리
val deferred = GlobalScope.async(handler) {
throw Exception("Error in async coroutine")
}
try {
deferred.await() // 결과를 받을 때 예외 처리
} catch (e: Exception) {
println("Caught exception in async: ${e.message}")
}
}
SupervisorScope & SupervisorJob
- 코루틴 스코프 내부에서 수행되는 자식 코루틴에 예외가 발생했을 때, 별도의 CoroutineExceptionHandler를 설정하지 않으면 취소가 양방향으로 전파됩니다.
- 이는 자식 코루틴의 예외로 인한 취소가 부모 코루틴까지 전파될 수 있음을 뜻합니다.
취소는 코루틴의 전체 계층 구조를 통해 전파되는 양방향 관계입니다. UI의 하위 작업 중 하나라도 실패하면 전체 UI 컴포넌트를 취소할 필요가 없지만(이러한 사실상 작업이 종료됩니다), UI 컴포넌트가 파괴되고 해당 작업이 취소되면 더 이상 결과가 필요하지 않으므로 모든 하위 작업을 취소해야 합니다.
- SupervisoreJob이나 SupervisorScope를 활용하여 자식 코루틴의 예외가 부모 코루틴까지 전파되지 않도록 막거나 CoroutineExceptionHandler를 활용하여 이를 해결할 수 있습니다.
Coroutine exceptions handling | Kotlin
SupervisorScope 활용
- RuntimeException이 발생하지만, 해당 예외는 supervisorScope 내에서만 처리되며 다른 코루틴의 실행을 중단시키지 않습니다.
fun getAllCartProducts() {
viewModelScope.launch(coroutineExceptionHandler) {
supervisorScope {
launch {
throw RuntimeException()
}
val cartProducts = cartRepository
.getAllCartProducts()
.map { it.toModel() }
_cartProducts.value = CartUiState.Success(cartProducts)
}
}
}
SupervisorJob과 결합
- 하위 코루틴에서 예외가 발생해도, 다른 하위 코루틴은 계속 실행됩니다.
- 이는 상위 코루틴으로 전파되지 않습니다.
import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught exception: ${exception.message}")
}
// SupervisorJob을 사용하여 상위 코루틴에 영향이 가지 않도록 설정
val supervisor = SupervisorJob()
val scope = CoroutineScope(supervisor + handler)
scope.launch {
throw Exception("Error in child coroutine") // 예외 발생
}
scope.launch {
println("This coroutine is still running")
}
delay(1000L) // 잠시 대기
println("Supervisor scope finished.")
}
Dispatcher 주입과 예외 처리
코루틴은 여러 디스패처(Main, IO, Default..) 중 하나에서 실행될 수 있습니다. 예외 처리 시에 적절한 디스패처를 주입하여 코루틴의 실행 환경을 지정할 수 있으며, 이를 통해 예외 처리도 해당 디스패처에서 수행됩니다.
- Dispatchers.IO를 활용해 I/O 작업에 적합한 백그라운드 스레드 풀에서 코루틴이 실행되도록 설정하였습니다.
- dispatcher와 coroutineExceptionHandler이 함께 주입되며, 코루틴 예외가 발생하면 coroutineExceptionHandler에서 이를 잡아 예외를 처리합니다.
import kotlinx.coroutines.*
fun main() = runBlocking {
// 예외 핸들러 정의
val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception ->
println("Caught exception: ${exception.message}")
}
// Dispatcher를 설정 (IO 스레드에서 코루틴 실행)
val dispatcher = Dispatchers.IO
// 코루틴 실행 시 Dispatcher와 ExceptionHandler를 함께 주입
launch(dispatcher + coroutineExceptionHandler) {
println("Running on ${Thread.currentThread().name}")
// 예외 발생
throw RuntimeException("Something went wrong!")
}
// 잠시 대기해서 예외 처리가 실행될 시간을 확보
delay(1000)
}
SupervisorJob과 결합 처리
- 아래 코드 처럼 SupervisorJob과 결합하여 여러 코루틴에서 하나의 코루틴이 실패하더라도 다른 코루틴에 영향을 주지 않도록 관리할 수 있습니다.
fun getAllCartProducts() {
viewModelScope.launch(coroutineExceptionHandler + Dispatchers.IO) {
supervisorScope {
launch {
throw RuntimeException("Failed in child coroutine")
}
val cartProducts = cartRepository.getAllCartProducts().map { it.toModel() }
_cartProducts.value = CartUiState.Success(cartProducts)
}
}
}
정리
- CoroutineExceptionHandler는 코루틴의 전역적인 예외 처리에 사용할 수 있습니다.
- launch에서는 코루틴 내에서 발생한 예외를 CoroutineExceptionHandler로 간단하게 처리할 수 있지만, async에서는 Deferred 객체의 결과를 사용할 때 별도의 예외처리를 필요로 합니다.
- SupervisorJob or SupervisorScope를 함께 사용하면 하위 코루틴에서 발생한 예외가 상위로 전파되지 않도록 하여 유연하게 예외를 처리할 수 있습니다.
- Coroutine Dispatcher를 사용하는 경우에도 CoroutineExceptionHandler, SupervisorScope와 결합하여 효과적으로 예외 처리가 가능합니다!