훌륭한 개발자가 되기 위하여
코루틴 예외 처리 본문
예외 처리
예외 전파
- 코루틴 실행 도중 예외가 발생하면, 예외가 발생한 코루틴이 취소되고 예외가 부모 코루틴으로 전파
- 예외를 전파 받은 코루틴도 예외를 적절히 처리하지 않는다면 취소되고, 그 상위의 코루틴으로 예외가 전파된다.
- 코루틴이 예외를 전파 받아 취소되면, 취소가 해당 코루틴의 하위에 있는 자식 코루틴들에 전파된다.
예외 전파 제한하기
코루틴의 구조화를 깨서 예외 전파를 제한하기
- 코루틴의 구조화를 깨면 예외 전파를 제한할 수 있다.
- 단순히 Job 객체를 새로 만들어 구조화를 깨고 싶은 코루틴에 연결하면 구조화가 깨진다.
👉 코루틴의 구조화가 깨지면, 예외 전파 뿐만 아니라 취소 전파도 제한된다.
SupervisorJob을 사용한 예외 전파 제한
- SupervisorJob 객체는 자식 코루틴으로부터 예외를 전파 받지 않느 특수한 Job 객체
👉 예외를 전파 받지 않아 자식 코루틴에서 예외가 발생하더라도 취소되지 않는다.
- SuperVisorJob 객체는 자식 코루틴에서 발생한 예외가 다른 자식 코루틴에게 영향을 미치게 못하게 만드는데 사용된다.
- 생성함수를 통해 생성된 SupervisorJob은 Job 생성함수와 마찬가지로 Complete 함수를 호출해 명시적으로 완료시켜줘야한다.
fun main() = runBlocking<Unit> {
// supervisorJob의 부모로 runBlocking으로 생성된 Job 객체 설정
val supervisorJob = SupervisorJob(parent = this.coroutineContext[Job])
launch(CoroutineName("Coroutine1") + supervisorJob) {
launch(CoroutineName("Coroutine3")) {
throw Exception("예외 발생")
}
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
launch(CoroutineName("Coroutine2") + supervisorJob) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
delay(1000L)
}
👉 실행시 예외가 발생한 상위 객체가 SupervisorJob 이므로 SupervisorJob 하위 객체인 Coroutine2는 정상 실행이 된다.
SupervisorJob 객체는 CoroutineScope 생성함수와 함께 자주 사용된다.
fun main() = runBlocking<Unit> {
val coroutineScope = CoroutineScope(SupervisorJob())
coroutineScope.apply {
launch(CoroutineName("Coroutine1")) {
launch(CoroutineName("Coroutine3")) {
throw Exception("예외 발생")
}
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
launch(CoroutineName("Coroutine2")) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
delay(1000L)
}
SupervisorJob이 잘못 사용되는 경우
- 단일 코루틴 빌더 함수의 context 인자로 SupervisorJob 객체를 넘기고 그 하위에 자식 코루틴들을 생성할 경우 SupervisorJob 객체는 아무런 역할을 하지 못한다.
fun main() = runBlocking<Unit> {
launch(CoroutineName("Parent Coroutine") + SupervisorJob()) {
launch(CoroutineName("Coroutine1")) {
launch(CoroutineName("Coroutine3")) {
throw Exception("예외 발생")
}
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
launch(CoroutineName("Coroutine2")) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
delay(1000L)
}
👉 실행시 Parent Coroutine 에 예외가 전파되어 아무런 Print 함수도 실행되지 않음
SupervisorScope을 사용한 예외 전파 제한
- supervisorScope 함수는 SupervisorJob 객체를 가진 CoroutineScope 객체를 생성한다.
- supervisorScope 함수를 통해 생성된 SupervisorJob 객체는 supervisorScope 함수를 호출한 코루틴을 부모로 가진다.
- supervisorScope 함수를 통해 생성된 SupervisorJob 객체는 코드가 모두 실행되고 자식 코루틴의 실행도 완료되면 자동으로 완료된다. 👉 복잡한 설정 없이 구조화를 깨지 않고 예외 전파를 제한할 수 있다.
fun main() = runBlocking<Unit> {
supervisorScope {
launch(CoroutineName("Parent Coroutine") + SupervisorJob()) {
launch(CoroutineName("Coroutine1"))
throw Exception("예외 발생")
}
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
launch(CoroutineName("Coroutine2")) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
CoroutineExceptionHandler란?
- CoroutineExceptionHandler는 CoroutineContext의 구성 요소 중 하나이다.
- CoroutineExceptionHandler는 처리되지 않은 예외만 처리한다.
- CoroutineExceptionHandler는 launch 코루틴으로 시작되는 코루틴 계층의 공통 예외 처리기로 동작하는 구성요소이다.
- CoroutineExceptionHandler은 CoroutineContext의 구성요소이기 때문에 CoroutineContext 객체에 포함될 수 있다.
fun main() = runBlocking<Unit> {
val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
println("[예외 발생] ${throwable}")
}
CoroutineScope(context = exceptionHandler).launch(CoroutineName("Coroutine1")) {
launch(CoroutineName("Coroutine2")) {
throw Exception("Coroutine2에 예외가 발생했습니다.")
}
}
delay(1000L)
}
- CoroutineExceptionHandler는 처리되지 않은 예외만 처리한다.
- 만약 launch 코루틴이 다른 launch 코루틴으로 예외를 전파하면, 예외가 처리된 것으로 보기 때문에 자식 코루틴에 설정된 CoroutineExceptionHandler는 동작하지 않는다.
CoroutineExceptionHandler가 사용되는 경우
- 예외를 로깅하거나, 오류 메시지를 표시하기 위해 구조화된 코루틴에 공통으로 동작하는 예외 처리기를 설정해야 하는 경우 사용된다.
try catch 문을 사용한 예외 처리
fun main() = runBlocking<Unit> {
launch(CoroutineName("Coroutine1")) {
try {
throw Exception("Coroutine1에 예외가 발생했습니다.")
} catch (e: Exception) {
println(e.message)
}
}
launch(CoroutineName("Coroutine2")) {
delay(100L)
println("Coroutine2 실행 완료")
}
}
- 코루틴 빌더 함수에 try catch 문을 사용하면 예외를 잡지 못한다.
- 코루틴 빌더 함수는 코루틴을 생성하는데 사용하는 함수이기 때문에 try-catch문을 감싸면 try-catch문은 coroutine이 잘 생성되는지만 확인합니다.
- 내부의 코드는 Coroujtine이 디스패처를 통해 thread로 보내져 실행된 시점에 실행되기 때문에 try-catch문은 그 곳에서 발생한 예외를 잡지 못한다.
- 코루친에 대한 예외를 처리하기 위해서는 launch 함수 자체의 try-catch문을 사용하지 않도록 주의해야한다.
async의 예외 노출
- async 코루틴 빌더 함수는 코루틴의 결과값을 Deferred 객체에 감싸고, await 호출 시점에 결과값을 노출한다.
- 만약 코루틴 실행 도중에 예외가 발생해 결과값이 없다면, Deferred에 대한 awiat 호출 시 예외가 노출된다.
fun main() = runBlocking<Unit> {
supervisorScope {
val deferred: Deferred<String> = async(CoroutineName("Coroutine1")) {
throw Exception("Coroutine1에 예외가 발생했습니다")
}
try {
deferred.await()
} catch (e: Exception) {
println("[노출된 예외] ${e.message}}")
}
}
}
- async 코루틴 빌더 함수 사용 시 가장 많이 하는 실수가 await 함수 호출부에서만 예외 처리를 하는 것이다.
- async 코루틴 빌더 함수도 launch 코루틴 빌더 함수와 마찬가지로 예외가 발생하면 부모 코루틴으로 예외를 전파한다.
- async 코루틴 빌더를 사용할 때는 전파된 예외와 노출된 예외를 모두 처리해줘야 한다.
전파되지 않는 예외
CancellationException
- 부모 코루틴으로 전파되지 않는다.
fun main() = runBlocking<Unit>(CoroutineName("runBlocking 코루틴")) {
launch(CoroutineName("Coroutine1")) {
launch(CoroutineName("Coroutine2")) {
throw CancellationException()
}
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
// 실행 결과
[main @runBlocking 코루틴#1] 코루틴 실행
[main @Coroutine1#2] 코루틴 실행
'안드로이드' 카테고리의 다른 글
코루틴 이해하기 (0) | 2025.01.14 |
---|---|
일시 중단 함수와 코루틴 (0) | 2025.01.13 |
구조화된 동시성 (0) | 2025.01.11 |
CoroutineContext란 (0) | 2025.01.10 |
코루틴으로부터 결과 수신 받기 (0) | 2025.01.08 |