본문으로 바로가기

[Kotlin] 코틀린 - 코루틴#6 - supervision

category 개발이야기/Kotlin 2018. 12. 12. 20:32
반응형

이 글은 아래 링크의 내용을 기반으로 하여 설명합니다.

https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md

또한 예제에서 로그 print시 println과 안드로이드의 Log.e()를 혼용합니다.


코루틴에서 Exception은 자식, 부모 양방향으로 전부 전달됩니다.

UI component 같은곳에서 하나의 Job을 사용하면 UI 자체를 destroy하거나 화면을 떠나는 경우 모든 자식들을 취소 시킬 수 있습니다.

다만, 자식중 하나가 실패되면 모든 UI component가 취소되는 상황도 같이 일어납니다.


Supervision job

이렇게 한방향으로만 취소를 전달하기 위한 방법으로 SupervisorJob이 있습니다.

SupervisorJob의 경우 아래 방향으로만 취소를 전파시킵니다


runBlocking {
        val supervisor = SupervisorJob()
        try {
            with(CoroutineScope(coroutineContext + supervisor)) {
                // launch the first child -- its exception is ignored for this example (don't do this in practice!)
                val firstChild = launch(CoroutineExceptionHandler { _, exception -> println("caught $exception")}) {
                    println("First child is failing")
                    throw AssertionError("First child is cancelled")
                }
                // launch the second child
                val secondChild = launch {
                    firstChild.join()
                    // Cancellation of the first child is not propagated to the second child
                    println("First child is cancelled: ${firstChild.isCancelled}, but second one is still active"
                    )
                    try {
                        delay(Long.MAX_VALUE)
                    } finally {
                        // But cancellation of the supervisor is propagated
                        println("Second child is cancelled because supervisor is cancelled")
                    }
                }
                // wait until the first child fails & completes
                firstChild.join()
                println("Cancelling supervisor")
                supervisor.cancel()
                secondChild.join()
            }
        } catch (e: CancellationException) {
            println("CoroutineScope is cancelled!")
        }
    }


위 코드를 수행하면 아래와 같은 log가 찍힙니다.
First child is failing
caught java.lang.AssertionError: First child is cancelled
First child is cancelled: true, but second one is still active
Cancelling supervisor
Second child is cancelled because supervisor is cancelled

firstChild에서 exception이 발생했으나, 부모로 전달되지 않으므로 secondChild는 정상적으로 수행 됩니다.
firstChild는 어찌됐건 완료가 됩니다.(exception이 발생하더라도 완료 입니다.)
그 이후 supervisor job을 cancel()하면 자식인 secondChild가 취소됩니다.

만약 context에 supervisor"+" 하지 않는다면 아래와 같은 로그가 찍힙니다.
First child is failing
CoroutineScope is cancelled!

fisrtChildlaunch하면서 exception이 발생하므로 second child에는 수행조차 안됩니다.
또한 이 exception이 부모 coroutineScope으로 전달되므로 부모 scope도 취소되었다는 로그가 찍힙니다.

Supervision Scope

위와 같은 역할을 하는 supervisorScope도 존재합니다.
위 이는 coroutineScope와 동일한 동작을 하지만 취소에 대한 전파는 자식으로만 진행됩니다.
fun main() = runBlocking {
    try {
        supervisorScope {
            val child = launch {
                try {
                    println("Child is sleeping")
                    delay(Long.MAX_VALUE)
                } finally {
                    println("Child is cancelled")
                }
            }
            // Give our child a chance to execute and print using yield 
            yield()
            println("Throwing exception from scope")
            throw AssertionError()
        }
    } catch(e: AssertionError) {
        println("Caught assertion error")
    }
}


Child is sleeping
Throwing exception from scope
Child is cancelled
Caught assertion error

supervisorScope를 사용하면 부모로는 취소를 전달하지 않습니다.

위 예제에서는 supervisorScope 자체에서 exception이 발생했으므로 자식인 Child는 취소됩니다.

만약 아래와 같이 코드를 고친다면 어떻게 될까요?

runBlocking {
            try {
                supervisorScope {
                    val child = launch {
                        try {
                            Log.e(TAG, "Child is sleeping")
                            delay(Long.MAX_VALUE)
                        } finally {
                            Log.e(TAG, "Child is cancelled")
                        }
                    }

                    val child2 = launch {
                        Log.e(TAG, "Throwing exception from scope")
                        throw AssertionError()
                    }
                }
            } catch (e: AssertionError) {
                Log.e(TAG, "Caught assertion error")
            }
        }

위 예제와 다른건 throw를 또다른 launch 안에서 했다는 겁니다.

이런 경우 아래와 같이 찍힙니다.

Child is sleeping 

Throwing exception from scope 

Exception in thread "main" java.lang.AssertionError


위 예제에서는 AssertionError을 처리하지 못하고 Exception이 그대로 발생되면서 process가 중지 됩니다.

SuperviorScope인 경우 부모로 실패를 전달하지 못하기 때문에 해당 자식이 반드시 Exception을 처리해야만 합니다.

위 예제가 coroutienScope이었다면 try-catch가 동작하면서 예제의 모든 로그(네개)가 다 찍힙니다.


Exceptions in supervised coroutines

따라서 supervisorScope을 사용하는 경우에는 필요에 따라 각각의 자식 coroutine에 exception handler를 달아줘야 합니다.
fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception -> 
        println("Caught $exception") 
    }
    try {
         supervisorScope {
             val child = launch(handler) {
                 println("Child throws an exception")
                 throw AssertionError()
             }
             println("Scope is completing")
         }
         println("Scope is completed")
    } catch(e: Exception) {
        Log.e(TAG, "Exception happen!")
    }
}


Scope is completing
Child throws an exception
Caught java.lang.AssertionError
Scope is completed

만약 coroutineScope을 썼다면 아래와 같이 찍힙니다.
Child throws an exception
Exception happen!


Summary

  • coroutineScope
    • Exception이 발생시 자식 Coroutine을 모두 취소후 부모로 Exception이 전달 시킨다.
    • 부모로 전달된 Exception은 try-catch으로 처리해야 한다. exception handler로는 처리할 수 없다.
      • 단!! try-catch는 해당 scope 내부에 존재하면 exception을 handling 할 수 없다.
      • 실패가 부모 scope으로 전달되는 특성때문으로, try-catch를 하는 부분은 exception이 발생한 곳과 다른 coroutineScope이어야 한다.
  • supervisorScope
    • SupervisorScope 자체에서 Exception이 발생하면 자식 Coroutine을 모두 취소시킨후 부모로 Exception을 전달 시킨다.
    • SupervisorScope는 param으로 Handler를 설정할 수 없기 때문이다.
    • SupervisorScope의 자식 coroutine에서 Exception이 발생하는경우 자식 스스로 Exception을 처리해야 한다.
      • 자식이 Handler를 달든..자식 내부에서 try-catch를 하든...
      • 이는 부모로 실패를 전달하지 않는 특성에 기인한다.
      • 만약 자식이 Handler 없이 exception을 일으키면 외부에서는 try-catch로는 이 exception을 handling 할 수 없다!!!
      • 외부에서 CoroutineExceptionHandler를 이용해서 처리해야 한다.
    • 여러개의 coroutine builder 중첩되어 안쪽에서 excpetion이 발생하는 경우 handler는 가장 바깥쪽에 위치해야만 exception을 전달받을 수 있다.
       supervisorScope {
          launch(handler) { 
              launch {
                  launch {
                      throw IOException
                  }
               }
           }
      }
  • supervisorJob
    • handler는 supervisorJob과 동일한 위치에 존재하거나 상위에 존재해야만 정상적으로 동작한다.
    • supervisorJob이 다른 위치 (launch)에 존재하면 exception이 위로 전파되지는 않으나, handler는 동작하지 않는다.

      val supervisorJob = SupervisorJob()
      
      // case #1: supervisorJob으로 인해 process가 죽지않고 exception은 위로 전달해 준다.
      // 따라서 정상적으로 handler가 동작한다.
      launch(handler) {
          launch {
              launch(supervisorJob) {
                  throw IOException()
              }
          }
      }
      
      // case #2: supervisorJob으로 인해 process가 죽지않고 exception은 handler에서 바로 처리한다.
      launch {
          launch {
              launch(supervisorJob + handler) {
                  throw IOException()
              }
          }
      }
      
      // case #3: exception이 발생한 상태에서는 supervisorJob과는 상관 없으므로 handler는 동작하지 못한다.
      // 단 supervisorJob까지 exception이 올라오면 더이상 위로 올라가는걸 막는다.
      launch(supervisorJob) {
          launch {
              launch(handler) {
                  throw IOException()
              }
          }
      }


반응형