Android & Kotlin

[ Android ] CallAdapter, SealedClass 를 활용한 레트로핏 응답 처리

쉽코기 2022. 7. 12. 01:28

📌 문제상황 

 

레트로핏을 통한 통신시 결과 값에 대해 처리에 대한 고민이 생겼습니다..

 

1. 서버로 부터 실패의 응답이 오는 경우

2. 서버로부터 잘못된 형태의 응답이 오는 경우

3. 응답 형태이긴 하지만 빈값인 경우

 

등등 응답에는 다양한 문제가 있을 수 있고 이를 관리할 수 있는 무언가가 필요하다는 생각을 했습니다.

응답에 대한 검증을 어디서 어떻게 할지 또한 고민이었습니다.

 

📌 해결 방법 1 : Network Reuslt Class (Seald Class)

 

첫번째 방법은 SealdClass 로 응답을 감쌈으로써 응답의 형태를 일관성 있게 추상화 하는 것입니다.

 

sealed class NetworkResult<T : Any> {
    class Success<T : Any>(val data: T) : NetworkResult<T>()
    class Error<T : Any>(val code: Int, val message: String) : NetworkResult<T>()
    class Exception<T : Any>(val e: Throwable) : NetworkResult<T>()
    class Loading<T : Any>() : NetworkResult<T>()
}

 

1. NetworkResult.Success 

무결성이 보장된 body 데이터를 의미합니다.

2. NetworkResult.Error

응답을 받아내긴 했지만 성공적인 응답이 아닌 경우, 데이터가 비어있는 경우 등을 의미합니다.

3. NetworkResult.Exception

응답조차도 받지 못한는 상태를 의미합니다. 레트로핏의 Response 의 fail 을 처리하는 클래스입니다.

4. NetworkResult.Loading

네트워크 결과를 받아오기 전 초기값을 의미합니다.  리엑티브한 구현에서 State-Flow를 사용하는 저에게 한눈에 알아볼 수 있는  Loading 이라는 초기값을 줄 수 있다는 것 또한 매력 적이었습니다.

 

그러나 매번 Response 마다 분기처리하면서 NetworkResult 로 감싸는 방법은 보일러플레이트를 유발하게됩니다.

 

 

📌  해결 방법 2 : Handle Api  -  Network Reuslt Class 확장

 

해당 해결방법은 위에서 제시했던 문제중 응답에대한 검증을 어디서 어떻게 일관성 있게 하느냐와

보일러 플레이트를 없애는 해결책 입니다.

 

Data Layer 에서 확장함수를 통해서 Rotrofit 의 Response 객체를 Custom 한 Seald Class 로 변형 시켜줍니다.

 

// network

suspend fun <T : Any> handleApi(result: Response<T>): NetworkResult<T> {
    return try {
        val body = result.body()
        if (result.isSuccessful && body != null) {
            NetworkResult.Success(body)
        } else {
            NetworkResult.Error(result.code(), message = result.message())
        }
    } catch (e: HttpException) {
        NetworkResult.Error(result.code(), message = result.message())
    } catch (e: Throwable) {
        NetworkResult.Exception(e)
    }
}
 // RetrofitApiClient
 
 @POST("upload/json/menu/{menuPath}")
    suspend fun getMenuDetail(@Path("menuPath") url: String): Response<Menu>

 
 
 // DataSource

 override suspend fun getMenuDetail(url: String): NetworkResult<Menu> {
     val response = starbucksApi.getMenuDetail(url)
        return handleApi(response)
    }

 

📌 해결 방법 3 : Custom CallAdapter 

 

그러나 매번 handleApi 를 호출해야하며  또한 DataLayer 에 API 결과를 가공하는 책임이 있음에도 handleApi 에 의존하고 있다는 문제가 있습니다. 이는 적절한 책임 분리가 되지 않았다고 볼 수 도 있겠습니다.

 

이는 custom 레트로핏 CallAdpater 를 통해 내부적으로 call response type 변환을 위임하는 방식으로 해결할 수  있습니다.

한마디로 레트포핏 응답 타입을 우리가 지정하는 SealdClass 로 받아낼 수 있다는 말입니다.

 

 

 

📍 CallAdpater 구현 방법

https://velog.io/@suev72/AndroidRetrofit-Call-adapter

 

[Android/Retrofit] Call adapter - 이해/개발

은 HTTP API를 별도 조작 없이 쉽게 응답을 객체로 변환해주는 라이브러리이다. 코틀린을 사용한다면 API 호출 시 내부적으로 요청이 이루어져서 따로 콜백을 정의할 필요없이 응답객체를 받을 수

velog.io

 

 

📌 추가사항 : 응답 결과에 따른 처리

아래 코드와 같이 확장함수를 만들고, 응답 결과에 따른 처리를 인자로 넘김으로써

좀더 일관성있게 응답에 따른 최종적인 처리를 할 수 있게된다.

suspend fun <T : Any> NetworkResult<T>.onSuccess(
    executable: suspend (T) -> Unit
): NetworkResult<T> = apply {
    if (this is ApiSuccess<T>) {
        executable(data)
    }
}

suspend fun <T : Any> NetworkResult<T>.onError(
    executable: suspend (code: Int, message: String?) -> Unit
): NetworkResult<T> = apply {
    if (this is ApiError<T>) {
        executable(code, message)
    }
}

suspend fun <T : Any> NetworkResult<T>.onException(
    executable: suspend (e: Throwable) -> Unit
): NetworkResult<T> = apply {
    if (this is ApiException<T>) {
        executable(e)
    }
}
viewModelScope.launch {
    val response = posterRemoteDataSource.invoke()
    response.onSuccess { posterList ->
        posterFlow.emit(posterList)
    }.onError { code, message ->
        errorFlow.emit("$code $message")
    }.onException {
        errorFlow.emit("${it.message}")
    }
}
view raw

 

 

📍 참고

https://proandroiddev.com/modeling-retrofit-responses-with-sealed-classes-and-coroutines-9d6302077dfe

 

Modeling Retrofit Responses With Sealed Classes and Coroutines

As the rate of data communication increases, the complexity of the application architecture also increases. How an application handles API…

proandroiddev.com