본문 바로가기
개발/Android

Kotlin Coroutines, 에러 처리와 SupervisorJob

by du.it.ddu 2025. 1. 29.

오늘날 Kotlin 기반의 안드로이드 앱을 개발하고 있다면 Coroutines를 사용하고 있을 것입니다.
그렇다면 십중팔구 Jetpack ViewModel에서 제공하는 ViewModelScope를 사용하고 있을텐데요.

ViewModelScope를 생성하는 코드를 보면 SupervisorJob이 등장합니다.

package androidx.lifecycle.viewmodel.internal

internal fun createViewModelScope(): CloseableCoroutineScope {
    val dispatcher = try {
        Dispatchers.Main.immediate
    } catch (_: NotImplementedError) {
        EmptyCoroutineContext
    } catch (_: IllegalStateException) {
        EmptyCoroutineContext
    }
    return CloseableCoroutineScope(coroutineContext = dispatcher + SupervisorJob())
}

SupervisorJob은 무엇이고 왜 ViewModelScope에서 기본적인 Job으로 사용하고 있을까요?
코루틴의 에러처리 관점에서 알아봅시다.

 

코루틴에서 에러처리

지난 포스팅(https://doitddo.tistory.com/172)에서, 코루틴은 구조화된 동시성 원칙을 따르고 있기 때문에 자식 코루틴에서 에러가 발생하면 부모 코루틴까지 전파가 되어 전체 코루틴이 취소됨을 알 수 있었습니다.

그렇다면 우리는 자식 코루틴의 에러로 인해 전체 코루틴이 취소되지 않도록 에러 처리를 해 주어야 합니다.
가장 쉬운 방법은 try-catch를 사용하는 방법이 있습니다.

CoroutineScope(context).launch {
    launch {
        try {
            // do something
        } catch (e: Exception) {
            // Handle error
        }
        
        launch {
            try {
                // do something
            } catch(e: Exception) {
                // Handle error
            }
        }
    }
    
    launch {
        try {
            // do something
        } catch(e: Exception) {
            // Handle error
        }
    }
    
    // do something
}

그런데 이 코드는 모든 코루틴에서 try-catch가 반복됩니다. 그리고 코루틴이 취소되었을 때 발생하는 CancellationException 까지 잡아버리기 때문에 코루틴의 취소가 정상적으로 이루어지지 않게 됩니다. 따라서 CancellationException은 별도로 처리하는 코드가 반복이 되어 버립니다.

결과적으로 에러 처리는 되겠지만 반복 코드의 발생으로 인해 나이스한 방향이 아닐 것이라 생각할 것입니다.
자 그렇다면 CoroutineExceptionHandler를 사용하면 어떨까요?

val exceptionHandler = CoroutineExceptionHandler { context, throwable -> 
    ...
}
CoroutineScope(context + exceptionHandler).launch {
    launch {
        // do something
        
        launch {
            // do something
        }
    }
    
    launch {
        // do something
    }

    // do something
}

이 경우, 에러가 발생하면 CoroutineExceptionHandler가 에러를 캐치하지만 코루틴이 에러로 인해 취소되는 것을 막을수는 없습니다.
자식 코루틴에서 발생한 에러가 부모로 전파되는 것은 막지 못하기 때문입니다.

그렇다면 어떻게 에러처리를 하는 것이 좋을까요?

 

SupervisorJob

자식 코루틴의 에러로 인해 부모 코루틴이 취소되고 결과적으로 전체 코루틴이 취소되는 근본적인 이유는, 자식 코루틴의 에러가 부모에게 전파되기 때문입니다.
그렇다면 자식 코루틴의 에러를 부모에게 전파하지 않는다면 문제가 해결이 될 것으로 보입니다.

이런 관점에서 사용할 수 있는 것이 SupervisorJob입니다.
SupervisorJob은 자식 코루틴에서 에러가 발생해도 부모 코루틴으로 전파하지 않도록 막습니다.

package kotlinx.coroutines

public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent)

private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
    override fun childCancelled(cause: Throwable): Boolean = false
}

SupervisorJob을 생성하는 함수를 호출하면 내부적으로 SupervisorJobImpl을 반환합니다.
그리고 SupervisorJobImpl은 JobImpl의 childCancelled를 오버라이드하여 false를 반환하도록 합니다.

package kotlinx.coroutines

public open class JobSupport constructor(active: Boolean) : Job, ChildJob, ParentJob {

    ...
    
    private fun cancelParent(cause: Throwable): Boolean {
        ...
        return parent.childCancelled(cause) || isCancellation
    }
}

다시 코드를 타고 들어가다보면 JobSupport 클래스에 도달하게 되고, cancelParent 함수를 만날 수 있습니다.
이 함수의 마지막 줄에는 자신의 부모 Job의 childCancelled를 예외와 함께 호출합니다.
SupervisorJob은 childCancelled가 false를 반환하도록 오버라이드 되어있기 때문에, 취소되지 않은 코루틴이라면 함수의 결과가 항상 false입니다.

The method that is invoked when the job is cancelled to possibly propagate cancellation to the parent. Returns true if the parent is responsible for handling the exception, false otherwise. Invariant: never returns false for instances of CancellationException, otherwise such exception may leak to the CoroutineExceptionHandler.

cancelParent 함수는 위와 같은 주석이 작성되어 있습니다.
주석에서 알 수 있듯이 cancelParent 함수를 통해 코루틴에서 오류가 발생했을 때 코루틴을 취소하면서 부모에게 취소를 전파하기 위해 호출하는 함수입니다.
따라서 SupervisorJob은 이 함수의 false이므로 부모에게 취소가 전파되지 않음을 알 수 있습니다.

 

마치며

ViewModelScope는 부모-자식 코루틴 간의 에러 전파와 취소를 막기 위해 SupervisorJob을 사용하고 있음을 알 수 있었습니다.
만약 다른 코루틴 스코프를 생성해서 사용한다면, 코루틴의 에러 처리에 대한 내용을 이해하고 SupervisorJob을 활용하면 더욱 견고한 코루틴을 작성할 수 있을 것입니다.

반응형