SupervisorJob()

정승훈·2024년 8월 20일
1

Coroutine

목록 보기
1/2
post-thumbnail

Coroutine 책을 읽던 중 SupervisorJob()에 대해 알게 되어 정리하게 되었다. 공식문서만 잘 읽고 넘어가려 했으나

이것만 읽고는 이해가 잘 되지 않았다.. 그래서 추가로 알아본 내용을 정리하고자 한다.
참고로 나는 이 책을 읽고 노션에 정리하고 있으니 많이 봐주길 바란다.

결론부터 말하면

SupervisorJob()은 자식 코루틴의 예외로부터 부모 코루틴을 보호해준다.

SupervisorJob()

  • CoroutineContext의 일부이다.
  • 자식 코루틴에서 발생한 예외가 부모 코루틴까지 전파되지 않게 해준다.

이는 다음과 같이 활용할 수 있다.

suspend fun getName(): String {
    delay(500)
    return "Seunghoon"
}

suspend fun getAge(): Int {
    delay(300)
    throw Error("what?")
    return 19
}

suspend fun getJob(): String {
    delay(100)
    return "Android Developer"
}	

이렇게 3개의 함수가 있다고 하자.
일반적으로 실행했을 때 순서는 getJob() -> getAge() -> getName()이 될 것이다.
하지만 getAge()는 예외를 던지고 있다.

fun main(): Unit = runBlocking {
    val scope = CoroutineScope(Dispatchers.IO)

    scope.launch {
        scope.launch {
            println(getName())
        }
        scope.launch {
            println(getAge())
        }
        scope.launch {
            println(getJob())
        }
    }

    delay(1000)
}

위 방식처럼 각 함수들을 호출했을 때 실행결과가 어떻게 될까?

-> 더 내리기 전에 스스로 한 번 생각해보길 바란다.

첫 번째 함수인 getJob()만 성공적으로 실행되고 에러가 발생한다.
그리고 3번째 함수는 정상적으로 실행되지 않는다.

Structured Concurrency

코루틴의 구조화된 동시성이라는 특징 덕분에 이같은 상황이 발생한 것이다.
여기서 길게 다루기엔 글이 길어질 것 같아서 짧게 설명하고 넘어가도록 하겠다.

  • 자식 코루틴은 부모 코루틴의 컨텍스트를 물려받는다.
  • 자식 코루틴에서 예외가 발생하면 부모 코루틴까지 전파된다.
  • 부모 코루틴이 예외로 취소된다.
  • 자식 코루틴도 모두 취소된다.

다시 돌아와서

하나의 코루틴이 예외로 취소되어도 다른 코루틴은 정상적으로 동작하게 하고 싶은데..

-> 이때 우리는 위에서 언급한 SupervisorJob()을 사용할 수 있다.

fun main(): Unit = runBlocking {
    val scope = CoroutineScope(SupervisorJob())

    scope.launch {
        scope.launch {
            println(getName())
        }
        scope.launch {
            println(getAge())
        }
        scope.launch {
            println(getJob())
        }
    }

    delay(1000)
}

맙소사! scope의 컨텍스트만 바꿔줬는데 getName() 함수가 실행되었다!
이 말은, SupervisorJob()은 예외가 위로 (부모로) 전파되는것을 방지해준다는 것이다!

getAge()를 호출한 코루틴에서 예외가 발생했지만 이 예외가 가장 바깥 코루틴까지 영향을 미치지 않았다. 따라서 자식 코루틴들도 종료되지 않았고, 0.5초후에 getName()이 정상적으로 실행될 수 있었다.

더 잘 사용하기

우리는 위에서 이런 형식으로 스코프에 SupervisorJob()을 전달하여 사용하였다.

val scope = CoroutineScope(SupervisorJob())

이것도 좋지만 매번 SupervisorJob()을 생성하기는 귀찮다.
이럴땐 supervisorScope{}를 써보자.

private suspend fun main() = supervisorScope {
    launch {
        println(getName())
    }
    launch {
        println(getAge())
    }
    launch {
        println(getJob())
    }
}

코드가 훨씬 간결해졌다!

  • 모든 코루틴에서 scope를 직접 참조 ❌
  • 별도의 SupervisorJob()을 생성 ❌
  • scope를 별도의 변수에 저장 ❌

그러면 supervisorScope만 쓰면 되는거 아닌가?

평소에는 생각해본적이 없는데 글을 쓰다가 갑자기 궁금해졌다.
예외로부터 부모 코루틴을 보호할 수 있는데 SupervisorJob()으로 도배해서 쓰면 좋은거 아닌가? 사람들이 이렇게 쓰지 않는데는 이유가 있다.

SupervisorJob()은 runCatching{}에서 예외를 무시한다.

fun main(): Unit = runBlocking {
    kotlin.runCatching {
        main1()
    }.onSuccess {
        println("Success")
    }.onFailure {
        println(it)
    }
    delay(1000)
}

private suspend fun main1() = supervisorScope {
    launch {
        println(getName())
    }
    launch {
        println(getAge())
    }
    launch {
        println(getJob())
    }
}

위에서 사용한 함수를 runCatching으로 한 번 감싸보았는데, 예외가 콘솔창에 찍히기만 하고 onSuccess로 잡혔다.

SupervisorJob을 사용한 코루틴은 상위 코루틴에서 예외를 잡을 수 없다.

결론

자식 코루틴을 부모 코루틴으로부터 독립시키고 싶다면 SupervisorJob()을 사용하자

단, SupervisorJob을 사용하면 상위 코루틴에서 예외를 잡을 수가 없다.
따라서 예외 발생 시 수행해야 할 작업이 있다면 그냥 coroutineScope + runCatching을 사용하는것이 좋을 것 같다.

ps.피드백은 언제나 환영이다.

참고자료

0개의 댓글