Excetion과 Cancellation에 대해서 이해하기 전에, Scope에 대해서 간단히 정리할 필요가 있습니다.

 

 

1. Scope 와 CoroutineContext

 Couroutine을 시작하기 전에, 항상 Scope를 정의해주어야 합니다.

ViewModel에서 사용할 때는 viewModelScope를 이용해서 쉽게 사용할 수 있지요.

 

아래는 보통 CoroutineScope를 생성하는 방법인데요.

CoroutineScope에 인자로 들어가는 것은 CoroutineContext로 아래와 같은 것들로 구성되어 있습니다.

  • job
    • 해당 coroutine을 control하기위해서 사용
  • coroutineDispatcher
    • 저희가 알고있는것처럼, Main과 IO같은 Dispacher들이 있습니다.
  • CoroutineName
    • 나중에 구분하기 위해서 사용하는 이름입니다.
  • CoroutineExceptionHandler
    • catch되지 않은 Exception을 Handling하기 위해서 사용합니다.

이들은 아래에서 보는 것처럼 "+"를 이용해서 합쳐서 구성하여 인자로 넘겨줄수 있습니다.

 

val scope = CoroutineScope(Job() + Dispatchers.Main)
val job = scope.launch {

}

 

위에서 CoroutineScope의 인자로 Coroutine Context들을 넘겨줄 수 있었는데요.

그런데 위에서 Job에 대해 Coroutine을 컨트롤 하기위해 사용한다고 간단히 정의해보았는데요.

Job은 또 왜 알아야 하는 것 일까요?

Job은 5가지의 lifecycle을 가지고 있습니다.

각 lifecycle안에서 control할수 있는 함수를 제공해주지는 않지만,

isActive, isCancelled 그리고 isCompleted 를 통해서 어떤 상태인지 알수 있습니다.

 

  • New,
  • Active,
  • Completing,
  • Completed,
  • Cancelling -> Cancelled

 

만약 active상태라면, job.cancel()을 해주면, cancel상태로 변경되겠지요.

한가지 알아두어야 할 것은, 해당 CoroutineScope에 속한 Coroutine들의 과업이 모두 끝나면,

Cancelled state로 가게 되고, job에 대해서 isCompleted()함수를 사용하면 true를 return해 줍니다.

 

B. CouroutineContext

위에서 CouroutineContext는 조합해서 인자로 넘겨줄 수 있다고 하였는데요.

주의를 해야하는 부분이 있습니다.

부모에 속해져 있지만, 부모와는 다른 형태로 override되거나 조합된 자식 Coroutine이 존재할 수 있다는 것 이지요.

이것도 Exception이나 Cancellation에 대해서 생각할 때  고려해 두어야 합니다.

따라서 부모의 Job과 자식의 Job객체도 달라지게 됩니다.

 

 

2. Structured 와 Cancellation

Scope를 알아야 하는 이유는, 그것이 HierArchy를 가지고 있기 때문에,

Exception이나 Cancellation을 처리할 때 매우 중요하기 때문입니다.

CouroutineScope는 Coroutine을 생성할 수 있고,

그안에 또다른 Coroutine이 생성될 수 있습니다.

부모와 자식같은 관계가 형성되는 것 이지요.

부모의 자식의 자식의 자식들이 생길수 있습니다.

 

이렇게 관계가 형성된 Coroutine들을 일일히 Cancelling할 필요는 없습니다.

아래와 같이 같은 scope의 다른 job들도 한번에 cancle()함수로 cancel할 수 있습니다.

또한, 그 아래의 자식들도 모두 cancel이 되게 되지요.

job1으로 정의한 Coroutine에 대해서 단독으로 cancel하는 것도 가능합니다.

이 때 알아두어야 할 것은 같은 부모의 다른 job에게는 Cancellation이 영향을 주지 않는다는 것 입니다.

 

val job1 = scope.launch { 
}
val job2 = scope.launch { 
}
scope.cancel()

 

3. throw를 이용한 Cancellation

 

Coroutine에서 Cancel을 하는 방법은 CancellationException을 throw함으로서 실행되도록 설계되어 있습니다.

디버깅하면서, CancellationException을 잡았다고 문제가 있는 것은 아닌 것이지요.

 

부모아래에 있는 하나의 Coroutine이 Cancel되었다고 가정해 보겠습니다.

그럼, 그 자식은 부모에게 CancellationException을 통해서 알려줍니다.

 

 

4. Cancel된 Scope에서 새로운 Coroutine은 생성할 수 없다.

 

 

5. 안드로이드는 viewModelScope와 lifecycleScope

Cancel에 대해서 들을수록 뭔가 복잡하게 컨트롤해야할 것 같은 느낌이 들수 있는데요.

Android에서는 viewModelScope와 lifecycleScope가 있어서,

복잡한 케이스가 아니라면, 실질적으로 Cancel에 대해서 신경을 쓸 필요가 없습니다.

 

위에서 저희가 정리해본것들은 사실 Exception을 이해하기 위한 기본적인 것들이구요.

사실 실제로 Canceldㅔ 대해서 AndroidCoding을 할 때 복잡하게 할 것들은 없습니다.

 

6. Exception이 난 자식들과 부모

부모 Coroutine아래 여러 자식 Coroutine중 하나의 Coroutine에 Exception이 생겼다고 가정해 보겠습니다.

그럼 아래와 같은 일들이 일어나게 됩니다.

 

  1. Exception을 부모에게 전달
  2. 부모가 나머지 자식과 자식Coroutine들을 Cancel
  3. 상위의 부모가 있다면, 그 부모에게 Exception을 전달

하나의 CoroutineScope에 있는 부모들에게 계속 올라가서, root까지 도달하게 됩니다.

 

 

 

 

 

B. SupervisorJob

CoroutineContext를 생성시에, SupervisorJob을 이용했다면,

Exception이 나서 fail된 자식은 다른 자식들이나 부모에게 영향을 치지 않습니다.

SupervisorJob은 Exception을 퍼트리지않고, 자식이 handling하도록 해 줍니다.

 

Job을 아래와 같이 생성할 경우에만 해당이 되는데요.

 


 supervisorScope


CoroutineScope(SupervisorJob())

 

 

아래에서 child1이 Exception이 나더라도, 부모나 다른 자식을 cancel시키지 않습니다.

 


val scope = CoroutineScope(SupervisorJob())
val child1= scope.launch { }
val child2= scope.launch {}

 

위에서 SupervisorJob을 이용하면 문제가 없을것 같이 이야기 했지만,

만약 Exception이 Handling되지 않았고 CorotuineExceptionHandler가 없다면,

Uncaught Exception이 throw되게 됩니다.

 

그런데, 문제는 아래와 같은 경우에 일어납니다.

단순히 보면, SupervisorJob아래에 job들이 있으므로,

문제가 없다고 생각할 수 있는데요.

새롭게 launch가 되면, 새로운 Job이 주어지게 됩니다.

따라서 아래의 각각의 자식들은 SupervisorJob이 아닌,

일반 Job의 아래에 있는 것 입니다.

따라서 childJob1에 문제가 생기면 모두에게 Exception이 전파되고, Crash가 발생하게 되겠지요.

 


val scope = CoroutineScope(Job())
scope.launch(SupervisorJob()) {
    vla childJob1 = launch { }
    vla childJob2 = launch { }
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

1. Cancellation Exception

Coroutine을 사용하다가, try catch를 다 뚫고 어떤 Exception이 났을 때,

보통은 아래와 같은 log를 보게되거나,

catch로 Exception을 잡아서 보면, CancellationException 임을 알 수 있습니다.

이는 Coroutine의 메커니즘하고도 관련이 있는데요.

Coroutine에서는 Exception발생시 부모를 cancel합니다.

부모가 Cancel되면 그 부모의 Child 들은 당연히 Cancel 되겠지요.

한가지 주의할 것은,

Coroutine자체가 cancel하면서 CancellationException을 발생시켜서 cancel 하는 구조로 되어있기 때문에,

이것을 bug로 인식하고 대응해서는 않된다는 점 입니다.

CancellationException은 코루틴 고유의 메커니즘 입니다.

 

 

Coroutine의 모든 suspend function은 이렇게 exception이 발생할 경우,

cancel의 대상이 됩니다.

하지만, 만약 coroutine이 computation등 다른 쓰레드에서 일하고 있고 cancellation을 체크하지 않는다면,

cancel이 되지 않습니다.

문제는 바로 이 지점에서 발생하는 것 인데요.

 

2. Exception 발생시

위에서 언급한 대로, Coroutine은 Exception발생시에,

Scope내의 자식에게 Exception을 발생시키는 것 뿐만이 아니라,

부모에게도 전파하여 CancellationException을 이용해 부모를 Cancel 시키는 데요.

 

중요한 점은 Exception처리를 해야할 때,

부모에게 전파된 CancellationException으로 인하여, 부모가 cancel되버리면,

Exception을 catch해서 계속 부모의 job을 이어가려고 했던 의도대로 되지 않는 것 이지요.

이 부분이 특히 처음 Coroutine을 사용할 때, 가장 어려운 부분중의 하나이구요.

Exception을 찍어보면, 해당 Exception의 Catch는 되지않고,

Job이 Cancel되었다는 로그만 나오게 되지요.

 

 

예를 들어 withTimeout()함수를 이용해서,

특정시간안에 값이 나오지 않으면, TimeoutCancellationException이 발생하도록 하였다고 가정해 보겠습니다.

TimeoutCancellationException을 catch해서 무언가를 하도록 하였을 텐데요.

의외로 해당 TimeoutCancellationException은 전파되지 않고, job만 위의 코드와 같이 cancelled되었다고 나옵니다.

catch로 들어간 것 같이 보이지만, 그렇지 않은 것 이지요.

 

3. withTimeout 제대로 알고 쓰자.

withTimeout의 Api를 보면 다음과 같습니다.

인자로 들어온 시간안에 실행되지 않으면,

블록안의 코드들은 cancel 이 됩니다.

그리고, 내부의 cancel이 가능한 suspending function이 TimeoutCancellationException을 throw 한다고 나와있습니다.

즉, withTimeout이 제대로 동작하기 위해서는, 내부에 suspending function을 필요로 하는 것 이지요.

 

 

3. async와 await사용시

 

 

 

 

2. Cancellation is cooperative
CancellationException 코루틴에서 정상적인 완료로 보기 때문에,

 

 

 

 

 

 

3. 특수한 케이스들

 

3. withTimout

withTimeout을 사용할 경우, 불편한 경우가 발생합니다.

바로 Coroutine에서의 까다로운 Exception처리 때문입니다.

항상 Async나 withTimeout처럼 블록안에 들어가는 API를 처리할 경우에는

조금 긴장하는 것이 좋습니다.

왜냐하면 이들의 Exception처리가 마냥 쉽지 않기 때문입니다.

withTimeout에서 Exception처리에 힘을 빼느니,

withTimeoutOrNull Api를 이용해서 Exception으로 처리하지 않는 것도 하나의 방법일 수 있습니다.

 

 

 

 

728x90

+ Recent posts