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

CallAdapter를 쓰게 된 배경..!

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

그러나 만약 API호출 시 에러가 발생하거나, 기대하지 않는 응답코드가 올 경우 처리하는 경우 매 호출마다 try-catch 예외 처리 지옥에 빠질 수 있다!!!!

→ 원하는 응답으로 변경하기 위해, 에러핸들링 하기 위해 CallAdapter를 적용했다.




개발하깅🤓

1. CallAdapter

Call → T타입으로 변환해주는 인터페이스, CallAdapter.Factory에 의해 인스턴스가 생성된다.

class NetworkResponseAdapter<T>(
    private val successType: Type,
) : CallAdapter<T, Call<NetworkResponse<T>>> { // 여기서 <앞, 뒤> 에 넣어준 것에 따라

    override fun responseType(): Type = successType

    override fun adapt(call: Call<T>): Call<NetworkResponse<T>> { // in(Call<앞>), out(Call<뒤>)의 타입이 정해짐
        return NetworkResponseCall(call) // 얘는 커스텀으로 구현한 클래스로 아래에 나옴!
    }
}
  • responseType : 어댑터가 HTTP 응답을 객체로 변환할 때, 반환값으로 지정할 타입을 리턴하는 메소드. 예를 들어 Call에 대한 responseType의 반환값은 Repo에 대한 타입이다.
  • adapt : 메소드의 파라미터로 받은 call에게 작업을 위임하는 T타입 인스턴스를 반환하는 메소드


2. CallAdapter.Factory

위 CallAdapter의 인스턴스를 생성하는 팩토리 클래스.

get()의 첫번째 인자 returnType에 서비스 메소드의 리턴 타입이 전달된다.

class NetworkResponseAdapterFactory : CallAdapter.Factory() {
    override fun get(
        returnType: Type,
        annotations: Array<Annotation>,
        retrofit: Retrofit,
    ): CallAdapter<*, *>? {

        // suspend functions wrap the response type in `Call`
				// returnType이 Call로 감싸져 있는지?
        if (Call::class.java != getRawType(returnType)) {
            return null
        }

        // check first that the return type is `ParameterizedType`
				// returnType이 제네릭 인자를 가지는지? Call<NetworkResponse<<Foo>> or Call<NetworkResponse<out Foo>>
        check(returnType is ParameterizedType) {
            "return type must be parameterized as Call<NetworkResponse<<Foo>> or Call<NetworkResponse<out Foo>>"
        }

        // get the response type inside the `Call` type
				// returnType에서 첫번째 제네릭 인자를 얻는다. NetworkResponse<out Foo>
        val responseType = getParameterUpperBound(0, returnType)
        // if the response type is not ApiResponse then we can't handle this type, so we return null
				// 기대한 것처럼 동작하기 위해서는 추출한 제네릭 인자가 내가 만든 NetworkResponse타입이어야함.
        if (getRawType(responseType) != NetworkResponse::class.java) {
            return null
        }

        // the response type is ApiResponse and should be parameterized
				// 제네릭 인자 가지는지 확인 NetworkResponse<Foo> or NetworkResponse<out Foo>
        check(responseType is ParameterizedType) { "Response must be parameterized as NetworkResponse<Foo> or NetworkResponse<out Foo>" }
				// Foo를 얻어서 CallAdapter를 생성한다. 
        val successBodyType = getParameterUpperBound(0, responseType)
        return NetworkResponseAdapter<Any>(successBodyType)
    }
}

팩토리는 세개의 메소드를 가진다.

  • get: 파라미터로 받은 returnType과 동일한 타입을 반환하는 서비스 메서드에 대한 CallAdapter 인스턴스를 반환한다.
  • getParameterUpperBound: type의 index 위치의 제네릭 파라미터에 대한 upper bound type을 반환한다. 예를 들어 getParameterUpperBound(1, Map<String, ? extends Runnable>)은 Runnable Type을 반환한다.
  • getRawType: type의 raw type을 반환한다. (raw type: 제네릭 파라미터가 생략된 타입. List<? extends Runnable>의 raw type은 List를 말한다.)


😲자자, 이게 제일 중요합니다용 여기를 잘 바까야대용!!😲



3. 응답 결과를 NetworkResponse로 감싸기 위해 NetworkResponseCall 생성

: 여기서 NetworkResponseCall의 enqueue()를 호출인자로 받아온 Call<>의 enqueue를 호출하여 이 결과에 따라 wrapping작업을 하게 된다.
→ 바로바로 이 과정에서 에러핸들링을 하는 것!!!! (내가 사용하는 sealed class(여기서는 NetworkResponse)로 래핑하면서 data의 에러핸들링을 내가 원하는 기준으로 쓸 수 있다는것이지용)


보면 아래 작업에서 response가 isSuccessful할 때 말고도 전부 Response.success()로 보내고 있다.
왜냐면 우리는 모든 경우를 NetworkResponse로 래핑해서 사용하고 싶으니깐!!

class NetworkResponseCall<T>(
    private val delegate: Call<T>,
) : Call<NetworkResponse<T>> {

    override fun enqueue(callback: Callback<NetworkResponse<T>>) {
        return delegate.enqueue(object : Callback<T> {
            override fun onResponse(call: Call<T>, response: Response<T>) {
                val body = response.body()
                val error = response.errorBody()

                if (response.isSuccessful) {
                    if (body != null) {
                        if((body as BaseResponse<*>).dataHeader.result == "SUCCESS"){
                            callback.onResponse(
                                this@NetworkResponseCall,
                                Response.success(NetworkResponse.Success(body))
                            )
                        } else {
                            callback.onResponse(
                                this@NetworkResponseCall,
                                Response.success(NetworkResponse.Error(COMMON_RESULT_FAIL_ERROR, "result fail"))
                            )
                        }
                    } else {
                        // Response is successful but the body is null
                        callback.onResponse(
                            this@NetworkResponseCall,
                            Response.success(NetworkResponse.Error(NULL_BODY_ERROR, "null body"))
                        )
                    }
                } else {
                    val errorBody = when {
                        error == null -> null
                        error.contentLength() == 0L -> null
                        else -> try {
                            error
                        } catch (ex: Exception) {
                            null
                        }
                    }
                    if (errorBody != null) {
                        callback.onResponse(
                            this@NetworkResponseCall,
                            Response.success(NetworkResponse.Error(API_ERROR, errorBody.toString()))
                        )
                    } else {
                        callback.onResponse(
                            this@NetworkResponseCall,
                            Response.success(NetworkResponse.Error(UNKNOWN_ERROR,
                                "unknown exception occurred"))
                        )
                    }
                }
            }

            override fun onFailure(call: Call<T>, throwable: Throwable) {
                val networkResponse: NetworkResponse<T> = when (throwable) {
                    is IOException -> NetworkResponse.Error(CONNECTION_ERROR,
                        throwable.message ?: "io exception occurred")
                    else -> NetworkResponse.Error(UNKNOWN_ERROR,
                        throwable.message ?: "unknown exception occurred")
                }
                callback.onResponse(this@NetworkResponseCall, Response.success(networkResponse))
            }
        })
    }

    override fun isExecuted() = delegate.isExecuted

    override fun clone() = NetworkResponseCall(delegate.clone())

    override fun isCanceled() = delegate.isCanceled

    override fun cancel() = delegate.cancel()

    override fun execute(): Response<NetworkResponse<T>> {
        throw UnsupportedOperationException("NetworkResponseCall doesn't support execute")
    }

    override fun request(): Request = delegate.request()

    override fun timeout(): Timeout = delegate.timeout()
}




실제 개발하면서 에러핸들링 된 일.

현재 통신을 하는데에 있어 kotlinx-serialization을 사용중이다. Gson대신 kotlinx-serialization로 갈아탄 이유에서 말했듯이

data class Person(val name: String)

에서 {"nick":"sangeun"}이런식으로 해당 키(name)가 없으면 에러를 뱉고 앱이 죽는다. (디폴트밸류가 있으면 됨)


근데, 위의 예외처리로 인해 앱이 죽지않고 에러를 뱉음~~

Ref. [https://medium.com/shdev/retrofit에-calladapter를-적용하는-법-853652179b5b](https://medium.com/shdev/retrofit%EC%97%90-calladapter%EB%A5%BC-%EC%A0%81%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B2%95-853652179b5b) 

완전히 도움이 되어버림.

좋은 웹페이지 즐겨찾기