[Kotlin] coroutines의 SupervisorJob의 역할은 무엇입니까?

시작하기



여러분 coroutines 를 사용하고 있습니까? coroutines를 사용하고 있다면 SupervisorJob을 사용하는 것이 있습니까? 그 때 우선 SupervisorJob 을 붙여 버렸다는 경험은 없을까요, 나는 말해지는대로 붙여 버린 적이 있습니다. SupervisorJob을 어쩔 수 없이 계속 사용하는 것은 위험하다고 생각해 왔으므로, 이번은 SupervisorJob에 대해 조사해 어떤 역할이 있는지 정리하고 싶습니다.

SupervisorJob이란?



SupervisorJob - kotlinx-coroutines-core 를 확인한 결과 SupervisorJob 에는 다음과 같은 역할이 있다고 합니다.
- SupervisorJob の子 Job は独立して失敗するようになる。
- つまり子 Job でエラーが発生してた場合に親 Job である SupervisorJob がキャンセルされない動作になる。

SupervisorJob 을 사용하지 않는 경우는 통상의 Job 로 처리가 실행됩니다. 그래서 Job A 하지만 에러가 발생했을 경우는 Parent Job 에 에러가 전파되어 최종적으로는 Job B 도 캔슬되는 동작이 됩니다.



SupervisorJob을 사용하면 자식 Job에서 발생한 오류는 부모 Job으로 전파되지 않는 동작입니다.



SupervisorJob의 동작 확인



SupervisorJob의 역할을 알면 동작을 확인합니다.
우선, 보통의 Job 를 사용했을 경우에 대해서 동작 확인해 보겠습니다.
/**
 * 通常の Job を持った CoroutineScope で起動した場合
 */
private fun launchOnDefaultJobScope() = runBlocking {
    // Job を指定していない場合は CoroutineScope 初期化時に Job を設定してくれるようになっている。
    // なので特に Job を指定しない場合には通常の Job で動作するようになる。
    val scope = CoroutineScope(Dispatchers.Default)

    scope.launch {
        throw Exception("GOOD ERROR MESSAGE!!")
    }

    scope.launch {
        println("GOOD PRINT")
    }

    delay(1000)
}

결과는 다음과 같이 특정 자식 Job에서 오류가 발생하면 다른 Job도 취소됩니다.
Exception in thread "DefaultDispatcher-worker-1" java.lang.Exception: GOOD ERROR MESSAGE!!
    at SampleKt$launchOnDefaultJobScope$1$1.invokeSuspend(Sample.kt:20)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)

Process finished with exit code 0

다음에 SupervisorJob 를 사용했을 경우에 대해서 동작 확인해 봅시다.
/**
 * SupervisorJob を持った CoroutineScope で起動した場合
 */
private fun launchOnSupervisorJobScope() = runBlocking {
    // SupervisorJob を設定した場合は子の Job のエラーが他の Job に伝搬しないようになる
    val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

    scope.launch {
        throw Exception("GOOD ERROR MESSAGE!!")
    }

    scope.launch {
        println("GOOD PRINT")
    }

    delay(1000)
}

결과는 이하와 같이 SupervisorJob 를 사용했을 경우라고 아이 Job 로 에러가 발생해도,
다른 작업은 취소되지 않고 실행할 수 있습니다.
GOOD PRINT
Exception in thread "DefaultDispatcher-worker-1" java.lang.Exception: GOOD ERROR MESSAGE!!
    at SampleKt$launchOnSupervisorJobScope$1$1.invokeSuspend(Sample.kt:67)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)

왜 SupervisorJob이 필요합니까?



SupervisorJob 을 이용하면 에러가 전파되지 않게 되므로, 그 자체에 메리트가 있습니다만 특히 Async 로 Deferred (Job)를 작성할 때에 유효합니다. 다음과 같이 Async에서 Deffered (Job)를 만들고 await에서 완료 될 때까지 기다리는 프로세스를 작성했다고 가정합니다. 만약 이 처리로 예외가 발생하면 try-catch 로 처리하게 된다고 생각합니다.

try-catch에서 예외를 처리하고 있기 때문에 문제없이 작동하는 것처럼 보입니다.
SupervisorJob을 사용하지 않으면 try-catch해도 다른 자식 Job이 취소됩니다.
/**
 * 通常の Job を持った CoroutineScope で async を使って起動した場合
 */
private fun launchAsyncOnDefaultJobScope() = runBlocking {
    // Job を指定していない場合は CoroutineScope 初期化時に JOB を設定してくれるようになっている。
    // 通常の JOB ではある子が失敗したら他の子に失敗が連鎖し動作がとまってしまうらしい
    val scope = CoroutineScope(Dispatchers.Default)

    // async を使って await したときに例外した場合には以下のように try catch で wrapping すると例外が拾える
    // これで問題ないように見えるが async は1つの Job になるので、ここで発生した例外はすべての 子 JOB に影響してしまう。
    scope.launch {
        try {
            async {
                delay(100)
                throw Exception("DEFFERED ERROR MESSAGE!!")
            }.await()
        } catch (e: Exception) {
            println(e.message)
        }
    }

    scope.launch {
        delay(200)
        println("GOOD PRINT")
    }

    delay(1000)
}
DEFFERED ERROR MESSAGE!!
Exception in thread "DefaultDispatcher-worker-2" java.lang.Exception: DEFFERED ERROR MESSAGE!!
    at SampleKt$launchAsyncOnDefaultJobScope$1$1$1.invokeSuspend(Sample.kt:44)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)

SupervisorJob을 사용하면 자식 Job의 실패는 다른 자식 Job에 전파되지 않습니다.
그래서 Async 로 Deffered (Job) 를 작성해, await 로 완료까지 기다리는 처리를 기술하는 경우에서도 안전하게 처리할 수 있습니다.
/**
 * async で発生した例外を他の Job に伝搬させないために async 内で例外を catch する
 */
private fun launchSafeAsync() = runBlocking {
    val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

    scope.launch {
        async {
            try {
                delay(100)
                throw Exception("DEFFERED ERROR MESSAGE!!")
            } catch (e: Exception) {
                println(e.message)
            }
        }.await()
    }

    scope.launch {
        delay(200)
        println("GOOD PRINT")
    }

    delay(1000)
}
DEFFERED ERROR MESSAGE!!
Exception in thread "DefaultDispatcher-worker-3" java.lang.Exception: DEFFERED ERROR MESSAGE!!
    at SampleKt$launchAsyncOnSupervisorScope$1$1$1.invokeSuspend(Sample.kt:90)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
GOOD PRINT

그 밖에도 SupervisorJob 가 약 서 있는 것은 있습니다만, 주로 이러한 장면에서 에러를 전파시키고 싶지 않은 경우에 사용할 수 있는 것 같습니다.

결론



이번에는 SupervisorJob의 역할을 조사하고 정리해 보았습니다. SupervisorJob은 한 아이 Job의 에러를 다른 아이 Job에 전파시키지 않는 특징이 있어 편리하네요. 하지만 SupervisorJob 그러니까 예외 처리를 하지 않아도 된다고는 할 수 없다고 생각하기 때문에 거기는 주의해 사용할 필요가 있군요.

참고문헌

좋은 웹페이지 즐겨찾기