Android

Android) Coroutine Exception Handling 어떻게 처리 할까

가짜 개발자 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 타입

반응형