Retrofit에서의 Call Adapter 도입 배경
Retrofit 정의
Retrofit
은 HTTP API에 대한 직접적인 별도 조작없이 인터페이스를 사용해 쉽게 요청을 보낼 수 있고 쉽게 응답결과를 객체(Object)로 변환해주는 라이브러리이다.
또한, 코틀린을 사용하면, API 호출시 내부적으로 요청이 이루어지기 때문에 따로 콜백을 정의할 필요없이 바로 응답객체를 받을 수 있다.
문제 상황
그러나, API 호출시 에러가 발생하거나, 기대하지 않은 응답 코드가 오는 등의 문제가 생기면 매 호출마다 try-catch 예외처리 지옥
에 빠질 수있다. 우리가 원하는 것은 요청 결과를 Wrapping
하여 API를 호출하는 위치에서 is Success
, is Failure
에 따라 동작을 변경하고자 하는것이다.
기존에는 ViewModel
에서 직접 HTTP API 인터페이스 함수를 호출하고, viewModelScope
내에서 변환해주었다. 매번 API 호출할 때마다 변환해주는 것이 번거로웠기 때문에 처음부터 반환값이 바뀌어서 내려온다면 편할 것이다.
사용방법
따라서 이를 원하는 응답결과로 변경하여 에러 핸들링하기 위해, CallAdapter
를 적용한다. 별도로 등록된 CallAdapter
가 없다면, Retrofit
은 DefaultCallAdapter
를 사용한다.
DefaultCallAdapter
는 대부분의 경우 Call을 변조하지 않는다. (@SkipCallbackExecutor Annotation을 사용한다던가 등) Call을 변조하지 않으면 invoke()함수에서 생성한 OkHttpCall
을 그대로 사용한다.
OkHttpCall
은 enqueue()
를 구현할 때, onResponse
에서 응답 json
을 파싱해 인터페이스 함수의 반환 타입 객체로 무작정 Mapping
하고자 한다.
관련 기초개념
Retrofit에서 받을 수 있는 데이터의 타입
1. Call
2. Response
3. 일반 타입
→ 여기서 Call Adpater
를 커스텀하면, Call
타입을 원하는 타입
으로 바꾸어 받을 수 있다. 이 방법을 이용해 Sealed Class
를 이용한 상태처리를 하는 것
Class: CallAdapter를 적용하기 위한 클래스부터 짚고 넘어가자
1. CallAdapter
Adapts a Call with response type R into the type of T. Instances are created by a factory which is installed into the Retrofit instance.
CallAdpater
는 Call을 T 타입으로 변환해주는 인터페이스로, CallAdpater.Factory
에 의해 Instance
가 생성된다
Type responseType();
T adapt(Call<R> call);
CallAdapter
는 위와 같이 2개의 메소드를 가진다
responseType: Adapter가 HTTP 응답을 객체로 변환할 때, 반환 값으로 지정할 Type을 return하는 메소드.
E.g.) Call<Repo>에 대한 responseType의 반환값은 Repo에 대한 타입이다.
adapt: 메소드의 parameter로 받은 call에게 작업을 위임하는 T 타입 Instance를 반환하는 메소드
2. CallAdpater.Factory
Creates CallAdapter instances based on the return type of the service interface methods.
위 CallAdpater
의 Instance
를 생성하는 Factory 클래스
로, Retrofit
서비스 메소드의 Return type
에 기반한 Instance
로 생성한다.
팩토리의 get
메소드에서 paramter로 받는 returnType
에 서비스 메소드의 리턴타입이 전달된다는 것을 말하는 듯 하다.
CallAdapter.Factory
는 3개의 메소드를 가진다.
public abstract @Nullable CallAdapter<?, ?> get(
Type returnType, Annotation[] annotations, Retrofit retrofit);
protected static Type getParameterUpperBound(int index, ParameterizedType type) {
return Utils.getParameterUpperBound(index, type);
}
protected static Class<?> getRawType(Type type) {
return Utils.getRawType(type);
}
get: parameter로 받은 returnType과 동일한 타입을 반환하는 서비스 메소드에 대한 CallAdpater Instance를 반환
getParamterUpperBound: type의 index위치의 제네릭 parameter에 대한 upper bound type을 반환.
E.g) getParameterUpperBound(1, Map<String, ? extends Runnable)은 **Runnable** Type을 반환
getRawType: type의 raw type을 반환한다. (raw type: 제네릭 paramter가 생략된 타입.
List<? extends Runnable>의 raw type은 List를 말한다
3. Call Interface
Custom CallAdapter
를 만들기 위해서는 Retrofit으로 부터 Call Interface를 implementing해야 한다.
Call Interface의 대부분의 로직은 enqueue
안에 들어있다. 그렇다면 enqueue
메소드란 무엇인가?
Asynchronously send the request and notify callback of its response or if an error occurred talking to the server, creating the request, or processing the response.
따라서 enqueue
메소드를 구현하고, 응답을 확인한 후, 올바른 callback
을 보내야 한다.
enqueue
가 구현해야할 메소드는 2개이다
1.onResponse:
받은 HTTP Response에 대해 호출되고, Response
은 Success일수도, Fail일수도 있다. 따라서, 여기서 Response
가 성공했는지를 확인한 뒤, 이후 만들 Custom Sealed Class
를 반환해야 한다.이 경우에서, parse가 성공하면 ApiError
State로, 실패하면 Unexpected
로 return한다.
Success Response가 아닌 경우, ErrorBody
를 Type
으로 제공하는 예상되는 error data class
로 parse를 시도해야 한다. (we try to parse the error body as the expected error data class we provide as a type)
2. onFailure:
서버와 통신하는 동안 네트워크 예외가 발생하거나, Creating Request/응답을 처리 도중 예기치 않은 예외가 발생할 때 호출된다. 이 경우, Exception
이 IOException
인지를 체크하여, 맞으면 NetworkError
State로, 아닌 경우 Unexpected
로 return 한다
구현
기대하는 방식으로 CallAdapter
를 구현하기 위해선 2가지 작업이 필요하다
1. CallAdapter
의 responseType
을 어떻게 구현할 지
2. 응답 결과를 NetworkResponse
로 어떻게 Wrapping
할 지
따라서 다음과 같은 순서를 거쳐 커스텀한 Call Adapter를 Retrofit에 적용시킬수 있다
1. Custom Call Class
만들기
2. Custom Call Adapter
, Call Adapter Factory
만들기
3. Retrofit Builder
에 추가
4. Retrofit Api Interface
에 suspend keyword
적용
1. Custom Data Sealed Class 만들기
가장 먼저 응답을 Wrapping하는 데이터 클래스부터 정의한다.
sealed class Result<out T : Any> {
data class Success<T : Any>(val body: T?) : Result<T>()
data class Failure(val code: Int, val error: String?) : Result<Nothing>()
data class NetworkError(val exception: IOException) : Result<Nothing>()
data class Unexpected(val t: Throwable?) : Result<Nothing>()
}
Success
: state code == 2xxFailure
: state code ≠ 2xxNetwork Error
: 네트워크 에러 발생Unexpected
: 기타
2-1. Custom CallAdapter 만들기
정의
R
타입의 응답을Call<Result<R>>
로 wrapping해야 하는 경우,CallAdapter<R, Call<Result<R>>>
를 구현하는 클래스를 정의해야한다
class ResultCallAdapter<R: Any>: CallAdapter<R, Call<Result<R>>> {
override fun responseType(): Type {
TODO("Not yet implemented")
}
override fun adapt(call: Call<R>): Call<Result<R>> {
TODO("Not yet implemented")
}
}
예시
class NetworkResponseAdapter<T> (
private val successType: Type,
) : CallAdpater<T, Call<NetworkResponse<T>>> { // 여기서 <앞, 뒤>에 넣어준 것에 따라
override fun responseType(): Type = successType
override fun adpat(call: Call<T>): Call<NetworkResponse<T>> { // in(Call<앞>) out(Call<뒤>)의 타입이 정해짐
return NetworkResponseCall(call) // 얘는 Custom으로 구현한 클래스 아래에서 작성해줄것
}
}
2-2. Custom Factory 만들기
- 내가 작성한 API가
Call<Result<Group>>
을 반환해야 한다면,Adapter
의responseType
은Group
을 반환해야 한다.Call<Result<Group>>
이라는 타입은 팩토리의get
메소드에서 parameter로 받기 때문에 이를 사용해Group
에 대한 타입을 추출할 수 있다.
예시
class Factory : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
// returnType의 rawType이 Call로 감싸져 있는지?
if (getRawType(returnType) != Call::class.java) {
return null
}
// 이후 returnType이 제네릭 인자를 가지는지? Call<Result<<Foo>> or Call<Result<out Foo>>
check(returnType is ParameterizedType) {
"return type must be parameterized as Call<Result<Foo>> or Call<Result<out Foo>>"
}
// returnType에서 1번째 제네릭 인자를 얻는다. Result<out Foo>
val responseType = getParameterUpperBound(0, returnType)
// 기대한것 처럼 동작하기 위해선 추출한 제네릭 인자가 Result 타입이어야 한다.
if (getRawType(responseType) != Result::class.java) {
return null
}
// Result 클래스가 제네릭 인자를 가지는지 확인한다. 제네릭 인자로는 응답을 변환할 클래스를 받아야 한다. Result<Foo> or Result<out Foo>
check(responseType is ParameterizedType) {
"Response must be parameterized as Result<Foo> or Result<out Foo>"
}
// 마지막으로 Result의 제네릭 인자 Foo를 얻어서 CallAdapter를 생성한다.
val successBodyType = getParameterUpperBound(0, responseType)
return ResultCallAdapter<Any>(successBodyType)
}
}
get()
의 1번째 인자 returnType
에 서비스 메소드의 returnType
이 전달된다
이를 통해 팩토리에서 responseType
을 추출하고, 추출한 Type Instance
로 CallAdapter
를 생성하도록 작성되었다.
3. 응답결과를 Wrapping
- CallAdapter의
adapt
메소드는 기존의Call<R>
을 parameter로 받아,Call<Result<R>>
로 wrapping한instance
를 반환한다. 응답결과를Result
로 감싸기 위해,ResultCall
을 생성Call<Result<R>>
을 구현하는CustomCall
을 만들어야 한다
예시
class ResultCall<T : Any>(private val call: Call<T>) : Call<Result<T>> {
override fun clone(): Call<Result<T>> = ResultCall(call.clone())
override fun execute(): Response<Result<T>> {
throw UnsupportedOperationException("ResultCall doesn't support execute")
}
override fun enqueue(callback: Callback<Result<T>>) {
call.enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
callback.onResponse(
this@ResultCall,
Response.success(Result.Success(response.body()))
)
} else {
callback.onResponse(
this@ResultCall,
Response.success(
Result.Failure(
response.code(),
response.errorBody()?.string()
)
)
)
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
val networkResponse = when (t) {
is IOException -> Result.NetworkError(t)
else -> Result.Unexpected(t)
}
callback.onResponse(this@ResultCall, Response.success(networkResponse))
}
})
}
override fun isExecuted(): Boolean = call.isExecuted
override fun cancel() = call.cancel()
override fun isCanceled(): Boolean = call.isCanceled
override fun request(): Request = call.request()
override fun timeout(): Timeout = call.timeout()
}
CustomCall
에서는 동기 처리방식인 execute
를 사용할 일이 없기 때문에, execute
를 사용하면 throw를 던져버리도록 처리한다.
ResultCall
의 핵심은 enqueue
메소드이다. 나머지 메소드는 parameter로 받은 기존의 Call<R>
인스턴스에게 작업을 위임한다.
여기서 ResultCall의 enqueue()를 호출시, 인자로 받아온 Call<>의 enqueue를 호출해, 이 결과에 따라 Wrapping 작업을 하게 된다
→ 이 과정에서 에러 핸들링을 하는 것이다. (내가 사용하는 Sealed Class (여기선 Result)로 Wrapping하면서, Data의 에러 핸들링을 내가 원하는 기준으로 쓸수 있다는 것)
Wrapping된 ResultCall
Instance에서 enqueue
메소드를 호출하면
1. 먼저 paramter로 받은 Call Instance의 enqueue
를 호출해 응답 결과를 얻는다.
2. 이후 응답 결과를 분석해, 정상적으로 성공했는지, 또는 실패했는지, 에러가 발생했는지에 따라 Result
객체를 생성해 Callback에 전달한다.
→ 각각의 경우에 보면 모든 콜백에 Response.success
를 호출하는 것을 볼 수 있다.
response
가 isSuccessful
이 아니라면, 의미상으론 Response.error
를 호출하는 것이 맞을듯 하나, 이미 이에 대해선 위임한 Call Instance에서 처리되고, 반환된 Result
객체에서 타입을 검사해 동작하기 때문에 문제가 없을 것이다.
→ 결국 모든 경우를 Result
로 Wrapping해서 사용하고 싶으니까 그러는 것이다.
다만, Kotlin의 suspend function
을 사용하지 않고, Call<Result<R>>
을 반환받는다면, 모든 status code가 200으로 처리되기 때문에 주의가 필요할것이다. suspend
keyword를 붙여줘야 Call
객체의 enqueue
를 이용해, 비동기 처리를 하는 방식을 열심히 내부적으로 돌려줄 텐데, 붙여주지 않으면 이러한 동작이 사라지기 때문에 문제가 발생하고, 맨 처음 returnType
에서 Call
Type으로 들어오지 않아, 온전한 데이터가 return 되지 않는다
이제 ResultCallAdapter
의 adapt
메소드에서 ResultCall(call)
을 반환하도록 수정하면 Coroutine에서 응답 결과를 Result
로 Wrapping해서 얻을 수 있다.
class NetworkResponseCall<T>(
private val delegate: Call<T>
): Call<NetworkResponse<T>> {
override fun enqueue(callback: Callback<NetworkResponse<T>>) {
return delegate.enqueue(object: Callback<T> {
val body = response.body()
val error = response.errorBody()
if (response.isScucessful) {
if (body != null) {
if ((body as BaseResponse<*>).dataHeader.result == "SUCCES") {
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 occured"))
)
}
}
}
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(UNKNWON_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()
Retrofit Builder
Retrofit.Builder()
.addCallAdapterFactory(NetworkResponseAdapterFactory)
...
addCallAdapterFactory에 관하여
addCallAdapterFactory() paramter로 CallAdapter.Factory 객체를 넣어주면 된다.
이 Factory 객체는 callAdapter를 생성할 수 있는 객체이다. 팩토리 패턴을 따른 것으로, Retrofit과 CallAdapter 사이의 의존성을 약하게 만들어주는 역할을 한다.
그런데 여기서 set이 아닌 add 접두어가 사용된 이유가 있다. 바로, 1개의 Retrofit Instance에는 여러 개의 CallAdapterFactory를 등록할 수 있기 때문이다. Retrofit이 HTTP API 인터페이스 함수를 호출하는 과정에서 등록된 여러개의 CallAdapter.Factory 중에서 적절한 것을 찾고 이를 생성해 Call을 변조시킨다.
그렇다면, Retrofit은 어떻게 적절한 CallAdapter.Factory를 찾을 수 있을까?
HTTP API 인터페이스 함수를 호출할 때 적절한 HttpServiceMethod를 찾아, call 객체를 생성한다. 이 과정에서 HttpServiceMethod를 생성하기 전에 적절한 CallAdapter를 찾는다.
HTTP API 인터페이스 함수의 정보를 담고 있는 메소드로부터 Annotation과 Return Type을 꺼내오고, 이를 바탕으로 createCallAdapter()를 호출해서 적절한 CallAdpater를 찾는다.
해당 함수는 최종적으로 parameter로 주어진 Retrofit 인스턴스의 nextCallAdapter()라는 함수를 호출한다.
해당 Retrofit Instance가 가지고 있는 CallAfapter.Factory 자료구조를 순회하면서 주어진 Return type과 Annotation을 처리할 수 있는 CallAdapter.Factory를 찾는다.
이때 null이 아닌 CallAdapter를 반환하는 CallAdapter.Factory가 있다면, 주어진 Return Type과 Annotation을 처리할 수 있는것으로 판단하고, 해당 CallAdapter를 채택한다.
하지만, 모든 CallAdapter를 살펴봐도 이를 처리할 수 있는 것을 찾지 못한다면 예외가 발생한다. 그러나, Retrofit은 기본적으로 DefaultCallAdapterFactory를 등록해두기 때문에 예외까지 발생하는 경우는 거의 없다.
따라서, ReturnType과 Annotation 만으로 이를 처리할 수 있을지 없을지를 판단하는 함수는 CallAdapter.Factory.get()이다.
- 처리할 수 있으면 CallAdpater를 반환한다
- 처리할 수 없다면 null를 반환한다
순차적으로 순회하기 때문에, 이를 처리할 수 있는 CallAdapter가 2개 이상이라 하더라도, 처음 등록된 것이 채택된다. → addCallAdapterFactory 호출시 순서가 중요하다
관련 라이브러리
찾아보니 이를 구현해 놓은 라이브러리들이 있어서 참고자 올려놓는다.
https://github.com/haroldadmin/NetworkResponseAdapter
https://velog.io/@skydoves/retrofit-api-handling-sandwich
참고
https://medium.com/shdev/retrofit에-calladapter를-적용하는-법-853652179b5b
https://velog.io/@suev72/AndroidRetrofit-Call-adapter
https://velog.io/@ams770/Android-Retrofit-Custom-Call-Adapter
https://mccoy-devloper.tistory.com/58
'IT > Android' 카테고리의 다른 글
[Android/FCM] (2) 플랫폼 별 차이와 Delivery Options알림 (0) | 2023.03.01 |
---|---|
[Android/FCM] (1) Firebase Cloud Messaging의 기초 개념및 구조 (0) | 2023.02.05 |
[Android Dev] RecyclerView의 스크롤 상태 체크 (0) | 2023.01.22 |
[Android Env]Keystore 비밀번호를 잊어버렸을 때 (2) | 2023.01.21 |
[Android] Custom App Bar 사용 - Kotlin (0) | 2021.07.27 |