본문 바로가기
Kotlin

[Kotlin] 코루틴(coroutine)예외 처리와 취소

by LoseyKim 2024. 12. 2.
반응형

Kotlin의 코루틴을 사용할 때, 예외 처리와 취소는 중요한 주제예요. 코루틴이 어떻게 예외를 전파하고, 취소 시 어떤 동작을 하는지 제대로 이해하면 더 안정적이고 신뢰할 수 있는 프로그램을 작성할 수 있어요. 이번 글에서는 코루틴의 예외 처리와 취소 메커니즘, 그리고 관련된 다양한 상황들을 예제와 함께 살펴볼게요.


1. 코루틴에서의 예외 전파

코루틴 빌더(`launch`, `async`)는 예외를 처리하는 방식이 달라요.

  1. `launch` 빌더:
    • 발생한 예외는 자동으로 전파돼요.
    • uncaught exception처럼 취급되어 기본 예외 처리기`Thread.defaultUncaughtExceptionHandler`에서 처리돼요.
  2. `async` 빌더:
    • 예외가 `Deferred` 객체에 담겨 사용자에게 노출돼요.
    • 사용자가 `await`를 호출하지 않으면 예외는 발생하지 않아요.

 

예제 코드

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
    // launch를 사용해 루트 코루틴 생성
    val job = GlobalScope.launch {
        println("launch에서 예외 발생")
        throw IndexOutOfBoundsException() // 예외가 Thread.defaultUncaughtExceptionHandler에 의해 처리됨
    }
    job.join() // 루트 코루틴 종료 대기
    println("launch 종료")

    // async를 사용해 루트 코루틴 생성
    val deferred = GlobalScope.async {
        println("async에서 예외 발생")
        throw ArithmeticException() // await 호출 시 예외 확인 가능
    }
    try {
        deferred.await() // 예외를 확인하려면 await 호출 필수
    } catch (e: ArithmeticException) {
        println("ArithmeticException 처리 완료")
    }
}

 

출력 결과

launch에서 예외 발생
Exception in thread ...
launch 종료
async에서 예외 발생
ArithmeticException 처리 완료

2. `CoroutineExceptionHandler`로 예외 처리

`CoroutineExceptionHandler`는 루트 코루틴에서 발생한 uncaught exception을 처리할 수 있어요. 이를 통해 예외가 발생했을 때 로그를 남기거나 사용자에게 알림을 보여줄 수 있죠. 하지만 다음과 같은 제한이 있어요.

  • 루트 코루틴에서만 작동: 자식 코루틴에서 발생한 예외는 부모로 전파되므로 이 핸들러는 호출되지 않아요.
  • async 빌더와는 호환되지 않음: `async`는 예외를 `Deferred` 객체에 담아두기 때문에 핸들러가 호출되지 않아요.

 

예제 코드

val handler = CoroutineExceptionHandler { _, exception ->
    // uncaught exception이 발생했을 때 동작
    println("CoroutineExceptionHandler에서 $exception 처리")
}

val job = GlobalScope.launch(handler) { // launch를 사용해 루트 코루틴 생성
    throw AssertionError("launch 실패") // AssertionError 발생
}

val deferred = GlobalScope.async(handler) { // async를 사용해 루트 코루틴 생성
    throw ArithmeticException("async 실패") // 핸들러에 의해 처리되지 않음
}

joinAll(job, deferred) // 모든 코루틴 종료 대기

 

출력 결과

CoroutineExceptionHandler에서 java.lang.AssertionError 처리

3. 취소와 예외

코루틴이 취소되면 내부적으로 `CancellationException`이 발생하며, 이는 예외 처리기에서 무시돼요. 취소된 코루틴에서 다른 예외가 발생하면 부모 코루틴으로 전파돼요.

 

예제 코드

val job = launch {
    val child = launch {
        try {
            delay(Long.MAX_VALUE) // 무한 대기
        } finally {
            println("자식 코루틴이 취소되었습니다.") // 자식 코루틴 취소 시 실행
        }
    }
    yield() // 다른 코루틴이 실행되도록 양보
    println("자식 코루틴을 취소합니다.")
    child.cancel() // 자식 코루틴 취소
    child.join() // 자식 코루틴 종료 대기
    println("부모 코루틴은 취소되지 않았습니다.")
}
job.join() // 부모 코루틴 종료 대기

 

출력 결과

자식 코루틴을 취소합니다.
자식 코루틴이 취소되었습니다.
부모 코루틴은 취소되지 않았습니다.

4. 예외 집계(Aggregation)

여러 자식 코루틴에서 예외가 발생하면 가장 먼저 발생한 예외가 처리되고, 나머지 예외는 `Suppressed Exception`으로 저장돼요.

 

예제 코드

val handler = CoroutineExceptionHandler { _, exception ->
    // 첫 번째 예외와 추가로 발생한 suppressed exception 출력
    println("CoroutineExceptionHandler에서 $exception 처리 (추가 예외: ${exception.suppressed.contentToString()})")
}

val job = GlobalScope.launch(handler) {
    launch {
        try {
            delay(Long.MAX_VALUE) // 대기 상태 유지
        } finally {
            throw ArithmeticException("두 번째 예외 발생") // 두 번째 예외
        }
    }
    launch {
        delay(100) // 약간의 지연 후 예외 발생
        throw IOException("첫 번째 예외 발생") // 첫 번째 예외가 우선 처리됨
    }
    delay(Long.MAX_VALUE) // 루트 코루틴 대기 상태 유지
}
job.join()

 

출력 결과

CoroutineExceptionHandler에서 java.io.IOException 처리 (추가 예외: [java.lang.ArithmeticException])

5. 단방향 취소: Supervision

`SupervisorJob`과 `supervisorScope`는 단방향 취소를 지원해, 자식 코루틴의 실패가 부모 코루틴에 영향을 주지 않도록 도와줘요.

 

5.1 SupervisorJob으로 단방향 취소

`SupervisorJob`을 사용하면 부모-자식 관계에서 자식의 실패가 부모에 영향을 미치지 않아요. 아래는 `SupervisorJob`의 동작 방식에 대한 예제예요.

 

예제 코드

val supervisor = SupervisorJob() // 단방향 취소를 위한 SupervisorJob 생성
with(CoroutineScope(coroutineContext + supervisor)) {
    val firstChild = launch {
        println("첫 번째 자식 코루틴에서 예외 발생")
        throw AssertionError("첫 번째 자식 코루틴 실패")
    }
    val secondChild = launch {
        firstChild.join() // 첫 번째 자식 코루틴 종료 대기
        println("첫 번째 자식 코루틴 취소: ${firstChild.isCancelled}, 두 번째 자식 코루틴은 여전히 실행 중")
        try {
            delay(Long.MAX_VALUE) // 두 번째 자식 코루틴 대기
        } finally {
            println("Supervisor가 취소되어 두 번째 자식 코루틴도 취소됨")
        }
    }
    firstChild.join()
    println("Supervisor를 취소합니다.")
    supervisor.cancel() // Supervisor 취소
    secondChild.join()
}

 

출력 결과

첫 번째 자식 코루틴에서 예외 발생
첫 번째 자식 코루틴 취소: true, 두 번째 자식 코루틴은 여전히 실행 중
Supervisor를 취소합니다.
Supervisor가 취소되어 두 번째 자식 코루틴도 취소됨

 

5.2 supervisorScope로 예외 관리

`supervisorScope`는 `SupervisorJob`과 유사하게 동작하지만, 스코프 내에서 자식 코루틴의 실패가 다른 자식에게 영향을 주지 않도록 설계돼 있어요. 이는 단방향 취소를 지원하면서도, 스코프 내 모든 작업이 완료될 때까지 대기하는 특성을 가집니다.

 

예제 코드

try {
    supervisorScope { // 단방향 취소를 지원하는 스코프
        val child = launch {
            try {
                println("자식 코루틴이 실행 중입니다.") // 자식 코루틴 실행
                delay(Long.MAX_VALUE) // 무한 대기
            } finally {
                println("자식 코루틴이 취소되었습니다.") // 스코프 종료로 인해 취소됨
            }
        }
        yield() // 다른 코루틴이 실행되도록 양보
        println("스코프에서 AssertionError 발생")
        throw AssertionError() // 스코프 내에서 예외 발생
    }
} catch (e: AssertionError) {
    println("AssertionError를 처리했습니다.")
}

 

출력 결과

자식 코루틴이 실행 중입니다.
스코프에서 AssertionError 발생
자식 코루틴이 취소되었습니다.
AssertionError를 처리했습니다.

 

5.3 SupervisorJob과 supervisorScope의 차이점

특징 SupervisorJob supervisorScope
사용 위치 특정 코루틴의 `Job`으로 설정 `coroutineScope`와 유사한 스코프
예외 전파 부모로 전파되지 않음 스코프 내 자식 간 전파되지 않음
스코프 관리 스코프와 무관 스코프가 종료되기 전에 모든 자식 종료 대기

참고자료

 

Coroutine exceptions handling | Kotlin

 

kotlinlang.org

 

 

 

[Android] 발생할 수 있는 Exception종류와 처리 방법

안녕하세요. 개발을 하다 보면 다양한 예외 상황에 직면하게 돼요. 이러한 예외는 프로그램의 흐름을 방해하고 예상치 못한 종료를 초래할 수 있기 때문에, 각 예외의 특성을 이해하고 적절히

losey-kim.tistory.com

반응형