Kotlin의 Coroutine은 RxJava를 대체하고 있다고 할 정도로,

정말 많이 쓰이고 있는데요.

Coroutine에서의  Exception을 핸들링 하는데 있어서,

의외로 여러가지 고려해야 할 점들이 많이 있습니다.

오늘은 이것들에 대해서 정리해 보려고 하는데요.

그 전에, 기본적으로 알아두어야 할,

Coroutine의 Structured Concurrentcy에 대해서 알아보며 시작하겠습니다.

 

1. Structured Concurrency

Coroutine들을 론칭하고 이것들을 제대로 관리하지 않는다면,

메모리 부족등 여러가지 문제에 부딪히게 됩니다.

이것들의 reference count를 일일히 관리하는 것도 좋은 방법은 분명 아니어서,

Coroutine은 Structured concurrency 라는 것을 사용하였습니다.

 

이것은, Coroutine을 론칭할 때, GlobalScope에 론칭하는 것이 아니라,

특정한 Scope를 정해놓고 그안에서 론칭을 해 주는 것 인데요.

그럼 이 scope안에 론칭된 coroutine들을 해당 부모의 scope에서 관리함으로서,

해당 Scope를 종료하면 Coroutine들을 한번에 종료시키는 것과 같은 방식으로,

문제를 해결하는 것 인데요.

당연히 CoroutineScope안에는 또 다른 Child CoroutineScope가 계속 존재할 수 잇겠지요.

가장 기본적인 방식은 Child나 부모에서 Exception발생시 해당 Exception이 양방향으로 전파되어서,

부모의 부모들 그리고 Child모두 Cancel되게 되어있습니다.

 

2. Coroutine안에서 Catch되지 않은 Exception

throw된 Exception이 catch되지 않으면

그것을 감싸고 있는 함수로 계속 전파가 되게 되는 것이 일반적인 함수에서의 동작인데요.

일반 함수에 비해서 다시 throw되는 경우에 있어서 좀 더 생각해 보아야 하는 경우가 있는데요.

 

바로, 해당 함수의 child Coroutine에서 나온 Exception이 catch되지 않았을 때 입니다.

아래 코드를 보면, child1에서 NoNetworkException을 throw하고 있는데요.

이것은 어디에서도 catch되고 있지 않은 상태입니다.

이 Exception에 대해서 Coroutine은 어떻게 동작할까요?

 

 

CoroutineScope에서 NoNetworkException을 다시 throw할 것이라고 생각할 수도 있는데요.

(물론 suspend함수 안에서는 실제로 그렇게 동작합니다.)

하지만, CoroutineScope에서는 해당 Scope안에 있는 Coroutine이 cancel 되어 버립니다.

위에서 언급한 Structured Concurrency에서, Coroutine은 scope단위로 관리를 한다고 했는데요.

해당 Scope에서 Catch되지 않는 Exception이 발생하면,

CancellationException을 발생시켜서 해당 Scope를 Cancel시켜 버립니다.

 

그리고 해당 Scope내의 Child에서 Catch되지 않는 Exception이 발생하면,

해당 Exception에 의해서, 부모와 Child모두 Cancel되어 버립니다.

이는 원치않는 결과와도 연결될 수 있으므로, 잘 관리를 해 주어야 합니다.

Coroutine Hierarchy를 잘 관리해 주어서 원하지 않는 Coroutine이 cancel되어 버리는 일이 없도록 해야하는 것 이지요.

 

예를 들면, repeat되는 부모K와 async로 빌드된 자식A, 자식B가 있다고 하겠습니다.

자식A가 Exception을 발생시에,

부모 Coroutine과 async블록으로 된 자식B의 경우도 마찬가지로 Cancel되어 버릴 수 있다는 것을 꼭 생각해 두어야 합니다.

cancel되지 말아야 할 부모K까지 cancel되어 버릴 수 있는 것 이지요.

아무 생각없이 여기저기 Coroutine을 마구 생성해서는 않되는 것 이지요.

Exception의 전파를 고려해서 Hierarchy를 생각해 두어야 합니다.

 

3. 까다로운 async 코루틴 빌더

아래 코드를 보면, try catch로 async로 빌드된 코루틴의 NoNetworkException을 잡아주고 있습니다.

따라서 정상적으로 동작할 것이라고 기대하게 되는데요.

 

 

실제로는 아래와 같이 앱은 Crash되고 Exception을 보게 됩니다.

아니 throw를 catch했는데 왜 Exception이 나오게 된 걸까요?

그것은 Exception으로 해당 Coroutine이 cancel되면서 자신의 가장 상위의 부모 CoroutineScope에 까지 영향을 주었기 때문입니다.

 

 

4. ExceptionHandler

async와 같이 결과같이 나오는 Coroutine Builder에서는 아래와 같이 exceptionHandler를 만들어서,

최상위 부모의 CoroutineScope()에 인자중 하나로 넣어주면 정상적으로 동작을 하는데요.

Coroutine Hierarchy에서 가장 위에서 마지막으로 Exception을 처리해 주는 것 이지요.

당연히도 로그를 보면, 중간에 System.err에 로그가 남는 경우가 있습니다.

해당블록에서 catch되지 않았기 때문에 에러가 발생했고, 이것에 대해 로그가 남는 것 이지요.

 

이 방법이외에도, android의 Thread.UncaughtExceptionHandler를 구현해서 부모에게 까지 전파되는 Exception을 잡아낼 수도 있습니다.

한가지 주의할 것은 만약 aysnc에서  await함수를 사용하지 않을 경우, Exception은 전파되지 않을 것 이라는 것 입니다.

 

재미있는 것은 async가 아니라 launch를 이용해서, Coroutine을 빌드하였다면,

이러한 현상은 발생하지 않는다는 것 인데요.

왜냐하면, launch로 부터 나온 결과값은 전부다 무시되기 때문입니다.

return 값이 필요할 경우에는 async를 사용하고 그렇지 않을 때 쓰는 것이 launch이기 때문이지요.

따라서 그로부터 나온 Exception역시 결과값과 같이 무시되어 집니다.

 

실행해 보면 아무이상없이 실행되는 것을 알 수 있습니다.

CoroutineException을 핸들링하다보면 처음에 드는 생각이,

왜 async만 이렇게 까다로운것일가 생각이 들 수 있는데요.

사실은 반대로, async만 해당 결과값의 영향이 부모에게 전달되는,

정상적인 structuredConcurrency에 따른 것이지요.

그런데도, 이렇게 두번 씩이나 Exception처리를 해 주어야 하는지 생각이 들게 됩니다.

(물론 Exception이 나게 될 경우의 값을 catch에서 디폴트로 줄 것이 아니라면, 두번 처리하지 않아도 됩니다.)

그래서 경우에 따라서는 다음과 같은 방법을 이용해 주어도 되는데요.

저도 개인적으로는 async블록안에서 catch해서 해결하거나 아래의 방법을 이용을 많이 하게 되네요.

 

4. SupervisorScope

SupervisorScope를 사용하면, Child에서 Exception이 발생하더라도,

부모와 다른 Child에게 영향이 가지 않게 하는 방법으로도,

async에서 catch해도 전파되는 Exception을 해결할 수도 있습니다.

아래와 같이 supervisorScope를 사용하면, async에서 throw된 Exception이

caught되어서 Crash 되지 않고 정상적으로 동작하게 됩니다.

 

 

4-1.  supervisorScope와 CoroutineScope의 방식의 차이점

supervisorScope를 대안으로 사용할 때 주의할 점은,

supervisorScope와 CoroutineScope의 방식의 차이점을 잘 알고 있어야 한다는 것 입니다.

 

만약 아래의 코드와 같이 두가지 경우가 있다고 가정해 보겠습니다.

둘다 getApiCall()과 getApi2Call()을 호출한 결과 값을 mResult와 mResultInSvScope에 넣어서 사용하는데요.

CoroutineScope에 있을 경우는,

둘 중 하나의 Api콜만 실패하는 순간, 해당 Coroutine은 Cancel이 되고,

이는 즉시 부모 Coroutine에게 계속 전파되어서, exceptionHandler나 Thread.UncaughtExceptionHandler가 catch해 주지 않으면, 부모들의 부모들과 그의 Child 코루틴들도 모두 취소하게 되는 결과를 가지게 됩니다.

 

반면, supervisorScope안에 에 있을 경우는, 하나의 Api콜이 fail되더라도, 두개의 async블록이 모두 실행이 끝나고 나서야 해당 Scope에서 Exception이 전파되서 cancel이 되게 됩니다. 대신 supervisorScope이므로 부모나 같은 부모를 둔 child에게는 영향을 끼치지 않겠지요.

 

위에서 정리한 것에 따르면,

네트워크 호출같은 경우에는 supervisorScope을 사용하는게 정답이 아닐 수 있을 것 같습니다.

두개의 네트워크를 호출 할 때, 하나가 Fail이 났다면 다시 Retry를 해 주는 것이 더욱 효율적일 것이기 때문이지요.

Fail이 났는데, 다른 호출의 결과가 나올 때 까지 기다리는 것은 굉장히 비효율적일 수 있습니다.

 

 

 

또 한가지 생각해야 할 것은 기존의 job을 override한다는 점 입니다.

공통의 job에 대해서 cancel을 한 다음 무언가를 하고 있었다면,

이것이 정상적으로 동작하지 않을 가능성에 대해서도 생각해 주어야 합니다.

공식문서에도 아래와 같이 설명되어 있는 것을 볼 수 있습니다.

 

 

supervisorScope가 만능인것처럼 보일 수 있지만,

실제로는 그렇지 않으며 위에서 언급했던 부분들을 고려해서 선택해야 한다는 것 이지요.

 

5. 정리

처음에 글이 Structured concurrency시작한 것 처럼,

Coroutine의 ExceptionHandling은 이를 기반으로 고려되어야 합니다.

해당 코드가 Exception이 생기면, 해당 Scope의 부모와 Child가 Fail이 나게 될 것이고,

그럴 경우 의도된대로 동작하는 코드가 될 것인가가 Exception을 핸들링하는데 있어서,

가장 먼저 고려되어야 할 것이지요.

 

아무생각없이 워커쓰레드 사용을 위해서 여기저기 CoroutineScope를 사용했다면,

한번 더 코드를 보고 가다듬을 필요가 있을 것 같습니다.

728x90

+ Recent posts