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

[coroutine] exception 을 이해해보자 #2 ( in global scope )

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


auto propagation, cancellationexception, coroutine exception, defaulthandler, depth 1 excpetion, globalscope, globalscope coroutine exception, globalscope exception propagation, globalscope job, globalscope vs coroutinescope, Job, launch, main thread, new job, try-catch, user handling propagation, [coroutine] exception 을 이해해보자 #2 ( in global scope )


-

[coroutine] exception 을 이해해보자 #1 ( in my coroutine scope ) 과 동일한 sample 들을 직접 생성한 coroutine 이 아닌 GlobalScope 에서 수행해보기로 했다.

혹시나 뭔가 동작이 다를까 해서..

내가 알고 있기로는 GlobalScope 은 defaultHandler 가 setting 되어 결국 모든 exception 을 먹어 crash 가 나지 않는다는 차이만 있는것으로 알고 있는데.. 검증해보자 ( 결론은 잘못 알고 있었다. )



-

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 를 달아보았다.

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

결과는 이해한 바와 같이 1depth launch 의 exception handler 에 걸려서 crash 가 나지 않는다.



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

마찬가지로 exceptionHandler1 의 녀석이 불린다.
E/cklee: MMM BUILDER_1111 exceptionHandler caught java.lang.RuntimeException: gamza!

그리고 모든 launch 를 GlobalScope.launch 로 바꾸면 가장 inner 에 있는 launch 가 exception 을 소화해버린다.
E/cklee: MMM BUILDER_3333 exceptionHandler caught java.lang.RuntimeException: gamza!



-

global scope 라고 뭐가 다르진 않다.

다만, global scope 의 preset default handler 가 특정 로그를 찍어준다는 사실만이 다르다고 해석될 수 있다.



-
그러나.. 이게 끝이 아니었다.
GlobalScope 의 critical 한 다름이 하나 있었으니..

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

E/cklee: MMM BUILDER_3333 exceptionHandler caught java.lang.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 로 잡을 수는 있다.



-
최근 비슷한 실험을 한 프로그래머의 블로그를 찾았다.
launch 외 async, coroutineScope 등의 builder 에 대해서도 실험했으므로 읽어볼만하다.



반응형

댓글