관리 메뉴

나만을 위한 블로그

[코틀린 코루틴] 10. 예외 처리 본문

책/코틀린 코루틴

[코틀린 코루틴] 10. 예외 처리

참깨빵위에참깨빵 2024. 3. 5. 22:28
728x90
반응형

잡히지 않은 예외가 발생하면 앱이 종료되듯 코루틴도 종료된다. 쓰레드도 종료되지만 차이가 있다면 코루틴 빌더는 부모도 종료시키고 취소된 부모는 모든 자식을 취소시킨다는 것이다.

아래 예에선 예외를 받았을 때 자신을 취소하고 launch가 예외를 부모로 전파한다.

 

import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main(): Unit = runBlocking {
    launch {
        delay(1000)
        throw Error("에러 발생")
    }
    launch {
        delay(2000)
        println("여기는 출력되지 않음")
    }
    launch {
        delay(500) // 예외 발생보다 빠름
        println("여기는 출력됨")
    }
    launch {
        delay(2000)
        println("여기는 출력되지 않음")
    }
}

// 여기는 출력됨
// Exception in thread "main" java.lang.Error: 에러 발생

 

부모는 자신과 모든 자식을 취소하고 예외를 부모에게 전파한다(runBlocking). runBlocking은 부모가 없는 코루틴이라 프로그램을 종료시킨다.

예외는 자식에서 부모로 전파되며 부모가 취소되면 자식도 취소되기 때문에 쌍방으로 전파된다.

 

코루틴 종료 멈추기

 

코루틴 종료 전 예외를 잡는 건 도움이 되지만 늦으면 손쓸 수 없다. 코루틴 간의 상호작용은 Job을 통해 일어나기 때문에 코루틴 빌더 안에서 새 코루틴 빌더를 try-catch로 래핑하는 건 도움이 되지 않는다.

 

SupervisorJob

 

코루틴 종료를 멈추는 중요한 방법은 SupervisorJob을 쓰는 것이다. 이걸 쓰면 자식에서 발생한 모든 예외를 무시할 수 있다. 일반적으로 SupervisorJob은 여러 코루틴을 시작하는 스코프로 쓰인다.

 

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main(): Unit = runBlocking {
    val scope = CoroutineScope(SupervisorJob())
    scope.launch {
        delay(1000)
        throw Error("에러 발생")
    }

    scope.launch {
        delay(2000)
        println("여기는 출력됨")
    }
    delay(3000)
}

// Exception in thread "main" java.lang.Error: 에러 발생
// 여기는 출력됨

 

흔한 실수 하나는 SupervisorJob을 부모 코루틴의 인자로 쓰는 것이다.

 

import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main(): Unit = runBlocking {
    // 이렇게 하지 마라. 자식 코루틴 하나가 있고 부모 코루틴이 없는 Job은 일반 Job과 동일하게 적용된다
    launch(SupervisorJob()) { // 1
        launch {
            delay(1000)
            throw Error("에러 발생")
        }

        launch {
            delay(2000)
            println("여기는 출력되지 않음")
        }
    }

    delay(3000)
}

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

 

1에서 정의된 launch가 SupervisorJob을 인자로 받는데, 이 경우 SupervisorJob은 단 하나의 자식만 갖기 때문에 Job 대신 써도 예외 처리에 도움이 되지 않는다.

하나의 코루틴이 취소되어도 다른 코루틴은 취소되지 않는단 점에서 같은 Job을 여러 코루틴에서 컨텍스트로 쓰는 게 더 낫다.

 

supervisorScope

 

예외 전파를 막는 다른 방법은 코루틴 빌더를 supervisorScope로 래핑하는 것이다. 다른 코루틴에서 발생한 예외를 무시하고 부모와의 연결을 유지해서 편하다.

 

import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.supervisorScope

fun main(): Unit = runBlocking {
    supervisorScope {
        launch {
            delay(1000)
            throw Error("에러 발생")
        }

        launch {
            delay(2000)
            println("여기는 출력됨")
        }
    }
    delay(1000)
    println("Done!")
}

// (1초 후)
// Exception in thread "main" java.lang.Error: 에러 발생
// (1초 후)
// 여기는 출력됨
// (1초 후)
// Done!

 

supervisorScope는 중단 함수일 뿐이고 중단 함수 본체 래핑에 사용된다. 일반적인 사용법은 서로 무관한 여러 작업을 이 스코프 안에서 실행하는 것이다.

 

suspend fun notifyAnalytics(actions: List<UserAction>) =
    supervisorScope { 
        actions.forEach { action ->
            launch { 
                notifyAnalytics(action)
            }
        }
    }

 

예외 전파를 막는 다른 방법은 coroutineScope 사용이다. 이 함수는 부모에 영향을 주는 대신 try-catch로 잡을 수 있는 예외를 던진다.

그리고 supervisorScope는 withContext(SupervisorJob())으로 대체될 수 없다.

 

await

 

예외 발생 시 async는 launch처럼 부모 코루틴을 종료하고 부모와 관련 있는 다른 코루틴 빌더도 종료시킨다. SupervisorJob이나 supervisorScope를 쓰면 이런 과정이 일어나지 않는데 await를 쓰면 어떤가?

 

import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.supervisorScope

suspend fun main() = supervisorScope {
    val str1 = async<String> {
        delay(1000)
        throw MyException()
    }
    val str2 = async {
        delay(2000)
        "Text2"
    }

    try {
        println(str1.await())
    } catch (e: MyException) {
        println(e)
    }

    println(str2.await())
}

class MyException: Throwable()

// com.example.coroutineprac.MyException
// Text2

 

코루틴이 예외로 종료되서 리턴할 값이 없지만 await가 MyException을 던져서 MyException이 출력된다. supervisorScope의 영향으로 2번째 async는 중단되지 않고 끝까지 실행된다.

 

CancellationException은 부모까지 전파되지 않는다

 

예외가 CancellationException의 서브클래스면 부모로 전파되지 않는다. 현재 코루틴을 취소시킬 뿐이다. CancellationException은 open class라 다른 클래스, 객체로 확장될 수 있다.

책을 읽을 때 이 예외가 뭔지 잘 몰라서 코틀린 공식문서와 다른 글을 확인해 봤다.

 

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines.cancellation/-cancellation-exception/

 

CancellationException - Kotlin Programming Language

 

kotlinlang.org

코루틴이 일시 중단된 상태에서 취소 가능한 일시 중단 함수에 의해 취소될 때 발생한다. 코루틴의 정상적 취소를 나타낸다

 

https://developer.android.com/kotlin/coroutines/coroutines-best-practices?hl=ko#exceptions

 

Android의 코루틴 권장사항  |  Kotlin  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Android의 코루틴 권장사항 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 페이지에서는 코루틴을

developer.android.com

코루틴 취소를 사용 설정하려면 CancellationException 유형의 예외를 쓰지 마라. 즉 예외를 포착하면 안 되며 포착하는 경우 예외가 항상 다시 발생한다. Exception 또는 Throwable 같은 일반적 유형보다 IOException 같은 특정 예외 유형을 포착하는 게 좋다

 

https://kotlinlang.org/docs/exception-handling.html#cancellation-and-exceptions

 

Coroutine exceptions handling | Kotlin

 

kotlinlang.org

취소(Cancellation)는 예외와 밀접하게 관련 있다. 코루틴은 내부적으로 취소를 위해 CancellationException을 쓰며, 이런 예외는 모든 핸들러에서 무시되므로 catch 블록을 통해 얻을 수 있는 추가 디버그 정보의 소스로만 써야 한다. 코루틴이 job.cancel()을 써서 취소되면 해당 코루틴은 종료되지만 부모 코루틴은 취소되지 않는다
코루틴이 CancellationException이 아닌 다른 예외를 만나면 해당 예외로 부모 코루틴을 취소한다. 이 동작은 재정의할 수 없고 구조화된 동시성을 위해 안정적인 코루틴 계층 구조를 제공하는 데 쓰인다. CoroutineExceptionHandler 구현은 자식 코루틴에는 쓰이지 않는다. 예외는 모든 자식이 종료될 때만 부모가 처리한다

 

https://stackoverflow.com/questions/72594539/cancellationexception-silent-exception-inside-coroutinescope-does-not-crash-exi#comment128235035_72594539

 

CancellationException silent Exception inside CoroutineScope, does not crash/exit the application

I've been studying Kotlin Coroutines. I figure it out that when one cancels a Coroutine, it is thrown a CancellationException, but that does not crash / exit application. e.g. override fun onSt...

stackoverflow.com

CancellationException은 내부적으로 코루틴을 취소하는 방법으로 쓰인다. 잡히지 않아야 하며 잡히더라도 다시 던져야 한다

 

아래 링크는 CancellationException을 다루지 않지만 코루틴을 다룬다면 읽어보면 좋은 내용들이 많아서 걸어둔다.

 

https://medium.com/androiddevelopers/exceptions-in-coroutines-ce8da1ec060c

 

Exceptions in coroutines

Cancellation and Exceptions in Coroutines (Part 3) — Gotta catch ’em all!

medium.com

 

catch 블록에서 Exception, Throwable 같은 포괄적인 예외를 쓰는 것보단 좀 더 자세한 예외를 설정하는 게 권장되는 것으로 알고 있는데, 디벨로퍼에서도 동일하게 말하고 있다.

이 글은 CancellaitonException을 집중적으로 다루는 글이 아니기 때문에 위 글들의 내용을 간단하게 정리하고 넘어간다.

 

  • 코루틴이 일시 중단됐을 때 취소되면 발생
  • catch 블록에서 쓰는 건 권장되지 않음
  • 디버그 정보를 얻는 용도로만 사용해야 함

 

CoroutineExceptionHandler

 

예외를 다룰 때 예외를 처리하는 기본 행동을 정의하는 게 유용할 때가 있다. 이 때 CoroutineExceptionHandler 컨텍스트를 쓰면 편하다. 예외 전파를 중단하지 않지만 예외 발생 시 해야 할 것들(예외 스택 트레이스 출력 등)을 정의할 때 쓸 수 있다.

안드로이드에선 유저에게 대화창이나 에러 메시지를 표시해서 어떤 문제가 발생했는지 보여주는 역할을 한다.

반응형
Comments