루피도 코딩한다

[Coroutine Basics 5] 코루틴의 예외 처리와 Job의 상태 변화 본문

Coroutine

[Coroutine Basics 5] 코루틴의 예외 처리와 Job의 상태 변화

xiaolin219 2024. 1. 21. 03:07

이전 글에서 CancellationException()을 통해 코루틴을 취소하는 방법에 대해 알아보았다.

그러면 코루틴에서 예외가 발생할 때 무슨일이 벌어지고, 어떻게 예외처리를 해야할까?

코루틴에서는 예외가 발생하면 '부모 코루틴'에게 예외가 전파된다.

따라서 첫번째로 코루틴의 계층 관계에 대해 정리해보도록 하자!

fun main(): Unit = runBlocking {
  val job1 = launch {
    delay(1_000L)
    printWithThread("Job 1")
  }
  val job2 = launch {
    delay(1_000L)
    printWithThread("Job 2")
    } 
}

위 코드의 경우 runBlocking이라는 최상위(aka. root) 코루틴에 launch로 만들어진 두개의 자식 코루틴이 있는 구조이다.

이 job1과 job2를 runBlocking과 독립적으로 만들려면 어떻게 해야할까?

CoroutineScope 함수를 이용해 새로운 영역을 만들고 해당 영역에서 launch를 호출하면 된다.

(Android의 경우 ViewModelScope도 root 코루틴을 생성한다)

계층이 있는 코루틴 구조 각각이 root 코루틴
image-20240119204227688 image-20240119204239251

Case1. 최상위 코루틴에서 예외 발생한 경우

  • launch : 예외 발생 시 예외 출력 후 코루틴 종료
  • async : 예외 발생하더라도 예외 출력 안함 (정상 종료)
  • async 내부에서 예외 발생 후 await() 호출 : 예외 출력 후 코루틴 종료

Case2. 자식 코루틴에서 예외 발생한 경우

  • launch, async 둘 다 예외 출력 후 die..
  • 코루틴 안에서 발생한 예외가 부모 코루틴으로 '전파'되기 때문이다
  • 자식 코루틴의 예외가 runBlocking으로 열린 부모 코루틴으로 이동되며, 부모코루틴도 취소하는 절차에 들어가게 된다.

Case3. 자식 코루틴에서 예외가 발생했지만, 부모에게 전파하지 않는 경우

  • async : 자식 코루틴을 생성할때 SupervisorJob()을 넣어주면 예외가 전파되지 않는다.

    • 단, 자식 코루틴의 job을 await()로 조회하는 경우에 예외가 발생하게 된다.

    • fun main() = runBlocking<Unit> {
          val scope = CoroutineScope(Dispatchers.IO + SupervisorJob() + ceh)
          val job1 = scope.launch { printRandom1() }
          val job2 = scope.launch { printRandom2() }
          joinAll(job1, job2)
      }
  • SupervisorScope : 코루틴 스코프와 슈퍼바이저 잡을 합친듯 한 SupervisorScope도 존재한다

    • 자식의 실패가 부모에게 전달되지 않기 때문에, 자식수준에서 예외를 처리해 주어야 한다

      import kotlin.random.Random
      import kotlin.system.*
      import kotlinx.coroutines.*
      
      suspend fun printRandom1() {
          delay(1000L)
          println(Random.nextInt(0, 500))
      }
      
      suspend fun printRandom2() {
          delay(500L)
          throw ArithmeticException()
      }
      
      suspend fun supervisoredFunc() = supervisorScope { ✅
          launch { printRandom1() }
          launch(ceh) { printRandom2() } ✅
      }
      
      val ceh = CoroutineExceptionHandler { _, exception ->
          println("Something happend: $exception")
      }
      
      fun main() = runBlocking<Unit> {
          val scope = CoroutineScope(Dispatchers.IO)
          val job = scope.launch {
              supervisoredFunc()
          }
          job.join()
      }

그러면 async는 supervisorjob을 이용해 부모 코루틴에 전파되는 것을 막았다고 해보자.

launch의 경우 어떻게 처리해야할까? launch에서 발생한 에러를 핸들링 하려면 두가지 방식이 있다.

1. try-catch 활용하기

fun main(): Unit = runBlocking {
val job = launch() { // async도 마찬가지로 try-catch를 적용할 수 있다
    try {
      throw IllegalArgumentException()
    } catch (e: IllegalArgumentException) {
      printWithThread("end")
        }
    }
}

2. CoroutineExcpetionHandler

  • CoroutineExceptionHandler 객체는 코루틴의 구성 요소와 발생한 예외를 파라미터로 받을 수 있다
  • launch와 함께 쓰이며, 부모 코루틴이 있으면 동작하지 않는다

Excpetion에 대해 얘기해보자

  1. CancellationException : 취소로 인식되며 부모 코루틴에게 전파하지 않음
  2. 나머지 예외 : 실패로 간주하고 부모 코루틴에 전파됨

State Machine을 보면, Cancellation Exception이 발생했을 때

NEW -> ACTIVE -> CANCELLING -> CANCELLED 상태가 된다.

만약 정상 처리 과정이라면

NEW -> ACTIVE -> COMPLETING -> COMPLETED 상태가 된다.

위 Life Cycle에 대한 자세한 내용은 다음 포스팅에서~

Comments