Home Coroutine 취소 및 예외처리
Post
Cancel

Coroutine 취소 및 예외처리

일반적인 코루틴 예외처리 방법 (CoroutineExceptionHandler)

코루틴에서 예외를 처리하는 일반적인 방법은 CoroutineScope을 이용해 새 코루틴을 생성할 때 CoroutineExceptionHandler를 이용해 예외를 처리할 수 있다.

코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import kotlinx.coroutines.*
import java.io.IOException
import kotlin.coroutines.cancellation.CancellationException

fun main() = runBlocking {
    val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
        println("coroutineExceptionHandler -> $throwable")
    }

    val job = CoroutineScope(Dispatchers.Default + coroutineExceptionHandler).launch {
        val child1 = launch {
            try {
                throw IOException() // 예외 발생
                delay(3000)
                println("child1 coroutine")
            } catch (e: CancellationException) {
                println("child1 Exception -> ${e.message}")
            }
        }
    }.join()
    println("main")
}

결과

1
2
coroutineExceptionHandler -> java.io.IOException
main

이렇게 함으로써 코루틴을 사용하는 도중에 예외가 발생했을 때 UI에 예외가 발생한 이유를 설명해주거나 다른 작업을 실행하도록 할 수 있다.


코루틴 취소 및 예외 발생

코루틴에서는 기본적으로 구조적 동시성때문에 부모 코루틴이 취소되거나 예외가 발생하면 부모 코루틴의 Scope에 있는 코루틴들은 모두 취소된다. (취소가 전파됨)

  • 부모 코루틴 취소 시

    코드

    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
    31
    32
    33
    34
    35
    36
    37
    38
    39
    
      import kotlinx.coroutines.*
      import kotlin.coroutines.cancellation.CancellationException
    
      fun main() = runBlocking {
          val job = CoroutineScope(Dispatchers.Default).launch {
              val child1 = launch {
                  try {
                      delay(3000)
                      println("child1 coroutine")
                  } catch (e: CancellationException) {
                      println("child1 Exception -> ${e.message}")
                  }
              }
    
              val child2 = launch {
                  try {
                      val child3 = launch {
                          try {
                              delay(3000)
                              println("child 3 coroutine")
                          } catch (e: CancellationException) {
                              println("child3 Exception -> ${e.message}")
                          }
                      }
    
                      delay(3000)
                      println("child 2 coroutine")
                  } catch (e: CancellationException) {
                      println("child2 Exception -> ${e.message}")
                  }
              }
    
              println("parent coroutine")
          }
          delay(2000)
          job.cancel() // 부모 코루틴 취소
    
          println("main")
      }
    

    결과

    1
    2
    3
    4
    
      parent coroutine
      child1 Exception -> StandaloneCoroutine was cancelled
      main
      child 2 Exception -> StandaloneCoroutine was cancelled
    
  • 부모 코루틴 예외 발생 시

    코드

    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
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    
      import kotlinx.coroutines.*
      import java.io.IOException
      import kotlin.coroutines.cancellation.CancellationException
    
      fun main() = runBlocking {
          val job = CoroutineScope(Dispatchers.Default).launch {
              val child1 = launch {
                  try {
                      delay(3000)
                      println("child1 coroutine")
                  } catch (e: CancellationException) {
                      println("child1 Exception -> ${e.message}")
                  }
              }
    
              val child2 = launch {
                  try {
                      val child3 = launch {
                          try {
                              delay(3000)
                              println("child 3 coroutine")
                          } catch (e: CancellationException) {
                              println("child3 Exception -> ${e.message}")
                          }
                      }
    
                      delay(3000)
                      println("child 2 coroutine")
                  } catch (e: CancellationException) {
                      println("child2 Exception -> ${e.message}")
                  }
              }
    
              throw IOException() // 예외 발생
              println("parent coroutine")
          }
          delay(2000)
          job.cancel()
    
          println("main")
      }
    

    결과

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
      child1 Exception -> Parent job is Cancelling
      child3 Exception -> Parent job is Cancelling
      child2 Exception -> Parent job is Cancelling
      Exception in thread "DefaultDispatcher-worker-2" java.io.IOException
          at CehKt$main$1$job$1.invokeSuspend(ceh.kt:34)
          at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
          at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
          at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
          at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:749)
          at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
          at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
          Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@55321535, Dispatchers.Default]
      main
    

만약 자식 코루틴에서 취소가 발생하면 해당 코루틴만 취소되고 다른 코루틴에 취소가 전파되지 않는다.

  • 자식 코루틴 취소 시

    코드

    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
    31
    32
    33
    34
    35
    36
    37
    38
    
      import kotlinx.coroutines.*
      import kotlin.coroutines.cancellation.CancellationException
    
      fun main() = runBlocking {
          val job = CoroutineScope(Dispatchers.Default).launch {
              val child1 = launch {
                  try {
                      delay(3000)
                      println("child1 coroutine")
                  } catch (e: CancellationException) {
                      println("child1 Exception -> ${e.message}")
                  }
              }
              child1.cancel() // 자식 코루틴 취소
    
              val child2 = launch {
                  try {
                      val child3 = launch {
                          try {
                              delay(3000)
                              println("child 3 coroutine")
                          } catch (e: CancellationException) {
                              println("child3 Exception -> ${e.message}")
                          }
                      }
    
                      delay(3000)
                      println("child 2 coroutine")
                  } catch (e: CancellationException) {
                      println("child2 Exception -> ${e.message}")
                  }
              }
    
              delay(1000)
              println("parent coroutine")
          }.join()
          println("main")
      }
    

    결과

    1
    2
    3
    4
    5
    
      child1 Exception -> StandaloneCoroutine was cancelled
      parent coroutine
      child 2 coroutine
      child 3 coroutine
      main
    

하지만, 자식 코루틴에서 예외가 발생하면 그 예외는 최상위 부모 코루틴에 전달되어 모든 코루틴이 취소가 된다. 아래 예제는 child3에 예외를 발생시킨 것으로 먼저 부모 코루틴인 child2가 취소되고 다시 취소가 전파되어 부모 코루틴까지 취소가 됨으로써 child1도 취소가 되었다.

  • 자식 코루틴 예외 발생 시

    코드

    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
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    
      import kotlinx.coroutines.*
      import java.io.IOException
      import kotlin.coroutines.cancellation.CancellationException
    
      fun main() = runBlocking {
          val job = CoroutineScope(Dispatchers.Default).launch {
              try {
                  val child1 = launch {
                      try {
                          delay(3000)
                          println("child1 coroutine")
                      } catch (e: CancellationException) {
                          println("child1 Exception -> ${e.message}")
                      }
                  }
    
                  val child2 = launch {
                      try {
                          val child3 = launch {
                              try {
                                  throw IOException() // 예외 발생
                                  delay(3000)
                                  println("child 3 coroutine")
                              } catch (e: CancellationException) {
                                  println("child3 Exception -> ${e.message}")
                              }
                          }
    
                          delay(3000)
                          println("child 2 coroutine")
                      } catch (e: CancellationException) {
                          println("child2 Exception -> ${e.message}")
                      }
                  }
    
                  delay(1000)
                  println("parent coroutine")
              } catch (e: CancellationException) {
                  println("parent Exception -> ${e.message}")
              }
          }.join()
          println("main")
      }
    

    결과

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
      child2 Exception -> StandaloneCoroutine is cancelling
      parent Exception -> StandaloneCoroutine is cancelling
      child1 Exception -> Parent job is Cancelling
      Exception in thread "DefaultDispatcher-worker-4" java.io.IOException
          at CehKt$main$1$job$1$child2$1$child3$1.invokeSuspend(ceh.kt:21)
          at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
          at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
          at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
          at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:749)
          at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
          at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
          Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@50074ae7, Dispatchers.Default]
      main
    

그렇다면 자식 코루틴에서 예외가 발생하더라도 부모에게 취소를 전파시키지 않는 방법은 무엇이 있을까?
해답은 SupervisorJob 또는 supervisorScope을 이용하는 것이다. 이 두 개는 자식에서 예외가 발생하더라도 부모에게는 취소를 전파하지 않고 자식에게만 취소를 전파하게 된다. 먼저 SupervisorJob을 이용해 부모에게 취소를 전파시키지 않는 방법을 살펴보면 아래와 같다.

  • SupervisorJob

    코드

    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
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    
      import kotlinx.coroutines.*
      import java.io.IOException
      import kotlin.coroutines.cancellation.CancellationException
    
      fun main() = runBlocking {
          val supervisorJob = SupervisorJob() // SupervisorJob 객체 생성
          val job = CoroutineScope(Dispatchers.Default).launch {
              try {
                  val child1 = launch {
                      try {
                          delay(3000)
                          println("child1 coroutine")
                      } catch (e: CancellationException) {
                          println("child1 Exception -> ${e.message}")
                      }
                  }
    
                  val child2 = launch {
                      try {
                          val child3 = launch(supervisorJob) { // supervisorJob 객체 전달
                              try {
                                  throw IOException() // 예외 발생
                                  delay(3000)
                                  println("child 3 coroutine")
                              } catch (e: CancellationException) {
                                  println("child3 Exception -> ${e.message}")
                              }
                          }
    
                          delay(3000)
                          println("child 2 coroutine")
                      } catch (e: CancellationException) {
                          println("child2 Exception -> ${e.message}")
                      }
                  }
    
                  delay(1000)
                  println("parent coroutine")
              } catch (e: CancellationException) {
                  println("parent Exception -> ${e.message}")
              }
          }.join()
          println("main")
      }
    

    결과

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
      Exception in thread "DefaultDispatcher-worker-1" java.io.IOException
          at CehKt$main$1$job$1$child2$1$child3$1.invokeSuspend(ceh.kt:22)
          at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
          at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
          at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
          at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:749)
          at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
          at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
          Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@3fb6433c, Dispatchers.Default]
      parent coroutine
      child1 coroutine
      child 2 coroutine
      main
    

    위 예제에서 본 것처럼, child3에서 예외가 발생하더라도 launch메소드에 SupervisorJob을 전달함으로써 부모에게 취소가 전파되지 않도록 한다. 또한, child2에서 예외가 발생할 경우 부모에게 예외를 전달하지 않지만 child2의 자식 코루틴인 child3에는 취소를 전파함으로써 같이 단방향으로 취소를 전파한다는 것을 알 수 있다.

    코드

    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
    31
    32
    33
    34
    35
    
      import kotlinx.coroutines.*
      import java.io.IOException
      import kotlin.coroutines.cancellation.CancellationException
    
      fun main() = runBlocking {
          val supervisorJob = SupervisorJob()
          [...]
                  val child2 = launch(supervisorJob) {
                      try {
                          val child3 = launch {
                              try {
                                  delay(3000)
                                  println("child 3 coroutine")
                              } catch (e: CancellationException) {
                                  println("child3 Exception -> ${e.message}")
                              }
                          }
                            
                          throw IOException() // 예외 발생
    
                          delay(3000)
                          println("child 2 coroutine")
                      } catch (e: CancellationException) {
                          println("child2 Exception -> ${e.message}")
                      }
                  }
    
                  delay(1000)
                  println("parent coroutine")
              } catch (e: CancellationException) {
                  println("parent Exception -> ${e.message}")
              }
          }.join()
          println("main")
      }
    

    결과

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
      child3 Exception -> Parent job is Cancelling
      Exception in thread "DefaultDispatcher-worker-1" java.io.IOException
          at CehKt$main$1$job$1$child2$1.invokeSuspend(ceh.kt:29)
          at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
          at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
          at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
          at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:749)
          at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
          at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
          Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@1b633969, Dispatchers.Default]
      parent coroutine
      child1 coroutine
      main
    

이렇게 되면 모든 자식 코루틴에 SupervisorJob을 전달해주어야 하는데, 자식 코루틴 개수가 많다면 일일히 다 전달해주기에는 조금 비효율적일 수 있다. 그럴 땐 supervisorScope을 사용할 수 있다. 단, supervisorScope안에 있는 자식 코루틴에만 적용되며 자식 코루틴의 자식 코루틴에서는 적용되지 않는다.

  • supervisorScope

    코드

    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
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    
      import kotlinx.coroutines.*
      import java.io.IOException
      import kotlin.coroutines.cancellation.CancellationException
    
      fun main() = runBlocking {
          val job = CoroutineScope(Dispatchers.Default).launch {
              supervisorScope {
                  try {
                      val child1 = launch {
                          try {
                              throw IOException() // 예외 발생
                              delay(3000)
                              println("child1 coroutine")
                          } catch (e: CancellationException) {
                              println("child1 Exception -> ${e.message}")
                          }
                      }
    
                      val child2 = launch {
                          try {
                              val child3 = launch {
                                  try {
                                      delay(3000)
                                      println("child 3 coroutine")
                                  } catch (e: CancellationException) {
                                      println("child3 Exception -> ${e.message}")
                                  }
                              }
    
                              delay(3000)
                              println("child 2 coroutine")
                          } catch (e: CancellationException) {
                              println("child2 Exception -> ${e.message}")
                          }
                      }
    
                      delay(1000)
                      println("parent coroutine")
                  } catch (e: CancellationException) {
                      println("parent Exception -> ${e.message}")
                  }
              }
          }.join()
          println("main")
      }
    

    결과

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
      Exception in thread "DefaultDispatcher-worker-2" java.io.IOException
          at CehKt$main$1$job$1$1$child1$1.invokeSuspend(ceh.kt:11)
          at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
          at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
          at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
          at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:749)
          at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
          at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
          Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@104cb185, Dispatchers.Default]
      parent coroutine
      child 2 coroutine
      child 3 coroutine
      main
    

    supervisorScope는 내부적으로 부모 코루틴 scope의 coroutineContext를 상속받으며 이 컨텍스트의 JobSupervisorJob로 오버라이드하기 때문에 가능한 일이다. 아래는 supervisorScope에 대한 주석 내용이다.

    image


[참고]

  • https://kotlinlang.org/docs/exception-handling.html
This post is licensed under CC BY 4.0 by the author.

Coroutine에서 Retrofit 사용 시 알아둘 점

버튼 커스텀 drawable 적용이 안될 때

Comments powered by Disqus.