Kotlin의 코루틴을 사용할 때, 예외 처리와 취소는 중요한 주제예요. 코루틴이 어떻게 예외를 전파하고, 취소 시 어떤 동작을 하는지 제대로 이해하면 더 안정적이고 신뢰할 수 있는 프로그램을 작성할 수 있어요. 이번 글에서는 코루틴의 예외 처리와 취소 메커니즘, 그리고 관련된 다양한 상황들을 예제와 함께 살펴볼게요.
1. 코루틴에서의 예외 전파
코루틴 빌더(`launch`, `async`)는 예외를 처리하는 방식이 달라요.
- `launch` 빌더:
- 발생한 예외는 자동으로 전파돼요.
- uncaught exception처럼 취급되어 기본 예외 처리기`Thread.defaultUncaughtExceptionHandler`에서 처리돼요.
- `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
'Kotlin' 카테고리의 다른 글
[Kotlin] Kotlin when 문에서 else를 써야 할 때와 생략할 때 (0) | 2024.12.12 |
---|---|
[Kotlin] StateFlow: Android 상태 관리를 위한 필수 도구 (1) | 2024.11.15 |
[Kotlin] 언더스코어(_)를 변수 이름에 붙이는 이유 (1) | 2024.09.03 |
[Kotlin] Coroutine Suspend function (0) | 2024.03.25 |
[Kotlin] Data class, Sealed class (0) | 2024.03.06 |
[Kotlin] Scope functions(let, run, with, apply, also) (0) | 2024.03.05 |
[Kotlin] object, companion object (0) | 2024.03.05 |