코루틴의 예외 처리에 대해서 알아보자
코루틴의 예외 전파
코루틴에서 예외가 전파되는 방식
- Coroutine5에서 예외가 발생
- Coroutine2로 예외 전파
- Coroutine2에서 예외 처리가 되지 않으면 Coroutine1으로 예외 전파
- Coroutine1에서도 예외가 처리되지 않으면 Coroutine1 취소 후 자식 코루틴 취소 전파
코드
fun main() = runBlocking<Unit> {
launch(CoroutineName("Coroutine1")) {
launch(CoroutineName("Coroutine3")) {
throw Exception("예외 발생")
}
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
launch(CoroutineName("Coroutine2")) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
delay(1000L)
}
// 결과
Exception in thread "main" java.lang.Exception: 예외 발생
- Coroutine3에서 예외 발생 (예외처리 부분 X) -> Coroutine1으로 예외 전파
- Coroutine1에서 예외처리 X -> runBlocking으로 예외 전파
- 루트 코루틴인 runBlocking 취소 -> Coroutine2 취소 전파
코루틴의 예외 전파를 제한하는 방법
1. Job 객체 사용
Job 사용해서 부모 코루틴과의 구조화를 분리
fun main() = runBlocking<Unit> {
launch(CoroutineName("Parent Coroutine")) {
// new Job 연결
launch(CoroutineName("Coroutine1") + Job()) {
launch(CoroutineName("Coroutine3")) {
throw Exception("예외 발생")
}
delay(100L)
println("[${Thread.currentThread().name}, ${currentCoroutineContext()[CoroutineName]}] 코루틴 실행")
}
launch(CoroutineName("Coroutine2")) {
delay(100L)
println("[${Thread.currentThread().name}, ${currentCoroutineContext()[CoroutineName]}] 코루틴 실행")
}
}
delay(1000L)
}
// 결과
Exception in thread "main" java.lang.Exception: 예외 발생
[main, CoroutineName(Coroutine2)] 코루틴 실행
// 새로운 Job을 Coroutine1에 연결해 Coroutine1과 Parent Coroutine의 구조화 break
// Parent Coroutine에는 예외가 전파되지 않고 Coroutine2만 Parent Coroutine의 Child라 실행
하지만, Job을 사용하면 취소 전파도 제한되어 불안정한 작업이 될 수 있다
fun main() = runBlocking<Unit> {
val parentJob = launch(CoroutineName("Parent Coroutine")) {
// new Job 연결
launch(CoroutineName("Coroutine1") + Job()) {
launch(CoroutineName("Coroutine3")) {
delay(100L)
println("[${Thread.currentThread().name}, ${currentCoroutineContext()[CoroutineName]}] 코루틴 실행")
}
}
launch(CoroutineName("Coroutine2")) {
delay(100L)
println("[${Thread.currentThread().name}, ${currentCoroutineContext()[CoroutineName]}] 코루틴 실행")
}
}
delay(20L)
parentJob.cancel()
delay(1000L)
}
// 결과
[main, CoroutineName(Coroutine3)] 코루틴 실행
// ParentJob을 cancel 해도 구조화가 깨져 Coroutine2만 실행
2. SupervisorJob 사용
구조화를 깨지 않으며 예외 전파를 제한하는 방법
자식 코루틴으로부터 예외를 전파받지 않는 특수한 Job 객체
(하나의 Child 에서 발생한 Exception이 다른 Child에 영향을 미치지 못함)
SupervisorJob 만드는 방법
// parent -> Optional Job
public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent)
// Root Job으로 생성
val supervisorJob = SupervisorJob()
사용
fun main() = runBlocking<Unit> {
// supervisorJob parent로 runBlocking Job 설정
val supervisorJob = SupervisorJob(parent = this.coroutineContext[Job])
launch(CoroutineName("Coroutine1") + supervisorJob) {
launch(CoroutineName("Coroutine3")) {
throw Exception("예외 발생")
}
delay(100L)
println("[${Thread.currentThread().name}, ${currentCoroutineContext()[CoroutineName]}] 코루틴 실행")
}
launch(CoroutineName("Coroutine2") + supervisorJob) {
delay(100L)
println("[${Thread.currentThread().name}, ${currentCoroutineContext()[CoroutineName]}] 코루틴 실행")
}
// 명시적으로 complete 호출
supervisorJob.complete()
}
// 결과
Exception in thread "main" java.lang.Exception: 예외 발생
[main, CoroutineName(Coroutine2)] 코루틴 실행
- Coroutine3에서 예외가 발생
- Coroutine1으로 예외 전파 (SupervisorJob으로 전파하지는 않음)
SupervisorJob()을 통해 생성된 Job은 Job()을 통해 생성된 Job 객체와 같이 자동으로 완료처리 되지 않는다
그래서 명시적으로 complete을 호출
CoroutineScope + SupervisorJob 사용
CoroutineScope의 Child 코루틴에서 발생하는 예외가 다른 Child로 전파되지 않는다
fun main() = runBlocking<Unit> {
val coroutineScope = CoroutineScope(SupervisorJob())
coroutineScope.apply {
launch(CoroutineName("Coroutine1")) {
launch(CoroutineName("Coroutine3")) {
throw Exception("예외 발생")
}
delay(100L)
println("[${Thread.currentThread().name}, ${currentCoroutineContext()[CoroutineName]}] 코루틴 실행")
}
launch(CoroutineName("Coroutine2")) {
delay(100L)
println("[${Thread.currentThread().name}, ${currentCoroutineContext()[CoroutineName]}] 코루틴 실행")
}
}
delay(1000L)
}
// 결과
Exception in thread "DefaultDispatcher-worker-1" java.lang.Exception: 예외 발생
[DefaultDispatcher-worker-1, CoroutineName(Coroutine2)] 코루틴 실행
SupervisorJob 사용 시 주의할 점
코루틴 빌더 함수의 context에 SupervisorJob을 넘기고, 빌더 함수 scope 내부에 Child 코루틴들을 생성하는 경우
fun main() = runBlocking<Unit> {
launch(CoroutineName("Parent Coroutine" + SupervisorJob())) {
launch(CoroutineName("Coroutine1")) {
launch(CoroutineName("Coroutine3")) {
throw Exception("예외 발생")
}
delay(100L)
println("[${Thread.currentThread().name}, ${currentCoroutineContext()[CoroutineName]}] 코루틴 실행")
}
launch(CoroutineName("Coroutine2")) {
delay(100L)
println("[${Thread.currentThread().name}, ${currentCoroutineContext()[CoroutineName]}] 코루틴 실행")
}
}
delay(1000L)
}
// 결과. Exception만 발생
Exception in thread "main" java.lang.Exception: 예외 발생
context 인자에 Job을 설정할 경우 해당 Job 객체를 Parent로 하는 새로운 Job 객체가 만들어짐
SupervisorJob Parent Job이 만들어지고 예외가 ParentCoroutine까지 전달되고 모든 Child 코루틴에게 취소가 전파된다
3. supervisorScope를 사용한 예외 전파 제한
SupervisorJob 객체를 가진 CoroutineScope 객체를 생성하고
생성된 SupervisorJob 객체는 supervisorScope 함수를 호출한 코루틴의 Job을 부모로 가진다
supervisorScope 내부의 SupervisorJob은 Child 코루틴도 모두 실행 완료되면 자동으로 완료 처리
supervisorScope
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn { uCont ->
val coroutine = SupervisorCoroutine(uCont.context, uCont)
coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
사용
fun main() = runBlocking<Unit> {
supervisorScope {
launch(CoroutineName("Coroutine1")) {
launch(CoroutineName("Coroutine3")) {
throw Exception("예외 발생")
}
delay(100L)
println("[${Thread.currentThread().name}, ${currentCoroutineContext()[CoroutineName]}] 코루틴 실행")
}
launch(CoroutineName("Coroutine2")) {
delay(100L)
println("[${Thread.currentThread().name}, ${currentCoroutineContext()[CoroutineName]}] 코루틴 실행")
}
}
}
// 결과
Exception in thread "main" java.lang.Exception: 예외 발생
[main, CoroutineName(Coroutine2)] 코루틴 실행
예외 처리 - CoroutineExceptionHandler
공통적으로 예외 처리를 설정할 때 사용
CoroutineContext Element로 CoroutineExceptionHandler 사용
CoroutineExceptionHandler
// 예외를 처리하는 람다식인 handler
public inline fun CoroutineExceptionHandler(crossinline handler: (CoroutineContext, Throwable) -> Unit): CoroutineExceptionHandler =
object : AbstractCoroutineContextElement(CoroutineExceptionHandler), CoroutineExceptionHandler {
override fun handleException(context: CoroutineContext, exception: Throwable) =
handler.invoke(context, exception)
}
생성
val ceh = CoroutineExceptionHandler { coroutineContext, throwable ->
println("[예외 발생] $throwable")
}
사용
fun main() = runBlocking<Unit> {
val ceh = CoroutineExceptionHandler { coroutineContext, throwable ->
println("[예외 발생] $throwable")
}
CoroutineScope(ceh).launch(CoroutineName("Coroutine1")) {
throw Exception("Coroutine1에 예외가 발생")
}
delay(1000L)
}
// 결과
[예외 발생] java.lang.Exception: Coroutine1에 예외가 발생
- CoroutineScope를 사용해 runBlocking으로 생성된 Job과의 구조화 깨짐
- Coroutine1은 CoroutineScope으로부터 ExceptionHandler 상속받음
처리되지 않은 예외만 처리하는 CoroutineExceptionHandler
기본적으로 CEH는 처리되지 않은 예외만 처리
Child -> Parent로 예외를 전파하면 Child에서는 예외가 처리된 것으로 간주
즉, 마지막으로 예외가 처리되는 위치에 설정된 CEH 객체만 예외를 처리 -> 공통 예외 처리기로 동작 가능
// ceh가 동작하지 않음
fun main() = runBlocking<Unit> {
val ceh = CoroutineExceptionHandler { coroutineContext, throwable ->
println("[예외 발생] $throwable")
}
launch(CoroutineName("Coroutine1") + ceh) {
throw Exception("Coroutine1에 예외가 발생")
}
delay(1000L)
}
// 결과
Exception in thread "main" java.lang.Exception: Coroutine1에 예외가 발생
- Coroutine1에서 Exception 발생
- runBlocking 코루틴으로 예외 전파
- 예외가 전파되어 Exception을 이미 처리한 것으로 간주해 ceh가 동작하지 않음
예외가 처리되도록 만든 CoroutineExceptionHandler 만드는 방법
1. Root Job + CEH 사용
fun main() = runBlocking<Unit> {
val coroutineContext = Job() + CoroutineExceptionHandler { coroutineContext, throwable ->
println("[예외 발생] $throwable")
}
launch(CoroutineName("Coroutine1") + coroutineContext) {
throw Exception("Coroutine1에 예외가 발생")
}
delay(1000L)
}
// 결과
[예외 발생] java.lang.Exception: Coroutine1에 예외가 발생
2. SupervisorJob + CEH 사용
SupervisorJob은 Child 코루틴으로부터 예외를 전파받지 않지만 발생한 예외에 대한 정보는 전달받음
fun main() = runBlocking<Unit> {
val ceh = CoroutineExceptionHandler { coroutineContext, throwable ->
println("[예외 발생] $throwable")
}
val supervisedScope = CoroutineScope(SupervisorJob() + ceh)
supervisedScope.apply {
launch(CoroutineName("Coroutine1")) {
throw Exception("Coroutine1에 예외가 발생")
}
launch(CoroutineName("Coroutine2")) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
delay(1000L)
}
// 결과
[예외 발생] java.lang.Exception: Coroutine1에 예외가 발생
[DefaultDispatcher-worker-1] 코루틴 실행
Child 코루틴이 Parent로 예외를 전파하지 않고 전달만 해도 Child 코루틴에서 예외가 처리된 것으로 간주
CoroutineExceptionHandler 사용시 주의할 점
CEH는 예외가 마지막으로 처리되는 위치에서 예외를 처리할 뿐이고 예외 전파를 제한하지 않는다
(try - catch 과는 다름)
CEH는 예외 전파를 제한하는 기능이 없다
fun main() = runBlocking<Unit> {
val ceh = CoroutineExceptionHandler { coroutineContext, throwable ->
println("[예외 발생] $throwable")
}
launch(CoroutineName("Coroutine1") + ceh) {
throw Exception("Coroutine1에 예외가 발생")
}
}
// 결과
Exception in thread "main" java.lang.Exception: Coroutine1에 예외가 발생
- Coroutine1에서 예외 발생
- 예외 전파를 제한하지 않아 runBlocking으로 예외 전파
try - catch를 사용한 예외 처리
try - catch를 사용해 코루틴 예외 처리
예외가 발생해도 부모로 예외가 전파되지 않음
fun main() = runBlocking<Unit> {
launch(CoroutineName("Coroutine1")) {
try {
throw Exception("Coroutine1에 예외가 발생")
} catch (e: Exception) {
println(e.message)
}
}
launch(CoroutineName("Coroutine2")) {
delay(100L)
println("Coroutine2 실행 완료")
}
}
// 결과
Coroutine1에 예외가 발생
Coroutine2 실행 완료
try - catch 시 주의할 점
코루틴 빌더 함수에 대한 try - catch문은 코루틴의 예외를 잡지 못한다
항상 코루틴 빌더 함수의 내부에서 try - catch문을 사용해야 한다
// 빌더 함수 자체에 대해 try - catch
// 빌더 함수 자체의 실행만 확인, 람다식은 예외 처리 대상 X
fun main() = runBlocking<Unit> {
try {
launch(CoroutineName("Coroutine1")) {
throw Exception("Coroutine1에 예외가 발생")
}
} catch (e: Exception) {
println(e.message)
}
launch(CoroutineName("Coroutine2")) {
delay(100L)
println("Coroutine2 실행 완료")
}
}
// 결과
Exception in thread "main" java.lang.Exception: Coroutine1에 예외가 발생
- Coroutine1에서 예외 발생
- runBlocking 코루틴으로 예외 전파
- Coroutine2 취소
async의 예외 처리
기본적으로 async 빌더를 사용할 때는 전파되는 예외와 await 호출 시 노출되는 예외를 모두 처리해주어야 한다
async의 예외 노출
async 빌더 사용 시 await 호출할 때 예외 처리를 해야 한다
// deferred에 대한 await 호출 시 예외 노출
fun main() = runBlocking<Unit> {
supervisorScope {
val deferred: Deferred<String> = async(CoroutineName("Coroutine1")) {
throw Exception("Coroutine1에 예외가 발생")
}
try {
deferred.await()
} catch (e: Exception) {
println("[노출된 예외] ${e.message}")
}
}
}
// 결과
[노출된 예외] Coroutine1에 예외가 발생
async의 예외 전파
async 빌더 함수도 예외가 발생하면 부모 코루틴으로 예외를 전파
예외 전파를 처리하지 않는 경우
// await이 없음에도 async 빌더 함수에 대한 예외 발생
fun main() = runBlocking<Unit> {
async(CoroutineName("Coroutine1")) {
throw Exception("Coroutine1에 예외 발생")
}
launch(CoroutineName("Coroutine2")) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
// 결과
Exception in thread "main" java.lang.Exception: Coroutine1에 예외 발생
- Coroutine1에서 예외 발생
- 부모 코루틴인 runBlocking으로 예외 전파
- Coroutine2 취소
예외 전파를 처리하는 경우
// supervisorScope 사용으로 예외 전파 X
fun main() = runBlocking<Unit> {
supervisorScope {
async(CoroutineName("Coroutine1")) {
throw Exception("Coroutine1에 예외 발생")
}
launch(CoroutineName("Coroutine2")) {
delay(100L)
println("[${Thread.currentThread().name}, ${currentCoroutineContext()[CoroutineName]}] 코루틴 실행")
}
}
}
// 결과
[main, CoroutineName(Coroutine2)] 코루틴 실행
- Coroutine1에서 예외 발생
- supervisorJob으로 예외 전파 X
- Coroutine2 실행
전파되지 않는 예외
CancellationException
코루틴은 CancellationException이 발생해도 부모로 예외가 전파되지 않음
코루틴의 취소에 사용되는 특별한 예외이기 때문이다
특정 코루틴만 취소하는데 사용됨
fun main() = runBlocking<Unit>(CoroutineName("runBlocking Coroutine")) {
launch(CoroutineName("Coroutine1")) {
launch(CoroutineName("Coroutine2")) {
throw CancellationException()
}
delay(100L)
println("[${Thread.currentThread().name}, ${currentCoroutineContext()[CoroutineName]}] 코루틴 실행")
}
}
// 결과
[main, CoroutineName(Coroutine1)] 코루틴 실행
JobCancellationException
Job cancel을 호출하면 JobCancellationException을 발생시켜 코루틴을 취소
fun main() = runBlocking<Unit> {
val job = launch {
delay(1000L)
}
job.invokeOnCompletion { exception ->
println(exception)
}
job.cancel()
}
// 결과
kotlinx.coroutines.JobCancellationException:
StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelled}@22a67b4
withTimeOut
제한 시간을 두고 작업 실행
timeMillis 내에 완료되지 않으면 TimeoutCancellationException 발생
예외가 Parent로 전파되지 않고 해당 코루틴만 취소
public suspend fun <T> withTimeout(timeMillis: Long, block: suspend CoroutineScope.() -> T): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
if (timeMillis <= 0L) throw TimeoutCancellationException("Timed out immediately")
return suspendCoroutineUninterceptedOrReturn { uCont ->
setupTimeout(TimeoutCoroutine(timeMillis, uCont), block)
}
}
// TimeoutCancellationException, CancellationException의 sub class
public class TimeoutCancellationException internal constructor(
message: String,
@JvmField @Transient internal val coroutine: Job?
) : CancellationException(message), CopyableThrowable<TimeoutCancellationException> { .. }
// TimeoutCoroutine
private class TimeoutCoroutine<U, in T: U>(
@JvmField val time: Long,
uCont: Continuation<U> // unintercepted continuation
) : ScopeCoroutine<T>(uCont.context, uCont), Runnable {
override fun run() {
cancelCoroutine(TimeoutCancellationException(time, this))
}
}
사용
fun main() = runBlocking<Unit>(CoroutineName("Parent Coroutine")) {
launch(CoroutineName("Child Coroutine")) {
withTimeout(1000L) {
delay(2000L)
println("[${Thread.currentThread().name}, ${currentCoroutineContext()[CoroutineName]}] 코루틴 실행")
}
}
delay(2000L)
println("[${Thread.currentThread().name}, ${currentCoroutineContext()[CoroutineName]}] 코루틴 실행")
}
// 결과
[main, CoroutineName(Parent Coroutine)] 코루틴 실행
- Child Coroutine에서 withTimeout 1초, delay 2초를 걸어 TimeoutCancellationException 발생
- Parent로 예외가 전파되지 않아 그대로 2초후 그대로 Parent Coroutine 실행
try - catch 를 사용한 withTimeout
// catch에서 처리
fun main() = runBlocking<Unit>(CoroutineName("Parent Coroutine")) {
try {
withTimeout(1000L) {
delay(2000L)
println("[${Thread.currentThread().name}, ${currentCoroutineContext()[CoroutineName]}] 코루틴 실행")
}
} catch (e: Exception) {
println(e)
}
}
// 결과
kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms
withTimeoutOrNull
코루틴을 취소하지 않고 null 반환
fun main() = runBlocking<Unit>(CoroutineName("Parent Coroutine")) {
launch(CoroutineName("Child Coroutine")) {
val result = withTimeoutOrNull(1000L) {
delay(2000L)
return@withTimeoutOrNull "결과"
}
println(result)
}
}
// 결과
null
// withTimeoutOrNull
public suspend fun <T> withTimeoutOrNull(timeMillis: Long, block: suspend CoroutineScope.() -> T): T? {
if (timeMillis <= 0L) return null
var coroutine: TimeoutCoroutine<T?, T?>? = null
try {
return suspendCoroutineUninterceptedOrReturn { uCont ->
val timeoutCoroutine = TimeoutCoroutine(timeMillis, uCont)
coroutine = timeoutCoroutine
setupTimeout<T?, T?>(timeoutCoroutine, block)
}
} catch (e: TimeoutCancellationException) {
// Return null if it's our exception, otherwise propagate it upstream (e.g. in case of nested withTimeouts)
if (e.coroutine === coroutine) {
return null
}
throw e
}
}
'코루틴' 카테고리의 다른 글
[코루틴] 코루틴(Coroutine)과 서브루틴(Subroutine) (0) | 2024.09.20 |
---|---|
[코루틴] 일시 중단 함수 - suspend (0) | 2024.09.18 |
[코루틴] 구조화된 동시성 (0) | 2024.09.08 |
[코루틴] CoroutineContext (0) | 2024.08.31 |
[코루틴] async & Deferred (0) | 2024.08.30 |