brunch

You can make anything
by writing

C.S.Lewis

바카라 꽁 머니 예외 처리 (Exception Handling)

코틀린 바카라 꽁 머니(11)

바카라 꽁 머니 예외 처리 (Exception Handling)


예외에는 다양한 종류가 있지만 앞서 바카라 꽁 머니에서 발생하는 독특한 예외를 본 적이 있다. 바로 바카라 꽁 머니이 정상적으로 취소될 때 발생하는 CancellationException이다. 바카라 꽁 머니 취소에 관해서는바카라 꽁 머니 Job에서 처음 살펴보았다.


그때의 코드를 다시 살펴보자.

실행 결과는 Hello, 이다. Kotlin!이 출력되지 않는 것은 바카라 꽁 머니의구조화된 동시성에 의해서 부모 바카라 꽁 머니이 취소되면 자식 바카라 꽁 머니도 취소되기 때문이다. 여기서 취소로 인해 CancellationException이 발생하는 것은 앞서바카라 꽁 머니 취소에서 확인해 봤기 때문에 알고 있을 것이다. 하지만 별도의 예외 처리를 하지 않았는데 실제로 콘솔에서 에러 문구를 확인할 수 없다. 그 이유는 취소 시 발생하는 CancellationException은 정상으로 간주하고 무시하기 때문이다.


만약 다른 예외를 발생시키면 어떻게 되는가?

다음과 같이 처리되지 않은 예외가 콘솔에 로깅되고 프로그램이 종료된다.

바카라 꽁 머니

별도의 예외 처리를 하지 않았기 때문에 당연한 결과로 볼 수 있다. 그렇다면 다음 코드의 실행 결과는 어떻게 될까?

여전히 별도의 예외 처리를 하지 않았다. 달라진 것은 GlobalScope를 사용했다는 것이다. GlobalScope는 특정 컴포넌트의 생명주기에 묶이지 않는 최상위 바카라 꽁 머니을 시작하는 데 사용되는 전역 CoroutineScope이다. 실행 결과는 Hello, 이다. 이때 예외로 인한 에러가 발생하지 않은 이유는 무엇일까?


GlobalScope에서 시작한 바카라 꽁 머니은 runBlocking의 생명주기와 별도로 동작한다.따라서 이 경우에는 GlobalScope에서 시작된 바카라 꽁 머니이 완료되기 전에 runBlocking 블록이 종료된다. runBlocking이 종료된다는 것은 메인 스레드가 종료된다는 것이고 일반적으로 애플리케이션이 종료된다는 것과 같은 뜻이다. 따라서 GlobalScope에서 시작한 바카라 꽁 머니 내부의 출력문이 실행되기 전에 프로그램이 종료되어 애초에 NullPointerException 발생이 되지 않은 것이다.


이 경우를 주목할 것은 아니었으나 GlobalScope에 대한 이해가 필요하여 조금 상세히 살펴보았다. 그러면 GlobalScope의 바카라 꽁 머니이 모두 실행되도록 기다리면 어떻게 될까? NullPointerException이 발생해야 한다.

실행 결과는 다음과 같다.

바카라 꽁 머니

GlobalScope의 바카라 꽁 머니이 모두 실행되기 때문에 World!가 출력된 후 Kotlin!도 출력이 되었다. 그 후

NullPointerException이 발생했다. (다만 예외가 서로 다른 스레드에서 발생하고 있다는 차이점이 있다. 이것은바카라 꽁 머니디스패처를 알고 있다면 이해할 수 있다.)


주목할 것은 예외 발생 후에 Hello, 가 출력된 것이다. 이것은 예외 발생 후에도 프로그램이 종료되지 않았다는 뜻이다. 별도의 예외 처리를 하지 않았는데 어떻게 이런 결과가 발생했을까?


그것은 GlobalScope를 사용했기 때문이다. GlobalScope에서 시작된 바카라 꽁 머니은 최상위 바카라 꽁 머니이다. 따라서 부모 바카라 꽁 머니이 없다. 이런 경우에 해당 바카라 꽁 머니에서별도의 예외 처리를 하지 않는 상황에서 예외가 발생하면 기본 예외 처리기가 이를 처리한다.따라서 (아직 미지의 존재인) 기본 예외 처리기가 예외 처리를 하였기 때문에 프로그램이 무사히 완료되는 것이다.


바카라 꽁 머니 예외 핸들러 (CoroutineExceptionHandler)

별도의 예외 처리를 하지 않는 상황에서 발생한 예외를 처리하는 기본 예외 처리기는

CoroutineExceptionHandler이다. (최종적으로 처리하는 부분이라기보다는 최초로 처리를 담당하는 부분이라고 생각하면 된다. 이것이 무슨 의미인지는 추후 CoroutineExceptionHandler의 동작 방식을 살펴보면 이해할 수 있다.) 일단 CoroutineExceptionHandler가 무엇인지 예제를 통해서 살펴보자.


먼저 예외 처리를 위해 사용하는 대표적인 구문 try-catch를 사용한 예제를 살펴보자.

NullPointerException 발생 시 catch 문에서 해당 예외 정보를 출력하고 있다. 이것을CoroutineExceptionHandler을 통해서 처리하도록 변경해 보면 다음과 같다.

try-catch 문 대신에 CoroutineExceptionHandler를 생성하여 GlobalScope의 바카라 꽁 머니의 컨텍스트로 넘겨주고 있다. 그렇다는 것은 CoroutineExceptionHandler가 CoroutineContext 혹은 하위 클래스의 타입을 가지는 객체라는 것을 추측할 수 있다. 실제로 CoroutineExceptionHandler는 CoroutineContext의 Element이다. (CoroutineContext의 Element인 것은 바카라 꽁 머니 디스패처와 같다. 관련된 내용은바카라 꽁 머니 디스패처에 좀 더 자세히 설명되어 있다.)


위 두 예제의 실행 결과는 모두 다음과 같다.

Caught java.lang.NullPointerException: My NullPointerException

그러면 try-catch를 쓰면 되는데 왜 CoroutineExceptionHandler가 존재하는 것일까? try-catch 문은 특정 코드 블록 내에서 예외를 처리하는 데 사용되지만CoroutineExceptionHandler는 바카라 꽁 머니의 전역 예외 처리를 위해 존재한다. 지금처럼바카라 꽁 머니의 컨텍스트로 넘겨주면 해당 바카라 꽁 머니 내에서 발생하는 모든 예외를 처리할 수 있다.이로 인해 구조화된 동시성으로 인해 발생하는 예외 전파를 효율적으로 처리할 수 있다. (예외 전파라는 표현은 처음 등장하는데 앞서 언급했던 부모 바카라 꽁 머니이 취소되면 자식 바카라 꽁 머니이 취소되는 것이 하나의 예시다. 이것은 추후 좀 더 살펴본다.)


try-catch 문은 예외를 처리하고 예외 발생 후 바카라 꽁 머니이 계속 실행된다. 반면 CoroutineExceptionHandler는 예외가 발생 후 바카라 꽁 머니을 계속 실행하지 않는다.CoroutineExceptionHandler는 잡히지 않은 예외를 처리하는 최후의 수단일 뿐이다. 예외를 로깅하거나 사용자에게 알리는 수단인 것이다.


CoroutineExceptionHandler의 동작 방식

별도의 예외 처리를 하지 않는 상황에서 발생한 예외를 처리하는 기본 예외 처리기는

CoroutineExceptionHandler라고 했다.ExceptionHandling_ex3에서 바카라 꽁 머니의 컨텍스트로 별도의 CoroutineExceptionHandler를 넘겨주지 않았다. try-catch 문도 없다. 즉 아무런 예외 처리를 하지 않은 것이다. 그럼에도 실행 결과를 보면 예외 처리가 이뤄졌다. 그것은 GlobalScope.launch처럼 launch 빌더로 생성된 최상위 바카라 꽁 머니에서 발생한 예외는 CoroutineExceptionHandler를 통해 처리되기 때문이라고 했다. 하지만 아직 의아하다. 원점으로 돌아온 것 같다. CoroutineExceptionHandler를 넘겨주지 않았는데 어떻게 CoroutineExceptionHandler를 통해서 예외 처리를 한다는 것인가?

바카라 꽁 머니CoroutineExceptionHandler.kt

CoroutineExceptionHandler의 동작 원리를 알아보기 위해서 CoroutineExceptionHandler.kt 파일을 살펴보자. 해당 파일에는 handleCoroutineException 메서드가 존재한다. 해당 메서드는 예외 처리 중에 또 다른 예외가 발생하거나 CoroutineExceptionHandler가 컨텍스트에 존재하지 않을 때 JVM 환경에서는 ServiceLoader를 통해 발견된 모든 CoroutineExceptionHandler 인스턴스와 Thread.uncaughtExceptionHandler가 호출된다.



ServiceLoader를 통해 찾는

CoroutineExceptionHandler 인스턴스의 범위는 JVM 클래스패스(Classpath) 내 접근 가능한 모든 서비스 제공자(Service Provider)에 해당한다. 클래스패스는 자바 프로그램이 실행될 때 필요한 클래스 파일(.class)과 라이브러리(jar 파일 등)의 위치를 지정하는 경로다. 자바 프로그램이 실행될 때 클래스 로더(ClassLoader)가 클래스패스에 지정된 경로에서 클래스와 리소스를 찾는다.


일반적으로는ExceptionHandling_ex3처럼 CoroutineExceptionHandler가 별도로 등록되지 않았다면 기본적으로 등록된 핸들러가 없을 수 있다. 따라서 GlobalScope에서 발생한 예외는 기본 스레드의

uncaughtExceptionHandler에 의해 처리될 가능성이 높다. 그래서 지금처럼 예외 발생에 대한 로깅만 하고 넘어가는 것이다.


여기까지 문제없이 이해했다면 앞서 언급한 다음 문장을 코드로 검증해 볼 수 있다.

별도의 예외 처리를 하지 않는 상황에서 발생한 예외를 처리하는 기본 예외 처리기는 CoroutineExceptionHandler이다.

실행 결과는 다음과 같다.

Caught in try-catch java.lang.NullPointerException: My NullPointerException

실행 결과를 보면 launch 내부에서 try-catch로 별도의 예외 처리를 하고 있고 따라서CoroutineExceptionHandler에서는 예외 처리를 하지 않는다는 것을 알 수 있다.


예외 처리 관점에서 다시 보는 CancellationException

다음 문장은 try-catch를 통해서 이미 검증했지만 좀 더 명확하게 CoroutineExceptionHandler를 통해서 확인할 수 있다.

취소 시 발생하는 CancellationException은 정상으로 간주하고 무시한다.

다시 try-catch 문을 통해서CancellationException이 발생하는 것을 확인해 보자. (try-catch 문이 없을 때 예외 로깅없이 취소 동작이 수행된다. 이것이 CancellationException을 다른 예외와 다르게 무시하고 있다는 의미다.)

실행 결과는 다음과 같다.

Start
Cancel
Caught CancellationException

CoroutineExceptionHandler를 통해서CancellationException을 처리해 보자.

실행 결과는 다음과 같다.

Start
Cancel

애초에 try-catch 문이 없는 상태에서도 CoroutineExceptionHandler에 의해서 무시되고 있었을 것이라는 것을 이제는 추측할 수 있다. 좀 더 명확히 컨텍스트에 CoroutineExceptionHandler를 넘겨주어 확인했지만 역시나 같은 결과이다.


예외 전파

CoroutineExceptionHandler를 바카라 꽁 머니의 컨텍스트로 넘겨주어 해당 바카라 꽁 머니 내에서 발생하는 모든 예외를 처리할 수 있다고 했다. 이로 인해 구조화된 동시성으로 인해 발생하는 예외 전파를 효율적으로 처리할 수 있다고 했다. 이때 예외는 CancellationException 이외의 예외를 의미한다.구조화된 동시성에서 언급했던 내용에 대해 잠시 복습을 하자.

1) 부모 바카라 꽁 머니은 자식 바카라 꽁 머니이 종료될 때까지 기다려준다.
2) 부모 바카라 꽁 머니이 취소되면 자식 바카라 꽁 머니도 모두 취소된다.

이 두 가지 내용을 예외 발생의 관점에서 재해석해 보면 어떨까?


1) 부모 바카라 꽁 머니은 자식 바카라 꽁 머니이 종료될 때까지 기다려주기 때문에자식 바카라 꽁 머니에서 예외가 발생하면 모든 자식 바카라 꽁 머니이 종료된 후에 예외를 처리한다.


2) 부모 바카라 꽁 머니이 취소되면 자식 바카라 꽁 머니도 취소되는 것은취소예외가 부모 바카라 꽁 머니에서 자식 바카라 꽁 머니으로 전파된 것이라고 할 수 있다.


이것을 다시 예외 관점으로 정리해 보자.


1) 자식 바카라 꽁 머니에서 예외가 발생하면 모든 자식 바카라 꽁 머니이 종료된 후에부모 바카라 꽁 머니에서예외를 처리한다.


2) 취소 예외가 부모 바카라 꽁 머니에서 자식 바카라 꽁 머니으로 전파된 것이라고 할 수 있다. 자식 바카라 꽁 머니의 예외를 부모 바카라 꽁 머니에서 처리한다고 하였으니자식 바카라 꽁 머니의 예외도 부모 바카라 꽁 머니으로 전파된다고 추측할 수 있다.


이 두 가지 내용을 코드를 통해 확인해 보자.

실행 결과는 다음과 같다.

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

GlobalScope에서 바카라 꽁 머니이 시작된다. 내부에는 두 개의 자식 바카라 꽁 머니이 존재한다. 자식 바카라 꽁 머니은Coroutine 1, Coroutine 2라고 부르겠다.


Coroutine 1에서NullPointerException이 발생했다. 그러면 예외로 인해 해당 바카라 꽁 머니을 취소하여 종료한다. 그 후 바로 해당 예외가 처리하지 않고,Coroutine 2가 종료된 것을 볼 수 있다. Coroutine 2는 매우 긴 시간 동안 delay 된 상태인데 취소되었다. 구조화된 동시성으로 인해 부모 바카라 꽁 머니은 자식 바카라 꽁 머니이 종료될 때까지 기다린다고 한 것과 다른 양상처럼 보인다. 그러나 이것은 Coroutine 1의 예외 발생이 부모 바카라 꽁 머니으로 전파되어 부모 바카라 꽁 머니이 해당 예외를 처리하기 위해서 Coroutine 2를 취소하기 때문이다. 이렇게 모든 자식 바카라 꽁 머니이 종료될 때까지 기다린 후에 부모 바카라 꽁 머니에서CoroutineExceptionHandler를 통해 예외 처리가 이뤄졌다.


정리하면 아래와 같다.

1) Coroutine 1이 NullPointerException을 던진다.
2) 부모 바카라 꽁 머니이 이 예외를 잡는다.
3) 부모 바카라 꽁 머니이 예외를 처리하기 위해 Coroutine 2를 취소한다.
4) Coroutine 2이 취소되어 finally 블록이 실행된다.
5) 예외가 처리된다.

여기서 하나 더 주목할 점은 예외 처리 후 부모 바카라 꽁 머니도 취소되었다는 것이다. Goodbye가 출력되지 않은 것으로 확인할 수 있다. CoroutineExceptionHandler은 예외 처리 후 바카라 꽁 머니을 계속 실행하지 않는다고 했다. 이것으로 알 수 있는 점이 있다. CoroutineExceptionHandler는 자식 바카라 꽁 머니에서 사용되지 않는다. 왜냐하면 자식 바카라 꽁 머니에서 발생한 예외도 부모 바카라 꽁 머니으로 전파되고 부모 바카라 꽁 머니에서 해당 예외를 처리하기 때문이다.


CoroutineExceptionHandler가 자식 바카라 꽁 머니에서 사용되지 않는다고 했는데 결과적으로 자식 바카라 꽁 머니의 예외를 처리하는 데 사용된 것이 모순처럼 보일 수 있다. 하지만 실제로 예외는 부모 바카라 꽁 머니으로 전파되었고 예외 처리 주체는 부모 바카라 꽁 머니이다. 부모 바카라 꽁 머니은 CoroutineExceptionHandler로 예외 처리 후 종료된다. 부모 바카라 꽁 머니이 종료되면 자식 바카라 꽁 머니도 종료된다. 이런 방식으로 자식 바카라 꽁 머니의 예외가 부모 바카라 꽁 머니으로 전파되고, 부모 바카라 꽁 머니에서 그에 대한 처리 결과인 바카라 꽁 머니 취소를 자식 바카라 꽁 머니으로 전파하기 때문에 모순으로부터 벗어날 수 있다.


요약하자.

- 취소 시 발생하는 CancellationException은정상으로 간주하여 CoroutineExceptionHandler에서 무시한다.
- 구조화된 동시성에 의해서 부모 바카라 꽁 머니이 취소되면 자식 바카라 꽁 머니도 취소된다.
- GlobalScope는 특정 컴포넌트의 생명주기에 묶이지 않는 최상위 바카라 꽁 머니을 시작하는 데 사용되는 전역 CoroutineScope이다.
- GlobalScope 내 바카라 꽁 머니에서별도의 예외 처리를 하지 않는다면 예외 발생 시기본 예외 처리기가 이를 처리한다.
- CoroutineExceptionHandler는 바카라 꽁 머니의 전역 예외 처리를 위해 존재한다.
- 자식 바카라 꽁 머니에서 예외가 발생하면 모든 자식 바카라 꽁 머니이 종료된 후에부모 바카라 꽁 머니에서예외를 처리한다.
-자식 바카라 꽁 머니의 예외는 부모 바카라 꽁 머니으로 전파된다.
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari