본문 바로가기

코루틴

[코루틴] 예외 처리

코루틴의 예외 처리에 대해서 알아보자

 

코루틴의 예외 전파

코루틴에서 예외가 전파되는 방식

코루틴 예외 전파

  1. Coroutine5에서 예외가 발생 
  2. Coroutine2로 예외 전파
  3. Coroutine2에서 예외 처리가 되지 않으면 Coroutine1으로 예외 전파
  4. 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: 예외 발생

 

  1. Coroutine3에서 예외 발생 (예외처리 부분 X) -> Coroutine1으로 예외 전파
  2. Coroutine1에서 예외처리 X -> runBlocking으로 예외 전파
  3. 루트 코루틴인 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)] 코루틴 실행

 

  1. Coroutine3에서 예외가 발생
  2. 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)] 코루틴 실행

 

supervisorScope 사용

 

 

예외 처리 - 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에 예외가 발생

 

  1. CoroutineScope를 사용해 runBlocking으로 생성된 Job과의 구조화 깨짐
  2. 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에 예외가 발생

 

  1. Coroutine1에서 Exception 발생
  2. runBlocking 코루틴으로 예외 전파
  3. 예외가 전파되어 Exception을 이미 처리한 것으로 간주해 ceh가 동작하지 않음

Child Coroutine1에만 설정된 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에 예외가 발생

 

Job + CEH

 

 

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] 코루틴 실행

 

SupervisorJob + CEH

 

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에 예외가 발생

 

  1. Coroutine1에서 예외 발생
  2. 예외 전파를 제한하지 않아 runBlocking으로 예외 전파

예외 전파를 제한하지 않는 CEH

 

 

 

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에 예외가 발생

 

  1. Coroutine1에서 예외 발생
  2. runBlocking 코루틴으로 예외 전파
  3. 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에 예외 발생

 

  1. Coroutine1에서 예외 발생
  2. 부모 코루틴인 runBlocking으로 예외 전파
  3. Coroutine2 취소

async 예외 전파

 

 

예외 전파를 처리하는 경우 

// 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)] 코루틴 실행

 

  1. Coroutine1에서 예외 발생
  2. supervisorJob으로 예외 전파 X
  3. 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)] 코루틴 실행

 

CancellationException

 

 

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)] 코루틴 실행

 

  1. Child Coroutine에서 withTimeout 1초, delay 2초를 걸어 TimeoutCancellationException 발생
  2. 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
    }
}