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 |