본문 바로가기
프로그래밍 놀이터/Kotlin, Coroutine

[coroutine] exception 을 이해해보자 #1 ( in my coroutine scope )

by 돼지왕 왕돼지 2019. 3. 5.
반응형


android coroutine, android coroutine exception, android dispatcher, android handler, android try catch, Async, cancallationexception, coroutine exception, coroutine exception catch, coroutine exception propagation, coroutine try catch, coroutinescope, coroutinescope define, defaultexceptionhandler, depth, dispatchers, exception, Exception Propagation, globalscope, globalscope coroutine scope, globalscope exception, jobcancellationexception, launch, Main, my coroutine scope, parent scope, suspend function, understanding coroutine exception, [coroutine] exception 을 이해해보자 #1 ( in my coroutine scope )


-

이 글은 coroutine exception 에 대한 일종의 심화과정이라고 볼 수 있다.

기본 exception 에 대한 내용을 먼저 이해한 후 이곳의 글을 보자!



-

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 가 난다.



-

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 를 장착해보았다.

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 의 exceptionHandler 가 exception 을 먹어버린다.
E/cklee: MMM BUILDER_1111 exceptionHandler caught java.lang.RuntimeException: gamza!


-

이번에는 launch 를 여러겹 싸보았다.

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 된다.



-
위의 규칙이 맞는지 검증해보자.

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!")
            }
        }
    }
}

결과는..

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!

우선 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 를 내지 않는다.) 라고 되어 있다.

실제 그럴까 테스트를 해보자.


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 했나 싶어할까봐 )



-

다음번에는 GlobalScope 에서의 exception 을 이해해보자 테스트로 돌아온다.

끝! 






반응형

댓글