[ 안드로이드 ] Retrofit 응답 Flow로 받기(FlowCallAdapter) - 2

 

 

 

 

[ 안드로이드 ] Retrofit 응답 Flow로 받기(FlowCallAdapter) - 1

이번 글에선 네트워크 요청에 Flow를 적용한 사례를 소개 드리고자 합니다. 문제 상황 기존 Api 요청 구조는 ViewModel / Repository / DataSource / Retrofit 등 한 곳에서 예외가 발생하면 바로 ExceptionHandler로

dongx2.tistory.com

 

 

이전 글에서 Flow를 통해 네트워크 통신 중 발생하는 예외에 대해 파이프라인을 구축하여 문제를 해결한 경험을 공유 드렸습니다.

 

해당 포스트에선 Retrofit CallAdapter를 통해서 Retrofit 응답을 Flow로 변환한 구체적인 과정을 공유 드릴 예정입니다.

Retrofit의 CallAdapter에 대해서 잘 모르신다면 간단하게 학습하고 읽으시는 것을 추천 드립니다.

 

 

기타 클래스 설명

FlowResult

typealias FlowResult<T> = Flow<Result<T>>

 

우선 FlowResult 타입을 정의 해주었습니다.

Flow가 Kotlin의 Result를 감싼 형태이며, Result 클래스를 통해 Api의 성공/실패를 반환할 예정입니다.

 

ApiException

class ApiException(
    val code: Int = -1,
    override val message: String? = ERROR_MSG_UNKNOWN,
    override val cause: Throwable? = null
) : Throwable(message) {

    fun toLog() = "$message (${code})"

    companion object {
        const val ERROR_MSG_UNKNOWN = "알 수 없는 에러가 발생하였습니다."
    }
}

fun Throwable.code(): Int {
    return if (this is WePLiException) this.code else -1
}

 

두 번째로 ApiException을 정의 해줍니다.

Result 클래스의 failure 메소드는 Throwable을 인자로 받고 있습니다.

@Suppress("INAPPLICABLE_JVM_NAME")
@InlineOnly
@JvmName("failure")
public inline fun <T> failure(exception: Throwable): Result<T> =
	Result(createFailure(exception))

 

실패 응답을 Result 클래스로 반환하기 위해서 Exception으로 매핑할 예정입니다.

 


Retrofit CallAdapter 커스텀

Retrofit은 Call 인터페이스를 통해 네트워크 통신을 지원합니다.

CallAdapter는 Call 인터페이스를 통해 Retrofit 응답 형식을 원하는 타입으로 변환할 수 있게 도와줍니다.

 

아래는 Retrofit에서 Flow를 반환하도록 구현한 코드입니다.

 

FlowCallAdapter

 

전체 코드

더보기
class FlowCallAdapter<T>(
    private val responseType: Type
) : CallAdapter<T, FlowResult<T>> {

    private val json = Json { ignoreUnknownKeys = true }
    override fun responseType() = responseType

    // Retrofit의 Call을 Result<>로 변환
    override fun adapt(call: Call<T>): FlowResult<T> = flow {
        emit(flowApiCall(call))
    }.flowOn(Dispatchers.IO)

    private suspend fun flowApiCall(call: Call<T>): Result<T> {
        return suspendCancellableCoroutine { continuation ->
            call.enqueue(object : Callback<T> {
                override fun onResponse(call: Call<T>, response: Response<T>) {
                    continuation.resume(parseResponse(response))
                }

                override fun onFailure(call: Call<T>, t: Throwable) {
                    continuation.resumeWithException(t)
                }
            })

            continuation.invokeOnCancellation {
                call.cancel()
            }
        }
    }

    private fun parseResponse(response: Response<T>): Result<T> {
        val nullBodyException by lazy {
            WePLiException(response.code(), ERROR_MSG_RESPONSE_IS_NULL)
        }

        if (!response.isSuccessful) {
            return Result.failure(parseErrorResponse(response))
        }

        return response.body()?.let {
            Result.success(it)
        } ?: Result.failure(nullBodyException)
    }

    // Response에서 오류를 파싱하여 RunnectException 객체를 생성
    private fun parseErrorResponse(response: Response<*>): ApiException {
        val errorBodyString = response.errorBody()?.string()
        val errorResponse = errorBodyString?.let { json.decodeFromString<ErrorResponse>(it) }

        val errorMessage = errorResponse?.message ?: ERROR_MSG_COMMON
        return WePLiException(response.code(), errorMessage)
    }

    companion object {
        private const val ERROR_MSG_COMMON = "알 수 없는 에러가 발생하였습니다."
        private const val ERROR_MSG_RESPONSE_IS_NULL = "데이터를 불러올 수 없습니다."
    }
}

 

1. adapt()

class FlowCallAdapter<T>(
    private val responseType: Type
) : CallAdapter<T, FlowResult<T>> {

    override fun responseType() = responseType

    // Retrofit의 Call을 FlowResult<T>로 변환
    override fun adapt(call: Call<T>): FlowResult<T> = flow {
        emit(flowApiCall(call))
    }.flowOn(Dispatchers.IO)
    
    /*...*/
}

 

CallAdapter는 adapt라는 메소드를 재정의 할 수 있도록 제공합니다.

이 메소드를 통해 원하는 타입으로 반환 값을 정할 수 있습니다.

 

2. flowApiCall()

private suspend fun flowApiCall(call: Call<T>): Result<T> {
    return suspendCancellableCoroutine { continuation ->
        call.enqueue(object : Callback<T> {
            override fun onResponse(call: Call<T>, response: Response<T>) {
                continuation.resume(parseResponse(response))
            }

            override fun onFailure(call: Call<T>, t: Throwable) {
                continuation.resumeWithException(t)
            }
        })

        continuation.invokeOnCancellation {
            call.cancel()
        }
    }
}

 

flowApiCall() 메소드의 구현은 간단합니다.

 

  1. suspendCancellableCoroutine을 이용하여 enquece 메소드의 콜백을 Coroutine으로 전환합니다.
  2. 서버와의 통신에 성공하면 응답을 반환합니다.
    • 데이터를 정상적으로 수신한 경우 Result.success로 반환 됩니다.
    • 서버에서 에러 응답을 내려준 경우 Result.failure로 반환 됩니다.
  3. 서버와의 통신에 실패하면 예외를 반환합니다.
    • 해당 예외는 Flow를 통해 최종적으로 ViewModel까지 전달 됩니다.

 

 

응답 파싱 

 

Api 응답을 Result 클래스로 변환하는 코드입니다.

 

    private fun parseResponse(response: Response<T>): Result<T> {
        val nullBodyException by lazy {
            ApiException(response.code(), ERROR_MSG_RESPONSE_IS_NULL)
        }

        if (!response.isSuccessful) {
            return Result.failure(parseErrorResponse(response))
        }

        return response.body()?.let {
            Result.success(it)
        } ?: Result.failure(nullBodyException)
    }
  1. 통신에 실패한 경우 parseErrorResponse()를 호출합니다.
  2. response.body()가 null인 경우 nullBodyException으로 변환합니다.
  3. 통신에 성공한 경우 Result.success(${Api 응답}})를 반환합니다.

 

    private fun parseErrorResponse(response: Response<*>): ApiException {
        val errorBodyString = response.errorBody()?.string()
        val errorResponse = errorBodyString?.let { json.decodeFromString<ErrorResponse>(it) }

        val errorMessage = errorResponse?.message ?: ERROR_MSG_COMMON
        return ApiException(response.code(), errorMessage)
    }

 

parseErrorResponse는 response.errorBody()를 파싱하여 ApiException으로 변환하는 메소드입니다.

서버에 응답에 맞게 자유롭게 파싱하면 됩니다.

 

 

 

Retrofit CallAdapter 연결

위 코드까지가 CallAdapter 커스텀의 끝입니다.

이제 Retrofit에 FlowCallAdapter를 연결해 주어야 합니다.

 

 

FlowCallAdapterFactory

Retrofit에 연결하기 위한 CallAdapter.Factory()를 구현해줍니다.

class FlowCallAdapterFactory private constructor() : CallAdapter.Factory() {

    override fun get(
        returnType: Type,
        annotations: Array<Annotation>,
        retrofit: Retrofit
    ): CallAdapter<*, *>? {
        // 최상위 타입이 Flow인지 체크
        if (getRawType(returnType) != Flow::class.java) {
            return null
        }

        check(returnType is ParameterizedType) {
            "Flow return type must be parameterized as Flow<Foo> or Flow<out Foo>"
        }

        val responseType = getParameterUpperBound(0, returnType)
        if (getRawType(responseType) != Result::class.java) {
            return null
        }

        check(responseType is ParameterizedType) {
            "Result return type must be parameterized as Result<Foo> or Result<out Foo>"
        }

        return FlowCallAdapter<Any>(
            getParameterUpperBound(
                0,
                responseType
            )
        )
    }

    companion object {
        @JvmStatic
        fun create() = FlowCallAdapterFactory()
    }
}

 

CallAdapter.Factory()는 CallAdapter를 연결하는 과정에서 타입이 올바르게 선언 되었는지를 확인하고, 선언된 타입에 맞는 CallAdapter를 반환하는 역할입니다.

 

 

Retrofit 연결

    @Provides
    @Singleton
    fun provideRetrofit(
        httpClient: OkHttpClient
    ): Retrofit {
        val contentType = "application/json".toMediaType()
        val converterFactory = json.asConverterFactory(contentType)

        return Retrofit.Builder()
            .baseUrl(serverUrl)
            .client(httpClient)
            .addConverterFactory(converterFactory)
            .addCallAdapterFactory(FlowCallAdapterFactory.create()) // 이 부분
            .build()
    }

 

연결도 매우 간단합니다.

Retrofit Builder의 addCallAdapterFactory 메소드로 등록해주면 됩니다.

 

Api 선언 및 호출

 

Api 선언

interface SampleApi {

    @GET("api/v1/sample")
    fun getSampleData(
        @Query("userId") userId: String
    ): FlowResult<SampleResponse>
}

 

Api 인터페이스에선 위와 같이 선언합니다.

단, 주의할 점은 suspend 키워드를 사용하지 않습니다.

 

Flow 자체가 비동기 스트림이므로 suspend가 필요하지 않습니다.

 

 

Api 호출

fun loadSampleData(userId: String) {
    viewModelScope.launch {
        sampleRepository.getSampleData(userId)
            .catch { e ->
                // 예외 처리
            }
            .firstResult(
                onSuccess = { data ->
                    // 성공 처리
                },
                onFailure = { exception ->
                    // 실패 처리 (서버에서 에러를 반환한 경우)
                },
            )
    }
}

 

이제 위와 같이 Api를 호출할 수 있습니다.

 

 

마치며

이번 글에서는 Retrofit의 CallAdapter를 커스텀하여 Api 응답을 Flow<Result<T>> 형태로 변환하는 과정을 정리해 보았습니다.

 

결과적으로 네트워크 레이어에서 예외를 처리하는 흐름이 고정되면서 아래와 같은 큰 장점이 생겼습니다.

  • "어디서 예외가 던져지는지"를 찾아보지 않아도 됨
  • 각 화면에서는 응답에 대한 UI 처리와 로직에만 집중할 수 있음

물론, 단일 Http 요청을 Flow로 감싸는 구조가 정답은 아닙니다.

 

다만, 위 글의 상황처럼 예외 처리 컨벤션을 강하게 가져가고 싶거나 네트워크 예외 및 서버 에러 응답을 한 파이프라인으로 모아서 처리하고 싶은 상황이라면 충분히 고려해볼 만한 선택지라고 생각합니다.