ABOUT ME

-

  • Android) Coroutine Exception Handling 어떻게 처리 할까
    Android 2021. 6. 27. 19:31


    예외 처리에는 다양한 방법들이 존재하겠지만, 몇 가지 방법을 알아보려 합니다.

     

    1. try / catch

    • 일반적으로 많이 사용하는 try-catch 블록을 사용하는 것입니다.
    • try 블록에서 예외를 던질 수 있는 코드를 작성하고, 예외가 발생하면 catch 블록에서 잡힙니다. 
    • 추가로 finally에서는 예외와 상관없이 해야 하는 코드를 작성해야겠습니다. (메모리 해제 등..)

     

    // ViewModel
    private val _images = MutableLiveData<ImageResponse>()
        val images : LiveData<ImageResponse> get() = _images
    
        fun fetchImages(query: String?, page: Int, size: Int) =
            viewModelScope.launch {
                val result = daumApi.loadImages(query, page, size)
    
                result.let {
                    try {
                        if(it.documents.isNotEmpty()) {
                            _images.postValue(it)
                        }else {
                            throw IllegalStateException()
                        }
                    }catch (e:Exception) {
                        // error handling
                        e.printStackTrace()
                    }
                }
            }
    • 위의 코드는 try-catch 블록을 활용한 코드입니다.
    • 사실 try-catch 블록만 사용해도 충분히 예외 처리를 할 수 있다고 생각합니다만, 코드가 많아질수록 boiler plate 코드가 늘어날 수 있고, 관리하기 힘들어 질 수 있습니다.
    • 또한, 범위 내에서 자식 중 예외가 발생하면 다음 자식이 실행되지 않고 실행이 종료되게 됩니다.
    • 그래서 개별 예외 처리를 다시 한번 해줘야 하는 필요가 생길 수 있습니다.

     

    	result.let {
                    try {
                        try {
                            _images.postValue(it)
                        } catch (e:Exception) {
                          // error handling  
                        }
                    }catch (e:Exception) {
                        // error handling
                    }
                }

     

    2. runCatching

    • runCatching은 코틀린 1.3 부터 도입된 캡슐화 블록입니다.
    • runCatching 블록 안에 성공/실패 여부가 캡슐화된 Result<T> 형태로 리턴합니다.
    public inline fun <R> runCatching(block: () -> R): Result<R> {
        return try {
            Result.success(block())
        } catch (e: Throwable) {
            Result.failure(e)
        }
    }
    • 위의 try-catch 블록을 runCatching 블록으로 바꾸면 아래와 같이 표현할 수 있습니다.
    fun fetchImages(query: String?, page: Int, size: Int) =
            viewModelScope.launch {
                val result = daumApi.loadImages(query, page, size)
    
                runCatching {
                   _images.postValue(result)
                }.onSuccess { 
                    // 성공시만 실행
                }.onFailure { 
                    // 실패시만 실행 (try - catch 블록의 catch와 유사)
                }
            }

     

    또한, runCatching의 retrun 값으로 전달받은 Result는 여러 methodproperties를 제공합니다.

     

    runCatching {
             _images.postValue(result)
          }.fold({
            // 성공시만 실행
          },{
            // 실패시만 실행
          })
                
    var fruitName: String? = null
    // 실패 시 default 값을 반환
    fruitName = fruitResult.getOrDefault("")
    // 실패 시 else block의 결괏값을 반환
    fruitName = fruitResult.getOrElse {
        when(it) {
            is IllegalStateException -> "Sold out"
            is NullPointerException -> "null"
            else -> throw it
        }
    }
    // 실패시 throwable이 다시 throw 
    fruitName = fruitResult.getOrThrow()
    
    // map, recover을 이용해 성공과 실패 시 원하는 값으로 바꿀 수 있음
    fruitResult.map {
        it.toUpperCase()
    }
    
    fruitResult.recover {
        when(it) {
            is IllegalStateException -> "Sold out"
            is NullPointerException -> "null"
            else -> throw it
        }
    }
           

     

    3. CoroutineExceptionHandler

    • CoroutineExceptionHandlerThreadUncaughtExceptionHandler를 구현하는 것으로 작동합니다.
    • UncaugthExceptionHandler는 캐치되지 않은 런타임 예외를 처리하는 방법입니다. 즉 Thread에서 캐치되지 않은 런타임 예외를 한 곳에서 처리할 수 있도록 도와줍니다.

     

     

    • 편하게 예외를 캐치하기 위해 전역으로 CoroutineExceptionHandler를 사용해봅시다.
    • coroutineExceptionHandler 블록 안에서 예외 처리를 해줍니다. 코드에서는 SingleLiveEvent를 이용해 예외를 옵저빙 하고 있습니다.

     

    open class BaseViewModel : ViewModel() {
    
        private val _isError = SingleLiveEvent<Boolean>()
        val isError: LiveData<Boolean> get() = _isError
    
        protected val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
            // error handling
            throwable.printStackTrace()
            _isError.call()
        }
    }
    // ViewModel
    class KakaoViewModel : BaseViewModel() {
    
        init {
            invokeException()
        }
    
        private fun invokeException() {
            viewModelScope.launch(coroutineExceptionHandler) {
                throw IllegalStateException()
            }
        }
        
    // Activity
    viewModel.isError.observe(this) {
          Toast.makeText(this, "Something Went Wrong!", Toast.LENGTH_SHORT).show()
     }

    • 강제로 예외를 발생시키고 있으니, 앱을 실행하자마자 토스트 메시지가 출력되겠죠?
    • 3가지의 예외 처리 방법을 알아봤습니다. 물론 정답은 없고, 상황에 맞게 예외를 처리하면 좋을 것 같습니다.

     


    Preference

    coroutineExceptionHandler

    runCatching과 Result 타입

    반응형

    댓글

Designed by Me.