brunch

You can make anything
by writing

C.S.Lewis

by 서준수 Nov 21. 2024

코루틴 SupervisorJob

코틀린 코루틴(12)

코루틴 SupervisorJob


SupervisorJob은 자식 코루틴에서 발생한 예외에 영향을 받지 않는다. 이 말을 이해하기 위해서 먼저 앞서 살펴봤던 예외 전파에 대해서 다시 살펴보자.


Job의 예외 전파

코루틴 예외 처리에서 예제 코드를 기반으로 구조화된 동시성으로 인해 발생하는 예외 전파에 대해 살펴봤다. 아래 예제 코드는 그때 봤던 코드에 catch 문 한 줄만 추가한 것이다.

실행 결과는 다음과 같다. catch 문을 추가한 이유는 실제로 부모로부터 취소 예외를 전달받은 것을 확인하기 위함이다.

Hello
Coroutine 1
Coroutine 1 was cancelled
Check: kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job="coroutine#2":StandaloneCoroutine{Cancelling}@3fd15b16
Coroutine 2 was cancelled
Caught in CoroutineExceptionHandler java.lang.NullPointerException
End

위 코드는 launch를 통해 코루틴을 생성했다. 이 경우는 코루틴을 생성할 때 Job 객체가 반환된다. 즉 일반적인 Job을 사용한 것이다. 다시 말하면 일반 Job을 사용하면 구조화된 동시성으로 인한 예외 전파가 발생한다고 할 수 있다. 이것을 도식화하면 다음과 같다.

일반 Job:
1) 자식 코루틴에서 예외가 발생하면 해당 예외가 부모 코루틴에게 전파됩니다.
2) 부모 코루틴은 예외를 감지하고 나머지 자식 코루틴들에게 예외를 전파하여 취소합니다.
3) 부모 코루틴도 취소됩니다.

자식 코루틴인 Coroutine 1에서 발생한 예외가 부모 코루틴에 전파된다. 그러면 부모 코루틴은 예외를 처리하고 자신도 종료(=취소)할 준비를 한다. 그 후 부모 코루틴은 다른 자식 코루틴인 Coroutine 2를 종료하기 위해 취소 예외를 전파한다. 그림에서 1번의 화살표 방향과 2번의 화살표 방향을 보면 된다. 이러한 관점으로 보면 예외 전파는 양방향 전파라고 할 수 있다.


즉 양방향 전파는 자식 코루틴이 실패하면 부모 코루틴도 취소되고, 부모 코루틴이 취소되면 자식 코루틴도 취소되는 경우를 의미한다. (단순히 양방향 전파라고 하면 마치 자식 코루틴에서 발생한 NullPointerException이 양방향으로 전파하는 것 같은 느낌을 준다. 그래서 양방향 전파라는 표현이 적절한지 의문이다.)


SupervisorJob의 예외 전파

SupervisorJob을 사용하면 자식 코루틴들은 독립적으로 예외를 처리한다. 즉, 하나의 자식 코루틴에서 발생한 예외가 다른 자식 코루틴으로 전파되지 않는다. 부모 코루틴인 SupervisorJob이 취소될 경우에만 모든 자식 코루틴이 취소된다.


실제로 그렇게 동작하는지 앞선 예제를 조금 수정하여 확인해 보자.

실행 결과는 다음과 같다.

Hello
Coroutine 1
Coroutine 1 was cancelled
Caught in CoroutineExceptionHandler java.lang.NullPointerException
Coroutine 2
Coroutine 2 has completed
Goodbye
End

결과를 보면 자식 코루틴인 Coroutine 1은 실행 후 예외가 발생하여 취소된 것을 알 수 있다. 예외가 전파되었다면 Coroutine 2는 실행되기 전에 취소가 될 것이다. 하지만 실제 결과는 Coroutine 2가 실행되었고 완료된 것까지 확인할 수 있다. 그리고 부모 코루틴도 취소되지 않아서 Goodbye라는 문구까지 출력되고 있다.


기존에 알고 있던 예외 전파의 규칙을 완전히 무시하고 있는 상황이다. 이렇게 하나의 자식 코루틴에서 발생한 예외로 모든 코루틴이 종료되지 않도록 할 수 있는 방법이 SupervisorJob을 사용하는 것이다.


이 상황을 도식화하면 다음과 같다.

SupervisorJob인 자식 코루틴에서 발생한 예외는 자식 코루틴 내에서 처리되고 다른 곳으로 전파되지 않는다. 따라서 부모 코루틴을 포함한 자식 코루틴은 종료되지 않는다. 예외가 다른 곳으로 전파되지 않고 자신에게 향하는 하나의 방향만 가지고 있기 때문에 단방향 전파라고 볼 수 있다. (하지만 단방향이란 표현 또한 적절한 표현인지는 개인적으로 의문이다. 굳이 그렇게 말할 필요가 있나 싶다.)

SupervisorJob:
1) 자식 코루틴에서 예외가 발생해도 해당 예외는 부모 코루틴이나 다른 자식 코루틴에게 전파되지 않는다.
2) 부모 코루틴과 다른 자식 코루틴은 계속 실행된다.

이러한 동작을 통해 SupervisorJob은 자식 코루틴의 실패가 전체 코루틴 구조에 영향을 미치지 않도록 한다. 그래서 SupervisorJob은 독립적으로 예외를 처리한다고 표현하는 것이 좀 더 명확하지 않나 싶다.


supervisorScope

앞선 예제에서는 SupervisorJob을 사용하여 코루틴 스코프(CoroutineScope)를 생성하였다. 이것보다 좀 더 편리하게 자식 코루틴을 모두 SupervisorJob으로 만들 수 있는 방법이 있다. 바로 supervisorScope를 사용하는 것이다. supervisorScope는 coroutineScope처럼 새로운 코루틴 스코프를 생성하는 스코핑 함수이다. 다만 supervisorScope 내에서 실행되는 자식 코루틴들은 SupervisorJob의 특성을 가진다. 따라서 자식 코루틴이 독립적으로 예외를 처리할 수 있다. supervisorScope는 자식 코루틴들을 포함하는 부모 코루틴의 역할을 한다.


앞선 예제를 다음과 같이 조금 수정해 보자.

실행 결과는 다음과 같다. CoroutineScope(SupervisorJob())을 사용했을 때와 결과가 똑같다.

Hello
Coroutine 1
Coroutine 1 was cancelled
Caught in CoroutineExceptionHandler java.lang.NullPointerException
Coroutine 2
Coroutine 2 has completed
Goodbye
End

GlobalScope.launch 대신 supervisorScope를 사용하였다. supervisorScope는 내부적으로 SupervisorJob을 포함하는 새로운 CoroutineScope를 생성한다. 그렇기 때문에 결국 기존에 사용했던 CoroutineScope(SupervisorJob())와 같은 의미다. 두 경우 모두 SupervisorJob을 사용하여 자식 코루틴이 독립적으로 예외를 처리한다.

자식 코루틴이 독립적으로 예외를 처리되는 것 역시 같기 때문에 예외 전파의 모습도 동일하다.



코루틴 예외 처리에서 CoroutineExceptionHandler가 자식 코루틴에서 사용되지 않는다고 했었다. 예외 전파를 통해 부모에서 처리하기 때문이다. 하지만 supervisorScope는 SupervisorJob을 사용하여 자식 코루틴이 독립적으로 예외 처리를 한다고 했다. 그러므로 자식 코루틴에 CoroutineExceptionHandler를 사용하면 자식 코루틴이 실패할 때 CoroutineExceptionHandler가 해당 예외를 처리할 수 있다. 따라서 supervisorScope 내에서는 자식 코루틴에 사용해도 의미가 있다.


요약하자.

- SupervisorJob은 자식 코루틴에서 발생한 예외에 영향을 받지 않는다.
- 양방향 예외 전파는 자식 코루틴이 실패하면 부모 코루틴도 취소되고, 부모 코루틴이 취소되면 자식 코루틴도 취소되는 경우를 의미한다.
- supervisorScope는 coroutineScope처럼 새로운 코루틴 스코프를 생성하는 스코핑 함수이다.
- supervisorScope 내에서 실행되는 자식 코루틴들은 SupervisorJob의 특성을 가진다.
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari