Developing Myself Everyday

예외 처리의 경우, 코루틴 빌더들은 다음의 전략을 따른다

 

 1. 부모 코루틴이 자식에게 발생한 오류와 동일한 오류로 취소된다.

 2. 자식들이 모두 취소되면 부모는 예외를 코루틴 트리의 윗부분으로 전달한다.

 

아래의 코드를 보면서 이해해보자

 

코드

fun main() {
    runBlocking {
        launch {
            throw Exception("A에 에러 발생")
            println("A 완료")
        }

        launch {
            delay(1000)
            println("B 완료")
        }

        println("메인")
    }
}

 

결과

메인
Exception in thread "main" java.lang.Exception: A에 에러 발생

 

 

위의 코드를 보게 되면 최상위 코루틴이 "A 완료" 를 시작하기 전에 예외가 발생한다. 이로 인해 최상위 코루틴의 작업이 취소되고 B 작업도 취소된다. 

 

코루틴에서의 예외는 해당 코루틴 뿐만이 아니라 해당 코투린이 실행하는 모든 코루틴으로 퍼진다. 그렇다면 하나의 작업만 실패처리하고 다른 작업을 계속 실행시키고 싶다면 어떻게 해야할까?

 

Handler로 예외 처리


코루틴이 실행될 때 예외 처리가 되지 않은 경우, 이를 기본으로 처리하는 CoroutineExceoptionHandler를 추가할 수 있다. 자바의 Thread.uncaughtExceptionHandler와 같다고 생각하면 되는데 이는 등록된 Default 동작을 실행하는 역할을 한다.

 

아래의 코드에 핸들러를 만들고 이를 사용하는 예제가 있다.

 

 

코드

suspend fun main() {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("$exception")
    }

    GlobalScope.launch(handler) {
        launch {
            throw Exception("A에 에러 발생")
            println("A 완료")
        }

        launch {
            delay(1000)
            println("B 완료")
        }

        println("메인")
    }.join()
}

 

결과

메인
java.lang.Exception: A에 에러 발생

 

CoroutineExceoptionHandler는 전역 영역에서 실행된 코루틴에 대해서만 정의할 수 있고, CoroutineExceoptionHandler가 정의된 코루틴의 자식에 대해서만 적용된다.

 

그래서 전역 영역에서 코루틴을 호출하고 suspend 추가 후 join() 호출을 사용해야 한다.

 

※ 주의 - 그냥 runBlocking()을 사용하면 Default 핸들러를 사용하게 된다.

 

 

 

 

Handler를 사용하지 않는 예외 처리


 

아래의 코드를 보게 되면 deferredA.await()에서 예외를 던짐으로 "메인"이 실행되지 않는다는 것을 알 수 있다. 이런 경우에는 CoroutineExceoptionHandler를 사용하지 않고 아무 효과가 없이 그냥 Default가 실행되게 된다.

 

 

코드

fun main() {
    runBlocking {
        val deferredA = async {
            throw Exception("A에 에러 발생")
            println("A 완료")
        }

        val deferredB = async {
            delay(1000)
            println("B 완료")
        }

        deferredA.await()
        deferredB.await()
        println("메인")
    }
}

 

결과

Exception in thread "main" java.lang.Exception: A에 에러 발생

 


그럼 만약에 try-catch로 예외를 처리하려고 하면 어떻게 될까? 만약 단순하게 안쪽에 있는 코드에서 에러를 처리하고 싶은 경우에는 이를 사용할 수 있다.

 

launch {
    try {
        // 예외가 발생한 경우 
        error("cancels coroutine")
    } catch (e: Exception) {
        // 코드가 여기로 진행된다. 
        print("launch failed")
    }
}

 

 

그럼 만약 발생한 예외를 전역 핸들러를 통하지 않고 try-catch를 사용해서 부모 수준에서의 예외 처리가 가능할까? 이는 그렇지 않다.

 

fun main() {
    runBlocking {
        val deferredA = async {
            throw Exception("A에 에러 발생")
            println("A 완료")
        }

        val deferredB = async {
            delay(1000)
            println("B 완료")
        }

        try {
            deferredA.await()
            deferredB.await()
        } catch (e: Exception) {
            println(e)
        }
        println("메인")
    }
}

 

위 처럼 코드를 바꾼다고 해도 deferredA는 계속해서 부모를 취소시키기 위해 예외를 던진다. 이 상태에서 코루틴 전체를 실패시키지 않고 하나의 자식만 예외 처리를 하려면 SupervisorScope 가 필요하다.

 

SupervisorScope() 함수의 영역 안에서는 하나의 자식에 예외가 발생해도 슈퍼바이저나 슈퍼바이저의 다른 자식이 영향받지 않는다. 아래의 예제를 봐보자.

 

 

코드

fun main() {
    runBlocking {
        supervisorScope {
            val deferredA = async {
                throw Exception("A에 에러 발생")
                println("A 완료")
            }

            val deferredB = async {
                delay(1000)
                println("B 완료")
            }

            try {
                deferredA.await()
            } catch (e: Exception) {
                println(e)
            }
            deferredB.await()
            println("메인")
        }
    }
}

 

결과

java.lang.Exception: A에 에러 발생
B 완료
메인

 

 


이렇게 해서 코틀린의 예외 처리까지 학습하였다. 다음 게시글에서부터는 여러 동시성 작업 사이에 효율적으로 데이터를 공유할 수 있는 채널에 대하 알아보도록 하겠다.

 

profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!