Q&A

코틀린 코루틴 예외처리

장동혁

9일 전

4801

이미지

여기서 발생하는 코루틴 예외처리의 문제점은 무엇일까요?

댓글 1

관리자9일 전

다음 이미지 코드는 코루틴 예외처리의 작동 방식과 한계를 잘 보여줍니다. 요약하면, CoroutineExceptionHandler를 특정 코루틴에 걸어두는 방식은 예외 흐름이 어떻게 흘러가는지에 따라 기대한 대로 동작하지 않을 수 있습니다. 특히 중첩된 자식 코루틴이 있을 때 더 그렇습니다.

가장 큰 문제점

  • CoroutineExceptionHandler의 역할 범위가 제한적임
- 이 핸들러는 “해당 코루틴이 예외를 던져서 처리되지 않은 경우”에 호출됩니다.
- 중첩된 자식 코루틴에서 예외가 발생하거나, 예외를 try/catch로 내부에서 처리하면 핸들러가 호출되지 않습니다.
  • 부모-자식 관계에서 예외가 흐르는 방식이 예측하기 어려움
- 예외가 4에서 발생하면 4는 취소되고, 4의 자식인 5 역시 취소될 수 있습니다.
- 이로 인해 5의 실행 경로가 의도대로 끝나지 않거나, 예외가 어디서 어떻게 처리됐는지 추적하기 어렵습니다.
  • 구조적 동시성의 의도와 다르게 동작 가능
- 한 코루틴의 예외가 상위 스코프를 취소시키면, 전체 블록(runBlocking 등)의 흐름이 예상치 않게 끝나거나 중간에 중단될 수 있습니다.
- 결국 “예외를 어디서 어떻게 로그/처리할지”를 명확히 하기보단, 예외 흐름이 흔들리기 쉽습니다.

왜 이런 문제가 발생하나요?

  • 예외가 전파되는 경로가 단순한 상위 코루틴의 예외가 아닐 수 있습니다. 자식 코루틴이 실패해도, 부모가 이미 취소되며 예외가 어디서 핸들링되는지가 불명확해질 수 있습니다.

  • CoroutineExceptionHandler는 최상위 루트에서의 예외를 기록하는 용도로 설계된 경우가 많고, 정상적인 에러 흐름 제어용으로 사용하면 오해의 소지가 있습니다.

  • 중첩된 launch 내부에서 예외를 포획하지 않으면, 핸들러 호출 여부나 취소의 범위가 의도와 다를 수 있습니다.

현실적인 해결책과 권장 패턴
1) 예외를 명확한 try/catch로 관리
  • 대부분의 에러는 각 코루틴 내부에서 try/catch로 포획하고, 필요 시 상위로 propagate 하거나 별도 로그를 남기는 방식이 더 명확합니다.

2) 구조적 동시성으로 예외 흐름 제어
  • 부모-자식 관계에서 한 자식의 예외가 다른 자식에 영향을 미치지 않도록 하려면 supervisorScope를 활용합니다. 다만 supervisorScope도 모든 예외를 동등하게 분리하는 것은 아니니 상황에 맞게 사용해야 합니다.

3) 최상위에서의 핸들링과 로깅 분리
  • 전체 흐름의 로그/알림은 CoroutineExceptionHandler를 전역적으로 한 번만 두고, 실제 비즈니스 로직의 예외는 try/catch로 처리하는 것이 더 예측 가능합니다.

예시 코드
  • 예외를 try/catch로 명확히 처리하는 패턴

@Test
fun coroutineExceptionHandling_withTryCatch() = runBlocking {
    try {
        coroutineScope {
            launch {
                println("Coroutine 4: Started")
                launch {
                    println("Coroutine 5: Started (Nested in Coroutine 4)")
                    delay(500)
                    println("Coroutine 5: Completed (Nested in Coroutine 4)")
                }
                delay(200)
                throw RuntimeException("Coroutine 4: Error occurred")
            }
        }
    } catch (e: Throwable) {
        println("Caught by outer catch: $e")
    }
    println("Coroutine 1: Completed (outer)")
}

  • 예외를 전역 핸들러로 로깅하되, 에러 흐름은 try/catch로 제어하는 패턴
@Test
fun coroutineExceptionHandling_withGlobalHandler() = runBlocking {
    val handler = CoroutineExceptionHandler { _, ex ->
        println("Global handler: $ex")
    }

    try {
        coroutineScope {
            launch(handler) {
                println("Coroutine 4: Started")
                launch {
                    println("Coroutine 5: Started (Nested in Coroutine 4)")
                    delay(500)
                    println("Coroutine 5: Completed (Nested in Coroutine 4)")
                }
                delay(200)
                throw RuntimeException("Coroutine 4: Error occurred")
            }
        }
    } catch (e: Throwable) {
        println("Caught by outer catch after scope: $e")
    }
    println("Main: Completed")
}
  • 예외를 격리시키는 패턴( Supervisor를 활용)
@Test
fun coroutineExceptionHandling_withSupervisor() = runBlocking {
    val handler = CoroutineExceptionHandler { _, ex -> println("Unhandled: $ex") }

    supervisorScope {
        val c4 = launch(handler) {
            println("Coroutine 4: Started")
            launch {
                println("Coroutine 5: Started (Nested in Coroutine 4)")
                delay(600)
                println("Coroutine 5: Completed (Nested in Coroutine 4)")
            }
            delay(200)
            throw RuntimeException("Coroutine 4: Error occurred")
        }

        // 다른 작업이 있다면 기다림
        c4.join()
    }
    println("Top-level: Completed")
}

주요 포인트 요약

  • CoroutineExceptionHandler는 전역적인 예외 로깅용으로는 유용하지만, 모든 예외를 포괄적으로 처리하는 일반적인 에러 핸들러로 사용하면 안 됩니다.

  • 중첩된 자식 코루틴이 있을 때 예외 흐름은 예측하기 어렵습니다. try/catch와 join/await를 활용한 명확한 제어가 필요합니다.

  • 필요 시 SupervisorJob/supervisorScope를 사용해 자식 간 실패를 격리하되, 상위 레벨에서 최종적으로 예외를 어떻게 보고/logging 할지 설계합니다.

이런 패턴들을 적용하면, 이미지의 예제처럼 예외가 발생했을 때의 동작이 더 명확해지고, "Coroutine 1: Completed" 같은 로그의 타이밍도 예측 가능해집니다. 필요하면 실제 코드 구조나 사용하는 라이브러리(Android, Kotlin Multiplatform 등)에 맞춘 예외 핸들링 설계도 함께 구체화해 드리겠습니다.

코틀린 코루틴 예외처리