-
[coroutine] exception 을 이해해보자 #1 ( in my coroutine scope ) 과 동일한 sample 들을 직접 생성한 coroutine 이 아닌 GlobalScope 에서 수행해보기로 했다.
혹시나 뭔가 동작이 다를까 해서..
내가 알고 있기로는 GlobalScope 은 defaultHandler 가 setting 되어 결국 모든 exception 을 먹어 crash 가 나지 않는다는 차이만 있는것으로 알고 있는데.. 검증해보자 ( 결론은 잘못 알고 있었다. )
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class MainActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById<Button>(R.id.btn).setOnClickListener { try { GlobalScope.launch { throw RuntimeException( "gamza!" ) } } catch (e: Exception) { Log.e( "cklee" , "MMM exception caught $e" ) } } } } |
try-catch 로 launch 내부의 exception 을 잡을 수 없다는 것은 알고 있었지만.. GlobalScope.launch 안에서 발생한 exception 이 앱을 crash 나도록 할 줄은 생각도 못 했다.
즉, 결과는 crash 이다.
그러나 한편으로는 다행이라는 생각도 든다. GlobalScope 이라고 해서 내가 만든 Scope 와 다른 동작을 하지 않는다는 것은 생각해야 하는 예외가 줄어든다는 의미이기도 하니까..
-
두번째 test 로 GlobalScope 는 기생성되어 있으므로 그녀석 자체의 defaultExceptionHandler 를 바꾸기는 어렵고, depth1 에 해당하는 launch 에 exceptionHandler 를 달아보았다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class MainActivity : Activity() { private val defaultExceptionHandler = CoroutineExceptionHandler { context, throwable -> Log.e( "cklee" , "MMM exceptionHandler caught $throwable" ) } override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById<Button>(R.id.btn).setOnClickListener { GlobalScope.launch(defaultExceptionHandler) { throw RuntimeException( "gamza!" ) } } } } |
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 | class MainActivity : Activity() { private val builderExceptionHandler1 = CoroutineExceptionHandler { context, throwable -> Log.e( "cklee" , "MMM BUILDER_1111 exceptionHandler caught $throwable" ) } private val builderExceptionHandler2 = CoroutineExceptionHandler { context, throwable -> Log.e( "cklee" , "MMM BUILDER_2222 exceptionHandler caught $throwable" ) } private val builderExceptionHandler3 = CoroutineExceptionHandler { context, throwable -> Log.e( "cklee" , "MMM BUILDER_3333 exceptionHandler caught $throwable" ) } override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById<Button>(R.id.btn).setOnClickListener { GlobalScope.launch(builderExceptionHandler1) { launch(builderExceptionHandler2) { launch(builderExceptionHandler3) { throw RuntimeException( "gamza!" ) } } } } } } |
-
global scope 라고 뭐가 다르진 않다.
다만, global scope 의 preset default handler 가 특정 로그를 찍어준다는 사실만이 다르다고 해석될 수 있다.
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 | class MainActivity : Activity() { private val builderExceptionHandler1 = CoroutineExceptionHandler { context, throwable -> Log.e( "cklee" , "MMM BUILDER_1111 exceptionHandler caught $throwable" ) } private val builderExceptionHandler2 = CoroutineExceptionHandler { context, throwable -> Log.e( "cklee" , "MMM BUILDER_2222 exceptionHandler caught $throwable" ) } private val builderExceptionHandler3 = CoroutineExceptionHandler { context, throwable -> Log.e( "cklee" , "MMM BUILDER_3333 exceptionHandler caught $throwable" ) } override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById<Button>(R.id.btn).setOnClickListener { GlobalScope.launch(builderExceptionHandler1) { try { delay(5000L) } catch (e: Exception) { Log.e( "cklee" , "MMM first launch cancelled $e" ) } } GlobalScope.launch(builderExceptionHandler2) { delay(3000L) } GlobalScope.launch(builderExceptionHandler3) { throw RuntimeException( "gamza!" ) } } } } |
일반 coroutineScope 에서는 propagate 되던 세번째 launch 의 RuntimeException 이..
GlobalScope 에서는 소비되고 끝난다.
즉, GlobalScope 는 Supervised job 을 가진 것처럼 작동한다는 이야기이다.
-
혹시나 main thread 가 아니라 그런건가 싶어 dispatcher 를 모두 Main dispatcher 로 설정하고 돌려보았으나 매한가지였다…
즉, GlobalScope 은 그 자체로 Supervised job 처럼 propagation 을 안 한다는 이야기이다.
-
그렇다면 supervisedJob 을 가진 것처럼 동작하니 Job() 을 assign 해보면 어떻게 될까?
한 개의 job 을 만들어 모든 launch 에 assign 해주었다.
E/cklee: MMM BUILDER_3333 exceptionHandler caught java.lang.RuntimeException: gamza!
E/cklee: MMM first launch cancelled kotlinx.coroutines.JobCancellationException: Job is cancelling; job=StandaloneCoroutine{Cancelling}@66efd2b
E/cklee: MMM BUILDER_1111 exceptionHandler caught java.lang.RuntimeException: gamza!
E/cklee: MMM BUILDER_2222 exceptionHandler caught java.lang.RuntimeException: gamza!
내가 만든 coroutineScope 와 동일하게 작동한다.
만약 각각의 launch 에 각각 Job() 으로 다른 instance 를 넘기면..
E/cklee: MMM BUILDER_3333 exceptionHandler caught java.lang.RuntimeException: gamza!
Job 의 성격을 잘 이해하면 동일한 job 을 넘겼을 때와 다른 job 을 던졌을 때의 차이를 이해할 수 있다.
그리고 이는 이렇게 해석될 수 있다. GlobalScope 의 coroutineBuilder 로 만든 녀석들은 parent job 이 Supervised Job 이거나 없거나!!
GlobalScope 의 설명에 가보면 이렇게 설명이 써있다.
A global[CoroutineScope] not bound to any job. ...
결론적으로 GlobalScope 은 job 이 없기 때문에 이렇게 동작한다고 이해할 수 있다.
-
이제 어느정도 Exception 에 대한 정리가 되었다.
* launch 와 auto propagation 을 하는 builder code 내의 exception 은 바깥쪽 try-catch 로 잡을 수 없고, async 같은 user handling propagation 류는 바깥쪽 try-catch 로 전달받아 잡을 수 있다.
*기본 GlobalScope 을 포함하여 defaultHandler 가 명시적으로 지정되지 않은 모든 scope 은 CancellationException 이외의 Exception 에 대해 crash 를 낸다. 다른 말로 propagate 된 exception 을 scope level 에서 어쨌든 handling 해야 crash 가 나지 않는다는 말이다.
* GlobalScope 은 Job 이 없는 녀석으로, 한 launch 에 의해 exception 이 발생하더라도 다른 sibling job 들에게 영향을 미치지 않는다. 영향을 미치게 하려면 Job 을 share 하면 된다.
직접 생성한 Scope 의 경우는 Job 이 없으면 새로 생성하는 로직이 있으므로, 한 launch 에 의한 exception 이 propagation 된다.
다시 말해 GlobalScope 는 Job 이 없고, 일반 Scope 는 Job 이 있다는 차이 이외에는 큰 차이가 없다.
* exceptionHandler 의 경우 scope 의 1 depth handler 만 호출되고 그 이하 depth handler 들은 무시된다.
* CancellationException 은 handler 의 유무에 상관없이 propagation 되지 않는다. 단 try-catch 로 잡을 수는 있다.
댓글