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 |