-
이 글은 coroutine exception 에 대한 일종의 심화과정이라고 볼 수 있다.
기본 exception 에 대한 내용을 먼저 이해한 후 이곳의 글을 보자!
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class MainActivity : Activity() { private val coroutineScope = CoroutineScope(Dispatchers.Main) override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById<Button>(R.id.btn).setOnClickListener { try { coroutineScope.launch { throw RuntimeException( "gamza!" ) } } catch (e: Exception) { Log.e( "cklee" , "MMM exception caught $e" ) } } } } |
결과는 어떨까?
launch 바깥쪽에서 try-catch 로 exception 을 잡지 못하고 app 은 crash 가 난다.
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class MainActivity : Activity() { private val defaultExceptionHandler = CoroutineExceptionHandler { context, throwable -> Log.e( "cklee" , "MMM exceptionHandler caught $throwable" ) } private val coroutineScope = CoroutineScope(Dispatchers.Main + defaultExceptionHandler) override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById<Button>(R.id.btn).setOnClickListener { coroutineScope.launch { throw RuntimeException( "gamza!" ) } } } } |
그렇다면 이번에는 defaultExceptionHandler 를 scope 에 장착해보았다.
그랬더니 다음과 같은 결과와 함께 crash 를 면할 수 있다.
E/cklee: MMM exceptionHandler caught java.lang.RuntimeException: gamza!
-
이번에는 launch 에 handler 를 장착해보았다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | class MainActivity : Activity() { private val defaultExceptionHandler = CoroutineExceptionHandler { context, throwable -> Log.e( "cklee" , "MMM exceptionHandler caught $throwable" ) } private val builderExceptionHandler1 = CoroutineExceptionHandler { context, throwable -> Log.e( "cklee" , "MMM BUILDER_1111 exceptionHandler caught $throwable" ) } val coroutineScope = CoroutineScope(Dispatchers.Main + defaultExceptionHandler) override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById<Button>(R.id.btn).setOnClickListener { coroutineScope.launch(builderExceptionHandler1) { throw RuntimeException( "gamza!" ) } } } } |
-
이번에는 launch 를 여러겹 싸보았다.
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 | class MainActivity : Activity() { private val defaultExceptionHandler = CoroutineExceptionHandler { context, throwable -> Log.e( "cklee" , "MMM exceptionHandler caught $throwable" ) } 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" ) } val coroutineScope = CoroutineScope(Dispatchers.Main + defaultExceptionHandler) override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById<Button>(R.id.btn).setOnClickListener { coroutineScope.launch(builderExceptionHandler1) { launch(builderExceptionHandler2) { launch(builderExceptionHandler3) { throw RuntimeException( "gamza!" ) } } } } } } |
아래 결과와 같이 가장 outer launch 의 exceptionHandler 만 동작한다.
E/cklee: MMM BUILDER_1111 exceptionHandler caught java.lang.RuntimeException: gamza!
그러나 모든 launch 를 coroutineScope.launch 로 변경하면, 가장 inner 에 있는 녀석의 handler 가 불린다.
E/cklee: MMM BUILDER_3333 exceptionHandler caught java.lang.RuntimeException: gamza!
-
어떻게 이해해야 할까?
android 개발자로서 쉽게 생각해보면, launch 를 Runnable 을 만들어 적정 handler(Dispatcher)에게 던지는 것과 같은 것으로 생각할 수 있다.
그래서 첫번째 테스트의 경우 handler.post{ } 에 대해 try-catch 는 post 자체에 대한 것이지 그 안에 대한 내용이 아닌 것이다.
coroutine 에서 발생한 exception 은 coroutine scope 으로 propagate 되고, 해당 exception 이 catch 되지 않으면 crash 가 나는 것이고 catch 되면 crash 를 피할 수 있는 것이다. 이 때 scope 단위의 exception catch 는 outer try-catch 가 아닌 exceptionHandler 를 통한다.
단 propagate 되어 최초로 exception 이 잡히고 consume 하는 것은 CoroutineScope 기준의 첫번째 depth 라고 보면 되겠다. 여기서 consume 하지 못하면 parent scope level 로 propagate 된다.
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 | class MainActivity : Activity() { private val defaultExceptionHandler = CoroutineExceptionHandler { context, throwable -> Log.e( "cklee" , "MMM exceptionHandler caught $throwable" ) } 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" ) } val coroutineScope = CoroutineScope(Dispatchers.Main + defaultExceptionHandler) override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById<Button>(R.id.btn).setOnClickListener { coroutineScope.launch(builderExceptionHandler1) { try { delay(5000L) } catch (e:Exception){ Log.e(“cklee”, “MMM first launch cancelled $e”) } } coroutineScope.launch(builderExceptionHandler2) { delay(3000L) } coroutineScope.launch(builderExceptionHandler3) { throw RuntimeException( "gamza!" ) } } } } |
우선 1 depth handler 가 모두 불리는 것은 확인하였다.
그런데.. builder3 이 호출되었는데도, depth 1 에 해당하는 coroutine 들의 exceptionHandler 가 모두 불린다.
이는 coroutineScope 의 Job 을 공유하기 때문이다. 다시 말하면 child job 이 RuntimeException 으로 cancel 된 것이 parent job 으로 propagate 되고 그것이 나머지 child job 으로 다시 propagate 된 것이다.
실제 CoroutineScope 생성자를 보면 Job 이 없으면 Job 을 새로 만들고, launch 등으로 job 이 수행될 때마다 parent job 에 child job 을 붙인다.
여기서 한가지 더 주목해야 할 것은..
잘 보면, 실제 suspend function 이 중지되면서는 JobCancellationException 이 발생하지만, exceptionHandler 에 전달된 것은 모두 RuntimeException 임을 유의해서 봐야 한다.
그리고 catch 부에는 JobCanellationException 이 전달되었다.
-
[coroutine] Exception handling 을 보면 CancellationException 은 모든 handler 에 의해 무시된다(crash 를 내지 않는다.) 라고 되어 있다.
실제 그럴까 테스트를 해보자.
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 | class MainActivity : Activity() { private val defaultExceptionHandler = CoroutineExceptionHandler { context, throwable -> Log.e( "cklee" , "MMM exceptionHandler caught $throwable" ) } 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" ) } val coroutineScope = CoroutineScope(Dispatchers.Main + defaultExceptionHandler) override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById<Button>(R.id.btn).setOnClickListener { coroutineScope.launch(builderExceptionHandler1) { delay(5000L) } coroutineScope.launch(builderExceptionHandler2) { delay(3000L) Log.e( "cklee" , "MMM delay not cancelled!!" ) } coroutineScope.launch(builderExceptionHandler3) { throw CancellationException( "gamza!" ) } } } } |
E/cklee: MMM delay not cancelled!!
결국 launch 자체에서 CancellationException 을 먹어버린 것이라 다른 coroutine 으로 전파도 되지 않아 handler2 를 달은 launch 도 정상적으로 delay 를 마치고 로그를 찍었다.
CancellationException 을 던지는 마지막 launch 에 handler 를 제거해도 매한가지다. ( 혹시나 그 녀석이 consume 했나 싶어할까봐 )
댓글