본문 바로가기

코틀린

CoroutineScope 의 cancel() 전파

반응형

Scope의 자식 Scope 에서 Exception 이 났을 때 동작

아무런 처리도 하지않았을 때

CoroutineScope(Dispatchers.IO).launch {
            launch(CoroutineName("child1")){
                delay(1000)
                Log.d("dayun","${this} end")
            }.invokeOnCompletion {
                Log.d("dayun","child1 throwable = $it")
            }
            launch(CoroutineName("child2")){
                throw Exception("test")
            }.invokeOnCompletion { throwable ->
                Log.d("dayun","child2 throwable = $throwable")
            }
            delay(1000)
            Log.d("dayun","${this} end")
        }.invokeOnCompletion { throwable ->
            Log.d("dayun","parent throwable = $throwable")
        }

결과

child2 throwable = java.lang.Exception: test
child1 throwable = kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=StandaloneCoroutine{Cancelling}@1c5e461

우선 child 2에서 에러를 Throw를 했을 때 ParnetScope 가 Cancelling 되면서 Child 2도 같이 Cancel이 됩니다.

Parent의 invokeOnCompletion은 호출이 안되는데.. 왜 그런지 아시는 분은 댓글 좀

 

SupervisorJob을 이용했을 때

일단 SupervisorJob의 경우 자식 Scope의 예외가 Parent 로 전파되지 않게 하는 방법으로 알고있죠.

        val supervisorJob = SupervisorJob()

	CoroutineScope(Dispatchers.IO).launch {
            launch(supervisorJob){
                delay(1000)
                Log.d("dayun","child1 end")
            }.invokeOnCompletion {
                Log.d("dayun","child1 throwable = $it")
            }
            launch(supervisorJob){
                throw Exception("test")
            }.invokeOnCompletion { throwable ->
                Log.d("dayun","child2 throwable = $throwable")
            }
            delay(1000)
            Log.d("dayun","parent end")
        }.invokeOnCompletion { throwable ->
            Log.d("dayun","parent throwable = $throwable")
        }

 

결과

parent end
parent throwable = null
child1 end
child1 throwable = null

SupervisorJob 을 이용하면 child2 에서 난 예외가 parent 로 전파되지 않기 때문에 parent 도 성공적으로 end 되고 child1 도 성공적으로 end 됩니다.

 

그냥 Job 을 이용했을 때

	val job = Job()

        CoroutineScope(Dispatchers.IO).launch {
            launch(job){
                delay(1000)
                Log.d("dayun","child1 end")
            }.invokeOnCompletion {
                Log.d("dayun","child1 throwable = $it")
            }
            launch(job){
                throw Exception("test")
            }.invokeOnCompletion { throwable ->
                Log.d("dayun","child2 throwable = $throwable")
            }
            delay(1000)
            Log.d("dayun","parent end")
        }.invokeOnCompletion { throwable ->
            Log.d("dayun","parent throwable = $throwable")
        }

이렇게 되면 parent 의 coroutineContext 에 + job 이 된것이 child1 과 child2 의 parentScopeContext 가 되면서 외곽에 있는 CoroutineScope의 겨우 내부 Coroutine 의 예외를 전달받지 않습니다.

그러나, 같은 parent를 둔 child1 과 child2 의 경우 예외가 전달되어 cancel 이 됩니다.

결과

child1 throwable = kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=JobImpl{Cancelling}@48cbc8
parent end
parent throwable = null

 

매번 newInstance 로 Job 을 만든다면?

CoroutineScope(Dispatchers.IO).launch {
            launch(Job()){
                delay(1000)
                Log.d("dayun","child1 end")
            }.invokeOnCompletion {
                Log.d("dayun","child1 throwable = $it")
            }
            launch(Job()){
                throw Exception("test")
            }.invokeOnCompletion { throwable ->
                Log.d("dayun","child2 throwable = $throwable")
            }
            delay(1000)
            Log.d("dayun","parent end")
        }.invokeOnCompletion { throwable ->
            Log.d("dayun","parent throwable = $throwable")
        }

이렇게 되면 3개의 쓰레드가 각각이 별도의 coroutineContext 를 launch 하는 형태라 서로 예외를 전파시키지 않습니다.

이 코드에서는 결과적으로 supervisorJob 과 같은 결과를 볼 수 있습니다.

이 방법과 supervisorJob 의 경우 다른 Need 를 갖고 있다고 생각합니다.

new Job()으로 매번 context 를 넣어주는 행위는 새로운 문맥을 만드는 독자적인 행위로 보여집니다.

그러나 supervisorJob의 경우 개발자가 해당 코드를 보았을 때 에러전파가 상위로 전달되지 않게한다는 Need를 코드를 보고 이해할 수 있다고 생각합니다.

따라서 가독성을 높이기 위해서는 각 상황에 맞게 사용하는 것이 좋아보입니다.

Main Thread 에서 SupervisorJob 을 테스트할 때

	val supervisorJob = SupervisorJob()

        CoroutineScope(Dispatchers.Main).launch {
            Log.d("dayun","start")
            launch(supervisorJob){
                delay(1000)
                Log.d("dayun","child1 end")
            }.invokeOnCompletion {
                Log.d("dayun","child1 throwable = $it")
            }
            launch(supervisorJob){
                throw Exception("test")
            }.invokeOnCompletion { throwable ->
                Log.d("dayun","child2 throwable = $throwable")
            }
            delay(1000)
            Log.d("dayun","parent end")
        }.invokeOnCompletion { throwable ->
            Log.d("dayun","parent throwable = $throwable")
        }

MainThread의 경우 싱글 스레드라 그런지 SupervisorJob 이 예상처럼 동작하지 않고 에러가 전파 되는 것으로 확인하였습니다.

mainThread에서 예외전파를 하지않으려면 try-catch 밖에 없을까요? 많은 제보 부탁드립니다.

반응형