Coroutine 에러 핸들링 with CoroutineExceptionHandler

💡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

 

Coroutine exceptions handling | Kotlin

 

kotlinlang.org

 

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와 결합하여 효과적으로 예외 처리가 가능합니다!