[Android] Result를 사용해 예외처리 구현해보기 (with. Compose)

김준영·2024년 8월 25일
0

Android

목록 보기
17/17
post-thumbnail

서론


앱을 사용하다보면 강제종료 경험은 누구나 한번쯤 겪어보는 경험입니다 😡
특히 앱은 웹과 다르게 빠른 업데이트가 불가능하기 때문에 더더욱 조심해서 개발해야합니다.

하지만 어떤 프로그램이던 100% 완벽히 동작하는 앱은 거의 없습니다 (버그가 없는 서비스가 더 무서워요 😦)

여기서 앱 개발자들의 최선은 무엇일까요?
위 사진처럼 에러가 발생해도 강제종료는 면하자입니다!

즉 앱이 강제로 꺼지는 현상은 매우 좋지못한 경험이기에 예외처리를 통해 사용자에게 저희가 앱을 잘못구현했습니다 절대 기기잘못이 아닙니다라는 정보를 알려야 할 의무가 있습니다

예외처리 방법

예외처리도 정말 많은 방법들이 있는데요
저는 그 중 코틀린 표준 라이브러리인 Result 객체를 활용해보려합니다

Result란?

성공 또는 실패를 나타내는 일종의 컨테이너입니다
보통 try, catch문을 사용해 예외처리를 하곤하지만 try, catch문은 코드의 가독성을 떨어뜨리고 예외처리에 대한 불편함이 종종 발생하곤 합니다.

한번 예제 코드를 볼까요?

try-catch문을 사용한 경우

fun divide(a: Int, b: Int): Int {
    return try {
        a / b
    } catch (e: ArithmeticException) {
        println("Error: ${e.message}")
        0 // 실패 시 기본값을 반환
    }
}

fun main() {
    val result1 = divide(10, 2)
    println("Result: $result1") // 성공적인 결과

    val result2 = divide(10, 0)
    println("Result: $result2") // 실패한 경우, 기본값 0 출력
}

Result를 사용한 경우

fun divide(a: Int, b: Int): Result<Int> {
    return runCatching {
        a / b
    }
}

fun main() {
    val result1 = divide(10, 2)
    result1.onSuccess { value ->
        println("Result: $value") // 성공적인 결과
    }.onFailure { exception ->
        println("Error: ${exception.message}")
    }

    val result2 = divide(10, 0)
    result2.onSuccess { value ->
        println("Result: $value")
    }.onFailure { exception ->
        println("Error: ${exception.message}")
    }
}

언뜻보기엔 try-catch문이 더 가독성이 좋다고 느껴질수도 있습니다
관점을 바꿔봅시다

만약 에러처리를 다르게 하고싶다면 어떻게 해야할까요?
try-catch문은 함수자체에서 에러처리를 하고있기 때문에 자유로운 에러처리가 불편합니다

위 코드에선 단순히 출력문을 통해 에러를 표현하고 있지만
사용자와 상호작용하는 서비스에서 에러처리는 매우 다양하게 표현될 수 있습니다
즉 실제 함수를 사용하는 부분에서 에러처리를 한다면 좀 더 자유롭게 처리를 할 수 있겠죠?

runCatching

Result를 본격적으로 사용해보기전에 runCatching에 대해 알아봅시다

@InlineOnly
@SinceKotlin("1.3")
public inline fun <T, R> T.runCatching(block: T.() -> R): Result<R> {
    return try {
        Result.success(block())
    } catch (e: Throwable) {
        Result.failure(e)
    }
}

runCatching 원본 코드를 보면 try-catch문을 사용해 Result로 감싸서 반환하는 것을 볼 수 있습니다.
즉, Result를 편하게 사용하기 위해 runCatching을 사용할 수 있습니다

예외처리 구현해보기

가장 흔하게 겪는 에러인 타임아웃, 네트워크에러를 다뤄보겠습니다

네트워크 에러처리

네트워크 연결이 안되어있다면 네트워크 에러가 발생합니다

우선 결과부터 보겠습니다

해당 화면은 특정 장소에 대한 API를 받아오는 로직이 포함되어있습니다
네트워크 연결을 해제한 상태로 API 호출을 하면 당연히 네트워크 관련 에러가 발생하겠죠?

이제 코드를 봅시다!

class PlaceRepositoryImpl @Inject constructor(
    ...
    private val getPlaceInfoService: GetPlaceInfoService,
    ...
): PlaceRepository {
    (..)
    override suspend fun getPlaceInfo(place: String): Result<PlaceInfo?> {
        return runCatching {
            val result = getPlaceInfoService.getPlaceInfo(place)
            result.body()?.let { response->
                placeDataMapper.mapperToPlaceInfo(response.placeInfoData?.firstOrNull())
            }
        }
    }
}

먼저 API 호출을 통해 받은 응답 body를 runCatching으로 감싸서 반환해줍니다.
여기서 body에 문제가 없다면 sucess를, body에 문제가 있다면 failure를 반환하게 됩니다

즉 해당 함수를 사용하는 곳에 에러처리를 위임하는 것이죠

사용하는 코드를 보겠습니다

@HiltViewModel
class PlaceDetailViewModel @Inject constructor(
    private val getPlaceInfoUseCase: GetPlaceInfoUseCase,
    (..)
): BaseViewModel<PlaceDetailContract.Event, PlaceDetailContract.State, PlaceDetailContract.Effect>() {
   (..)
    fun getPlaceInfo(place: String){
        setState { copy(placeInfoState = PlaceInfoState.LOADING) }
        if(place.isNotBlank()){
            viewModelScope.launch {
                getPlaceInfoUseCase(place)
                    .onFailure { exception->
                        val errorState = when(exception) {
                            is java.net.UnknownHostException -> PlaceInfoState.NETWORK_ERROR
                            is java.net.SocketTimeoutException -> PlaceInfoState.NETWORK_ERROR
                            else -> PlaceInfoState.ERROR
                        }
                        setState { copy(placeInfoState = errorState, errorThrowable = exception) }
                    }
                    .onSuccess { response->
                        response?.let {
                            setState {
                                copy(
                                    placeInfo = response,
                                    placeStateColor = uiMapper.mapperLivePeopleColor(response.livePeopleInfo ?: ""),
                                    placeInfoState = PlaceInfoState.SUCCESS,
                                )
                            }
                        }
                    }
            }
        }
    }
    (...)
}
   

참고로 getPlaceInfoUseCase는 위 코드 리포지토리를 유스케이스 단위로 분할한 것입니다!

getPlaceInfoUseCase의 반환값이 Result이기 때문에 onFailure에 쉽게 에러처리를 할 수 있습니다.
만약 try-catch문을 사용했다면 코드의 가독성이 상대적으로 떨어지고 에러처리에 대한 불편함이 있을 것입니다

해당 네트워크 에러가 발생한다면 특정 에러에 대한 화면을 사용자에게 보여줌으로써 서비스 사용경험을 향상시킬 수 있습니다

네트워크 에러 뿐만아니라 다양한 에러처리를 Result를 이용해 간편하고 자유롭게 구현할 수 있습니다!

결론

예외처리는 어떻게보면 가장 중요한 과제라고 생각합니다!
아무리 훌륭한 서비스라도 예외처리를 적절히 하지 못한다면 사용자는 더이상 사용하지 않을겁니다

Result를 사용해 예외처리를 간편하고 다양하게 할 수 있었습니다
해당 글이 도움이 되었으면 좋겠습니다 😊

profile
Android, Flutter를 공부하고 있습니다🧐

0개의 댓글

관련 채용 정보