애플리케이션은 다양한 예외 상황에 노출되며, 예외를 적절히 처리해 애플리케이션의 안정성을 확보할 수 있습니다. 코루틴에서 예외가 발생한다면 어떤 일이 벌어질까요?
1. 코루틴에서 예외가 발생하면?
코루틴은 비동기 작업을 실행할 때 사용되기 때문에 애플리케이션의 안정성을 위해 예외 처리가 필수죠.
일례로, 중첩된 코루틴에서 예외가 발생하는 상황을 재현해보았습니다.
위 코드는 언뜻 보기에 “Coroutine3”의 코루틴에서 예외가 발생하였으니, 나머지 코루틴 속 println
이 문제없이 실행 될 것처럼 보입니다.
하지만, 실행해보면 Coroutine3
에서 발생한 예외가 상위 코루틴까지 전파되어, 모든 코루틴이 취소되는 것을 확인할 수 있습니다.
flowchart TD runBlocking --> parentCoroutine1[Job\nCoroutine 1] runBlocking --> parentCoroutine2[Job\nCoroutine 2] parentCoroutine1 --> childCoroutine["❌예외 발생❌\nCoroutine 3"] childCoroutine -. 예외 전파 .-o parentCoroutine1 parentCoroutine1 -. 예외 전파 .-o runBlocking runBlocking -. 취소 .-x parentCoroutine2
코루틴에서 발생한 예외는 부모 코루틴으로 전파되며, 적절히 처리되지 않으면 최상위 루트 코루틴까지 전파됩니다. 예외를 전파받은 코루틴이 취소되면 해당 코루틴의 모든 자식 코루틴에 취소가 전파됩니다. 즉, 루트 코루틴이 예외로 취소되면 구조화된 모든 코루틴이 취소되어 프로그램이 종료됩니다.
이렇게 코루틴에서 발생한 예외가 전파되는 것을 막으려면 어떻게 해야할까요? 우선, 코루틴에서 발생하지만, 부모 코루틴으로 전파되지 않는 예외에 대해 알아보겠습니다.
2. 전파되지 않는 코루틴 예외
CancellationException
TimeoutCancellationException
코루틴에서 발생한 예외는 부모 코루틴으로 전파되지만, 일부 예외는 전파되지 않습니다.
그 중 대표적인 예외로 CancellationException
과 TimeoutCancellationException
이 있습니다.
CancellationException
은 다른 예외와 달리 부모 코루틴으로 전파되지 않습니다.
CancellationException
이 전파되지 않는 이유는 CancellationException
은 코루틴을 취소하기 위한 특별한 예외이기 때문이죠.
TimeoutCancellationException
은 코루틴 실행 시간이 예상을 초과한 경우, 발생하는 예외입니다.
TimeoutCancellationException
의 상세 구현을 보면 CancellationException
의 서브 클래스라는 것을 확인할 수 있습니다.
이 경우도 마찬가지로 예외가 발생한 코루틴만 취소한다.
그렇다면, 코루틴에 실행 예상 시간을 어떻게 설정할 수 있을까요? 바로 withTimeout
을 사용하면 됩니다.
2.1. withTimeout
으로 TimeoutCancellationException
생성하기
withTimeout
함수를 사용해 코루틴의 실행 시간을 제한할 수 있습니다.
withTimeout
함수는 실행 시간 초과 시 CancellationException
의 서브 클래스인 TimeoutCancellationException
을 발생시켜 해당 코루틴만 취소합니다.
추가로 withTimeoutOrNull
을 사용하면 실행 시간 초과 시 null이 반환되도록 할 수 있습니다.
withTimeoutOrNull
을 사용하면 실행 시간 초과 시 결과값을 null로 반환하도록 할 수 있습니다.
이외에, 전파되는 예외들을 제어하려면 어떻게 해야할까요?
2. 코루틴 예외 전파를 막으려면?
2.1. 코루틴 예외 전파 차단 - Job
새로운 루트 Job
객체를 생성하여 코루틴의 구조화를 깨서 코루틴의 예외 전파를 제한할 수 있습니다.
코루틴 예외 관련 이야기 서두에 등장한 예시 코드와 동일한 코드를 사용하여 예시를 만들어 보았습니다.
Coroutine 1
생성 시, Job
객체를 새로 만들어 runBlocking
에서 이어지는 구조를 끊어보았습니다.
실행해보면, Coroutine 3
에서 발생한 예외가 Coroutine 1
까지는 전파되지만, Coroutine 2
는 실행되어 종료되는 것을 확인할 수 있습니다.
위 코드를 이와 같이 그래프로 나타낼 수 있습니다.
flowchart TD runBlocking["Job\nrunBlocking"] --> parentCoroutine2["Job\nCoroutine 2"] parentCoroutine1[Job\nCoroutine 1] --> childCoroutine["❌예외 발생❌\nCoroutine 3"] childCoroutine -. 예외 전파 .-o parentCoroutine1
Coroutine 3
에서 발생한 예외는 Coroutine 1
까지 전파되지만,
runBlocking
과 관계 없이 새로 생성한 Job
객체를 사용한 Coroutine 1
까지만 전파되고, Coroutine 2
는 실행되어 종료됩니다.
flowchart TD runBlocking["Job\nrunBlocking"] runBlocking --> parentCoroutine2["Job\nCoroutine 2"] parentCoroutine1[Job\nCoroutine 1] --> childCoroutine1["Job\nCoroutine 3"] parentCoroutine2 --> childCoroutine2["😵취소😵\nCoroutine 4"] parentCoroutine2 --> childCoroutine3["Job\nCoroutine 5"] childCoroutine2 -. 취소 전파 .-o parentCoroutine2 parentCoroutine2 -. 취소 전파 .-o runBlocking parentCoroutine2 -. 취소 전파 .-o childCoroutine3
Job 객체를 생성해 코루틴의 구조화를 깨는 것은 예외 전파를 제한하는 것뿐만 아니라 취소 전파도 제한시킵니다. 일반적으로 코루틴의 구조화는 큰 작업을 연관된 작은 작업으로 나누는 과정을 통해 일어나는데, 만약 작은 작업의 구조화가 깨진다면 큰 작업에 취소가 요청되더라도 작은 작업은 취소되지 않으며 이는 비동기 작업을 불안정하게 만듭니다.
2.2. 코루틴 예외 전파 차단 - SupervisorJob
SupervisorJob
객체를 사용해 예외 전파를 제한할 수 있다. SupervisorJob
객체는 예외를 전파받지 않는 특수한 Job
객체입니다.
SupervisorJob
객체는 자식 코루틴으로부터 예외를 전파받지 않는 특수한 Job
객체로 하나의 자식 코루틴에서 발생한 예외가 다른 자식 코루틴에게 영향을 미치지 못하도록 만드는 데 사용됩니다.
일반적인 Job
객체는 자식 코루틴에서 예외가 발생하면 예외를 전파받아 취소되지만 SupervisorJob
객체는 예외를 전파 받지 않아 취소되지 않습니다.
SupervisorJob 생성 함수를 parent 인자 없이 사용하면 Supervisorjob 객체를 루트 Job으로 만들 수 있으며, parent 인자로 Job 객체를 넘기면 부모 Job이 있는 Supervisorjob 객체를 만들 수 있습니다.
그래프를 통해 비교해보도록 하겠습니다.
flowchart TD subgraph "SupervisorJob을 runBlocking 하위에 추가" runBlocking0["Job\nrunBlocking"] --> parentCoroutine10[SupervisorJob\nCoroutine 1] parentCoroutine10 --> parentCoroutine20["Job\nCoroutine 2"] parentCoroutine10 --> childCoroutine0["❌예외 발생❌\nCoroutine 3"] childCoroutine0 -. 예외 전파 .-o parentCoroutine10 end subgraph "예외 발생 예시" runBlocking --> parentCoroutine1[Job\nCoroutine 1] runBlocking --> parentCoroutine2[Job\nCoroutine 2] parentCoroutine1 --> childCoroutine["❌예외 발생❌\nCoroutine 3"] childCoroutine -. 예외 전파 .-o parentCoroutine1 parentCoroutine1 -. 예외 전파 .-o runBlocking runBlocking -. 취소 .-x parentCoroutine2 end
SupervisorJob
객체를 사용한 경우, Coroutine 3
에서 발생한 예외는 Coroutine 1
까지만 전파되고, Coroutine 2
는 문제없이 실행되어 종료됩니다.
즉, 일반 Job
객체와는 다르게, SupervisorJob
을 사용하면 작업의 구조를 깨지 않으면서도 예외 전파를 제한할 수 있습니다.
3. 코루틴 예외 처리 방법
3.1. 코루틴 예외 처리 방법 - try
-catch
코루틴 내부에서 try
-catch
문을 사용해 예외를 처리할 수 있습니다.
이 때, 우리가 염두에 두어야할 것은 코루틴 빌더 함수에 대한 try
-catch
문은 코루틴이 실행될 때 발생하는 예외를 잡지 못한다는 것입니다.
이 코드를 보면 Coroutine1 코루틴을 만드는 launch
코루틴 빌더 함수를 try
-catch
문으로 감싸지만
이 try
-catch
문은 Coroutine1 코루틴에서 발생하는 예외 를 잡지 못한다.
launch
는 코루틴을 생성하는 데 사용되는 함수일 뿐으로 람다식의 실행은 생성된 코루틴이 CoroutineDispatcher
에 의해 스레드로 분배되는 시점에 일어나기 때문이다.
즉, 이 try
-catch
문은 launch 코루틴 빌더 함수 자체의 실행만 체크하며, 람다식은 예외 처리 대상이 아니다.
3.2. 코루틴 예외 처리 방법 - CoroutineExceptionHandler
코루틴 실행 중 발생한 예외를 처리하는 두번째 방법은 CoroutineExceptionHandler
객체를 사용하는 것입니다.
CoroutineExceptionHandler
객체는 코루틴에서 발생한 예외를 처리하는 핸들러로, launch
함수의 두번째 인자로 넘겨줄 수 있습니다.
3.2.1. CoroutineExceptionHandler
- 예외 전파
여기서 우리가 기억할만한 점은 CoroutineExceptionHandler
객체는 예외 전파를 제한하지 않는다는 점입니다.
이 코드를 실행하면, Coroutine 1
에 exceptionHandler
를 설정하였음에도,
상위 코루틴으로 예외가 전파되는 것을 막지 못해 Coroutine 2
의 실행 내용이 출력되지 않음을 확인할 수 있습니다.
3.2.2 CoroutineExceptionHandler
- 예외 처리
이 코드를 실행하면, Coroutine 3
에서 발생한 예외가 Coroutine 1
에서 처리되어 프로그램이 종료되지 않음을 확인할 수 있습니다.
동시에 Coroutine 3
은 취소되어, CoroutineExceptionHandler
를 사용할 수 없었지만,
Coroutine 1
에서 예외를 처리하여 CoroutineExceptionHandler
에 선언한대로 메시지가 출력되는 것을 확인할 수 있습니다.
4. 코루틴 예외 처리
4.1. 코루틴 예외 처리 - async
-await
async 함수로 생성된 코루틴에서 발생한 예외는 await 호출 시 노출됩니다. async 코루틴에서 발생한 예외 또한 부모 코루틴으로 전파됩니다.
4.2. 코루틴 예외 처리 - coroutineScope
이전 코드에서 관람객 목록을 가져오는 함수 중 하나에서 예외가 발생하는 상황을 예로 들어, coroutineScope 의 예외 전파에 대해 알아보겠습니다.
이 코드에서 getParticipantList2
함수에서 예외가 발생하여, getParticipantList1
관람객 목록만 출력하고 프로그램이 종료될까요?
안타깝게도, coroutineScope
함수는 일반 Job
객체를 생성하기 때문에, 하위 코루틴에서 발생한 예외가 상위 코루틴으로 전파되어 프로그램이 종료됩니다.
위와 같은 상황을 방지하기 위해 supervisorScope
함수를 사용할 수 있습니다.
4.2. supervisorScope
예외 전파 막기
flowchart TD subgraph coroutineScope runBlocking["*Job*\nrunBlocking"] --> coroutineScope[*Job*\ncoroutineScope] coroutineScope --> childCoroutine1[*Deferred*\nchildCoroutine1] coroutineScope --> childCoroutine2[*Deferred*\nchildCoroutine2] end subgraph supervisorScope runBlocking0["*Job*\nrunBlocking"] --> supervisorScope0[*SupervisorJob*\nsupervisorScope] supervisorScope0 --> childCoroutine10[*Deferred*\nchildCoroutine1] supervisorScope0 --> childCoroutine20[*Deferred*\nchildCoroutine2] end
supervisorScope
는 coroutineScope
와 동일하게 일시 중단 함수 내에서 상위 코루틴 환경을 가져와 사용할 수 있게 도와줍니다.
다만 한가지 차이가 있다면, 생성되는 Job
이 일반 Job
이 아닌 SupervisorJob
라는 점입니다.
그 덕분에 하위 코루틴에서 오류가 발생하더라도 상위 코루틴으로 예외가 전파되는 것을 방지 할 수 있습니다.
4.2. supervisorScope
예외 전파 막기 - 코드
이번에는 getParticipantList2
함수에서 예외가 발생하더라도, getParticipantList1
함수의 결과를 출력하고 프로그램이 종료되지 않음을 확인할 수 있습니다.