Notice
Recent Posts
Recent Comments
Link
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

훌륭한 개발자가 되기 위하여

코루틴 예외 처리 본문

안드로이드

코루틴 예외 처리

jay20033 2025. 1. 12. 22:00

예외 처리

예외 전파

  • 코루틴 실행 도중 예외가 발생하면, 예외가 발생한 코루틴이 취소되고 예외가 부모 코루틴으로 전파
  • 예외를 전파 받은 코루틴도 예외를 적절히 처리하지 않는다면 취소되고, 그 상위의 코루틴으로 예외가 전파된다.
  • 코루틴이 예외를 전파 받아 취소되면, 취소가 해당 코루틴의 하위에 있는 자식 코루틴들에 전파된다.

예외 전파 제한하기

코루틴의 구조화를 깨서 예외 전파를 제한하기

  • 코루틴의 구조화를 깨면 예외 전파를 제한할 수 있다.
  • 단순히 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