[오류 해결] Coroutine Error Handling

na.ram·2024년 12월 8일
0

Kotlin

목록 보기
3/3
post-thumbnail

이전에 티스토리에 작성해두었던 부분을 옮겨왔습니다.


😮‍💨 문제 상황

중첩 코루틴 내부에서 오류가 발생하면 앱이 비정상적으로 종료되는 현상이 생겼습니다.
이런 현상을 방지하기 위해 Coroutine 예외처리 코드가 필요했습니다.

// Coroutines Error 가 발생하는 코드
suspend fun function(): ResultType = coroutineScope {
    val deferred1 = async { someWork() }
    val deferred2 = async {
        val result = deferred1.await()
        anotherWork(result) // 중첩된 비동기 작업
    }
    deferred2.await()
}

🔎 이유

  • launch 타입의 빌더로 생성된 코루틴이 루트 코루틴인 경우, 코루틴 계층 구조 내에서 발생한 예외는 루트 코루틴의 예외 핸들러에 의해 처리됩니다.

    • Exception을 외부로 전파(propagation) 시킨다.
  • async 타입의 빌더로 생성된 코루틴이 루트 코루틴인 경우, 코루틴 계층 구조 내에서 발생한 예외는 호출자에게 예외 처리를 맡기며 루트 코루틴의 예외 핸들러는 동작하지 않습니다. (호출자가 await(), receive() 등의 데이터 수신 함수를 호출할 때 예외가 발생함)

    • Exception을 노출(exposing) 시킨다.

async로 생성한 코루틴을 async에서 await()하면 예외가 발생한 코루틴에서 처리하기 때문에 앱이 비정상적으로 종료되는 현상은 발생하지 않습니다.

/**
 * MainViewModel.kt
 */
private fun coroutinesErrorHandling() {
    viewModelScope.async {
        Log.d(TAG, "${comApiRepository.function()}")
    }
}

하지만 만약 async로 생성한 코루틴을 예외 핸들러를 달지 않은 launch 블럭에서 await()하면 예외가 전파되면서 앱이 비정상적으로 종료되는 현상이 발생합니다.

/**
 * MainViewModel.kt
 */
private fun coroutinesErrorHandling() {
    viewModelScope.launch {
        Log.d(TAG, "${comApiRepository.function()}")
    }
}
--------- beginning of crash
E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.naram.dynamicurlapi, PID: 6508
    kotlin.KotlinNullPointerException: Response from com.rit.data.remote.service.ApiService.someWork was null but response body type was declared as non-null
        at retrofit2.KotlinExtensions$await$2$2.onResponse(KotlinExtensions.kt:43)
        at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:161)
        at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:535)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:764)
        Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@f3485f9, Dispatchers.Main.immediate]

💊 해결 방법

CoroutineExceptionHandler 사용

/**
 * MainViewModel.kt
 */
private val _state = MutableLiveData<State>()
val state: LiveData<State>
    get() = _state

// CoroutineExceptionHandler 생성
protected val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception ->
    Log.e("CoroutineExceptionHandler", "${exception.message}")

    when(exception) {
        is SocketException -> _state.postValue(State.BAD_INTERNET)
        is HttpException -> _state.postValue(State.PARSE_ERROR)
        is UnknownHostException -> _state.postValue(State.WRONG_CONNECTION)
        else -> _state.postValue(State.FAIL)
    }
}

private fun getData() {
    // 루트 코루틴에 CoroutineExceptionHandler 달기
    viewModelScope.launch(coroutineExceptionHandler) {
        Log.d(TAG, "${comApiRepository.function()}")
    }
}

runCatching 구문

루트 코루틴이 async인 경우

/**
 * MainViewModel.kt
 */
private fun coroutinesErrorHandling() {
    viewModelScope.async {
        Log.d(TAG, "${comApiRepository.functionErrorHandlingResult()}")
    }
}

/**
 * RepositoryImpl.kt
 */
suspend fun functionErrorHandlingResult(): ApiResponse? {
    coroutineScope {
        val someWork = async { someWork() }
        val anotherWork = async { anotherWork(someWork.await()) }

        return kotlin.runCatching {
            anotherWork.await()
        }.onSuccess {
            Log.d(this::class.java.name, "$it")
        }.onFailure {
            Log.e(this::class.java.name, "${it.message}")
        }.getOrThrow()
    }
}

루트 코루틴이 launch인 경우

/**
 * MainViewModel.kt
 */
private fun coroutinesErrorHandling() {
    viewModelScope.launch {
        Log.d(TAG, "${comApiRepository.functionErrorHandlingResult()}")
    }
}

/**
 * RepositoryImpl.kt
 */
suspend fun functionErrorHandlingResult(): ApiResponse? {
    coroutineScope {
        val someWork = async { someWork() }
        val anotherWork = async { anotherWork(someWork.await()) }

        return kotlin.runCatching {
            anotherWork.await()
        }.onSuccess {
            Log.d(this::class.java.name, "$it")
        }.onFailure {
            Log.e(this::class.java.name, "${it.message}")
        }.getOrThrow()
    }
}

루트 코루틴에서 runCatching

/**
 * MainViewModel.kt
 */
private fun coroutinesErrorHandling() {
    viewModelScope.launch {
        kotlin.runCatching {
            comApiRepository.function()
        }.onSuccess {
            Log.d(TAG, "$it")
        }.onFailure {
            Log.e(TAG, "${it.message}")
        }
    }
}

참고

코루틴 예외 다루기
[Kotlin] 코루틴의 취소와 예외 처리 파고들기
runCatching을 이용한 kotlin에서 exception처리 방법

0개의 댓글

관련 채용 정보