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는 여러 method와 properties를 제공합니다.
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
- CoroutineExceptionHandler는 Thread의 UncaughtExceptionHandler를 구현하는 것으로 작동합니다.
- 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
반응형