응답 에러를 wrapping 하기
앞서 소개한 Retrofit suspend 함수는 HTTP status code 혹은 각종 조건에 따라서 에러를 throw 하고 있다. 따라서 해당 함수를 사용하기 위해서는 try-catch 문 등으로 에러를 잡아줘야한다. 혹시 실수로 try-catch 를 사용하지 않을 경우, 앱이 비정상 종료될 수 있다. 조금 더 안전하게 에러를 wrapping 해서 반환받을 수는 없을까 ?
확장함수 사용
앞서 Retrofit suspend 함수는 내부적으로 Call.await() 를 호출해서 enqueue()를 실행하고 있음을 알게 되었다. 이 await() 함수를 사용해서 다음과 같이 확장함수를 만들 수 있다.
interface GitHubService {
@GET("users/{user}/repos")
fun listRepos(@Path("user") user: String): Call<List<Repo>>
}
// 성공 시 데이터, 실패 시 에러를 wrapping 하고 있는 클래스
sealed class ApiResponse<out T> {
data class Success<T>(val data: T): ApiResponse<T>()
data class Failure(val error: Throwable): ApiResponse<Nothing>()
}
// Call -> ApiResponse
suspend fun <T : Any> Call<T>.toApiResponse(): ApiResponse<T> {
return try {
ApiResponse.Success(await())
} catch (t: Throwable) {
ApiResponse.Failure(t)
}
}
// ViewModel
viewModelScope.launch {
when (val response = gitHubService.listRepos("winter223").toApiResponse()) {
is ApiResponse.Success -> {
val listRepos = response.data
// Ui 작업
}
is ApiResponse.Failure -> {
when (response.error) {
is HttpException -> {
// 에러 응답 코드에 따른 동작 처리
}
else -> {
// 네트워크 에러이거나 API 응답이 잘못된 경우가 많음
// UnexpectedError
}
}
}
}
}
이렇게 하면 toApiResponse() 를 실행할 때 Retrofit suspend 함수를 호출할 때와 마찬가지로, 내부적으로 enqueue()를 실행한다. 그리고 발생하는 에러를 잡아 ApiResponse.Failure 로 감싸서 반환한다. 따라서 해당 함수를 호출할 때 try-catch 문으로 감싸지 않아도 된다. 타입 검사 때문에 코드량은 조금 늘어난 것 같지만 실수로 앱을 종료시키는 일은 없을 것이다. (타입 검사 또한 ApiResponse 내에 유틸 함수를 추가해서 제거할 수 있다.)
CallAdapter 사용
Call 을 변조하지 않는 경우의 플로우
위의 예제에선 ViewModel 에서 직접 HTTP API 인터페이스 함수를 호출하고, viewModelScope 내에서 toApiResponse()로 변환해주었다. 매번 API를 호출할 때마다 toApiResponse()를 호출하는 것이 꽤 번거로울 수도 있다. Repository 를 사용한다면 그 안에서 변환 작업을 할 수 있겠지만, 진작에 반환 값으로 ApiResponse 가 내려오면 훨씬 편할 것이다.
그렇게 하기 위해서는 다음과 같이 suspend 키워드와 ApiResponse<T> 반환타입을 사용해서 인터페이스 함수를 정의해야한다.
interface GitHubService {
@GET("users/{user}/repos")
suspend fun listRepos(@Path("user") user: String): ApiResponse<List<Repo>>
}
그럼 지난 포스팅에서 알아봤듯이 내부적으로 Call 로 감싸진 다음, Call.await() 로 enqueue() 가 실행될 것이라 기대된다.
//HttpServiceMethod
@Override
final @Nullable ReturnT invoke(Object[] args) {
// Call로 wrapping
Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
// adapt() 호출
return adapt(call, args);
}
//HttpServiceMethod.SuspendForBody
static final class SuspendForBody<ResponseT> extends HttpServiceMethod<ResponseT, Object> {
private final CallAdapter<ResponseT, Call<ResponseT>> callAdapter;
...
@Override
protected Object adapt(Call<ResponseT> call, Object[] args) {
// callAdapter 를 통해 주어진 call 변조
call = callAdapter.adapt(call);
Continuation<ResponseT> continuation = (Continuation<ResponseT>) args[args.length - 1];
// Call enqueue
try {
return isNullable
? KotlinExtensions.awaitNullable(call, continuation)
: KotlinExtensions.await(call, continuation);
} catch (Exception e) {
return KotlinExtensions.suspendAndThrow(e, continuation);
}
}
}
그러나 야속하게도, ResponseBody 를 ApiResponse() 에 매핑하는 과정에서 ApiResponse()를 생성할 수 없다는 에러와 함께 앱이 종료된다.
이유는 다음과 같다.
(콜스택이 많이 모든 코드를 첨부할 수 없어서 간략하게 나타내었다. 궁금하다면 위 에러를 일으킨 다음, 직접 콜스택을 따라가보자.)
1. 별도로 등록된 CallAdapter 가 없다면, Retrofit 은 DefaultCallAdapter 를 사용한다.
2. DefaultCallAdapter 는 대부분의 경우 Call 을 변조하지 않는다. (@SkipCallbackExecutor 어노테이션을 사용한다던가 등)
3. Call 을 변조하지 않으면, invoke() 함수에서 생성한 OkHttpCall 을 그대로 사용한다.
4. OkHttpCall 은 enqueue()를 구현할 때, onResponse 에서 응답 json 을 파싱해서 인터페이스 함수의 반환 타입 객체로 무작정 매핑하고자 한다.
5. ApiResponse 는 sealed 클래스이므로, abstract 하다. 따라서 인스턴스화 할 수 없기 때문에 에러가 발생한다.
6. 이는 try-catch 문으로 잡혀서 Callback.onFailure()를 호출한다.
7. Call.await() 에서는 위 OkHttpCall 의 enqueue()를 호출하기 때문에 onFailure()를 타고, resumeWithException() 가 실행되어 앱이 종료되는 것이다.
4 에 해당하는 OkHttpCall 의 enqueue() 부분을 살펴보면 다음과 같다.
// OkHttpCall.java
@Override
public void enqueue(final Callback<T> callback) {
//..
call.enqueue(
// Retrofit.Callback 이 아닌, OkHttp3.Callback 을 사용
new okhttp3.Callback() {
@Override
// Retrofit.Response 타입이 아닌 OkHttp3.Response 타입이 제공된다.
// 해당 타입은 rawResponse 이므로, 직접 파싱해야한다.
public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse) {
Response<T> response;
try {
// 응답값 파싱 후 반환 타입으로 매핑
response = parseResponse(rawResponse);
// ApiResponse 는 인스턴스화 불가 -> 에러 캐치
} catch (Throwable e) {
throwIfFatal(e);
// callFailure() 호출
callFailure(e);
return;
}
try {
callback.onResponse(OkHttpCall.this, response);
} catch (Throwable t) {
throwIfFatal(t);
t.printStackTrace(); // TODO this is not great
}
}
@Override
public void onFailure(okhttp3.Call call, IOException e) {
callFailure(e);
}
// 실패한 경우, Callback.onFailure() 호출
private void callFailure(Throwable e) {
try {
callback.onFailure(OkHttpCall.this, e);
} catch (Throwable t) {
throwIfFatal(t);
t.printStackTrace();
}
}
});
}
보다시피 OkHttp3.Callback 을 사용하고 있기 때문에 rawResponse 를 받아서 직접 파싱하고 매핑하며, 이 과정에서 에러가 발생하면 onFailure() 로 처리한다. 참고로 Retrofit.Callback 은 Retrofit.Response 타입으로 받기 때문에 이미 기본 데이터 타입으로 파싱 및 매핑되어 전달된다.
결론을 말하자면 OkHttp3.Call 은 ApiResponse 에 응답을 매핑할 줄 모른다. 따라서 우리가 저 enqueue() 를 재작성해서 ApiResponse 로 직접 매핑해줘야한다. 그걸 할 수 있는 게 바로 CallAdapter이다.
CallAdapter와 CallAdapterFactory 작성하기
위에서 Call.await() 를 호출하기 전, CallAdapter.adapt() 함수로 Call 을 변조하는 것을 알 수 있었다. 이 CallAdapter 는 어디서 생성되어 온 것일까 ? 바로 Retrofit 을 생성하는 과정에서, 개발자가 직접 구현해서 적용할 수 있다. 이를 적용하기 위해서는 Retrofit.Builder() 가 제공하는 addConverterFactory() 라는 함수를 사용하면 된다.
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(/**CallAdapter.Factory**/)
.build()
addConverterFactory() 의 파라미터로 CallAdapter.Factory 객체를 넣어주면 된다. 이 팩토리 객체는 CallAdapter 를 생성할 수 있는 는 객체이다. 팩토리 패턴을 따른 것으로, Retrofit 과 CallAdapter 사이의 의존성을 약하게 만들어주는 역할을 한다. 어떤식으로 의존성을 약하게 만드는지는 아래에서 조금 더 알아보자.
그런데 setCallAdapterFactory() 와 같이 set 이 아닌 add 접두어를 사용한 이유가 뭘까? 그렇다. 하나의 Retrofit 인스턴스에는 여러 개의 CallAdapterFactory 를 등록할 수 있다. Retrofit 이 HTTP API 인터페이스 함수를 호출하는 과정에서 등록된 여러 개의 CallAdapter.Factory 중에서 적절한 것을 찾고, 이를 생성해 Call 을 변조시킨다. Retrofit 은 어떻게 적절한 CallAdapter.Factory 를 찾을 수 있을까 ?
지난 포스팅에서 HTTP API 인터페이스 함수를 호출할 때 적절한 HttpServiceMethod 를 찾아 Call 객체를 생성한다는 것을 살펴보았다. 이 과정에서 HttpServiceMethod 를 생성하기 전에 적절한 CallAdapter 를 찾는다.
static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
Retrofit retrofit, Method method, RequestFactory requestFactory) {
boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction;
// 리턴 타입 및 어노테이션 정보 추출 과정
Annotation[] annotations = method.getAnnotations();
Type adapterType;
if (isKotlinSuspendFunction) {
Type[] parameterTypes = method.getGenericParameterTypes();
Type responseType =
Utils.getParameterLowerBound(
0, (ParameterizedType) parameterTypes[parameterTypes.length - 1]);
adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
annotations = SkipCallbackExecutorImpl.ensurePresent(annotations);
} else {
adapterType = method.getGenericReturnType();
}
// 리턴타입과 어노테이션 등으로 CallAdapter 찾기
CallAdapter<ResponseT, ReturnT> callAdapter =
createCallAdapter(retrofit, method, adapterType, annotations);
// ... HttpServiceMethod 생성 및 반환
}
HTTP API 인터페이스 함수의 정보를 담고 있는 Method 로 부터 어노테이션과 리턴 타입을 꺼내오고, 이를 바탕으로 createCallAdapter() 를 호출해서 적절한 CallAdapter 를 찾고 있다. 해당 함수는 최종적으로 파라미터로 주어진 Retrofit 인스턴스의 nextCallAdapter() 라는 함수를 호출하는데, 구현부는 다음과 같다.
//Retrofit.java
public CallAdapter<?, ?> nextCallAdapter(
@Nullable CallAdapter.Factory skipPast, Type returnType, Annotation[] annotations) {
int start = callAdapterFactories.indexOf(skipPast) + 1;
for (int i = start, count = callAdapterFactories.size(); i < count; i++) {
// CallAdater.Factory.get() 으로
// 해당 리턴 타입과 어노테이션을 처리할 수 있는지 판단한다.
CallAdapter<?, ?> adapter = callAdapterFactories.get(i).get(returnType, annotations, this);
if (adapter != null) {
return adapter;
}
}
throw new IllegalArgumentException(/**찾지 못했다는 에러메시지**/);
}
해당 Retrofit 인스턴스가 가지고 있는 CallAdapter.Factory 자료구조를 순회하면서 주어진 리턴 타입과 어노테이션을 처리할 수 있는 CallAdapter.Factory 를 찾는다. 이때 null 이 아닌 CallAdapter 를 반환하는 CallAdapter.Factory 가 있다면, 주어진 리턴 타입과 어노테이션을 처리할 수 있는 것으로 판단하고 해당 CallAdapter 를 채택한다. 하지만 등록된 모든 CallAdapter 를 살펴봤지만 이를 처리할 수 있는 것을 찾지 못했다면 예외가 발생한다. 그러나 아래에서 살펴보겠지만, Retrofit 은 위에서 잠깐 언급한 DefaultCallAdapterFactory 를 기본적으로 등록해두어서 예외까지 발생하는 경우는 거의 없다.
아무쪼록 여기서 알 수 있는 것은 다음과 같다.
- 리턴 타입과 어노테이션만으로 이를 처리할 수 있을지 없을지를 판단하는 함수는 CallAdapter.Factory.get() 이다.
- 처리할 수 있으면 CallAdapter 를 반환한다.
- 처리할 수 없다면 null 을 반환한다.
- 순차적으로 순회하기 때문에, 이를 처리할 수 있는 CallAdapter 가 두 개 이상이라 하더라도, 처음에 등록된 것이 채택된다. 즉, addCallAdapterFactory() 를 호출하는 순서가 중요하다.
그럼 CallAdapter.Factory 가 어떻게 구성되어 있는지 살펴보자.
abstract class Factory {
/**
* Returns a call adapter for interface methods that return {@code returnType}, or null if it
* cannot be handled by this factory.
*/
public abstract @Nullable CallAdapter<?, ?> get(
Type returnType, Annotation[] annotations, Retrofit retrofit);
/**
* Extract the upper bound of the generic parameter at {@code index} from {@code type}. For
* example, index 1 of {@code Map<String, ? extends Runnable>} returns {@code Runnable}.
*/
protected static Type getParameterUpperBound(int index, ParameterizedType type) {
return Utils.getParameterUpperBound(index, type);
}
/**
* Extract the raw class type from {@code type}. For example, the type representing {@code
* List<? extends Runnable>} returns {@code List.class}.
*/
protected static Class<?> getRawType(Type type) {
return Utils.getRawType(type);
}
}
위에서 살펴본 get() 함수만이 필수로 구현해야할 추상 메서드이다. 주석에도 적혀있듯이 주어진 인터페이스 함수를 처리할 수 있는 CallAdapter 를 반환하고, 해당 팩토리로 처리할 수 없다면 null 을 반환해야한다. 즉, "저는 이러이러한 리턴 타입과 어노테이션을 처리할 수 있는 CallAdapter 를 생성합니다." 라는 것을 명확하게 나타내야한다. 그 외 protected 함수들은 주어진 리턴 타입을 분석하는데 사용할 수 있는 유틸 함수들로 이루어져있다.
그럼 Retrofit 이 기본적으로 사용하고 있는 DefaultCallAdapterFactory 는 이를 어떻게 구현하고 있을까 ?
final class DefaultCallAdapterFactory extends CallAdapter.Factory {
private final @Nullable Executor callbackExecutor;
DefaultCallAdapterFactory(@Nullable Executor callbackExecutor) {
this.callbackExecutor = callbackExecutor;
}
@Override
public @Nullable CallAdapter<?, ?> get(
Type returnType, Annotation[] annotations, Retrofit retrofit) {
// 리턴 타입이 Call 이어야 함
if (getRawType(returnType) != Call.class) {
return null;
}
// Call 이 제네릭 타입이어야 함
if (!(returnType instanceof ParameterizedType)) {
throw new IllegalArgumentException(
"Call return type must be parameterized as Call<Foo> or Call<? extends Foo>");
}
// 제네릭 타입 추출
final Type responseType = Utils.getParameterUpperBound(0, (ParameterizedType) returnType);
// SkipCallbackExecutor 어노테이션이 붙었다면 Executor 를 사용하지 않음
// 없다면 생성자로 전달받은 callbackExcecutor 사용 (nullable)
final Executor executor =
Utils.isAnnotationPresent(annotations, SkipCallbackExecutor.class)
? null
: callbackExecutor;
// CallAdapter 생성
return new CallAdapter<Object, Call<?>>() {
@Override
public Type responseType() {
return responseType;
}
@Override
public Call<Object> adapt(Call<Object> call) {
// Executor 를 사용하지 않는다면 Call 을 변조하지 않고 그대로 반환
// 사용한다면 Call 을 재작성하는 ExecutorCallBackCall 을 사용
return executor == null ? call : new ExecutorCallbackCall<>(executor, call);
}
};
}
반환 타입 Call<T> or Call<out T> 라면 모두 처리할 수 있다고 주장하고 있다. 이러니 어지간하면 처리할 CallAdapterFactory 를 찾지 못하는 일이 없다는 것이다.
이제 이걸 참고해서 ApiResponse 타입을 처리할 수 있는 CallAdapterFactory 를 만들어보자.
class ApiResponseCallAdapterFactory : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
// Call<ApiResponse<T>> 형태여야하기 때문에, Call 타입이어야 함
if (getRawType(returnType) != Call::class.java) {
return null
}
// 제네릭 타입이어야 함
if (returnType !is ParameterizedType) {
return null
}
// 제네릭 타입 추출
val responseType = getParameterUpperBound(0, returnType)
// 추출한 제네릭 인자가 ApiResponse 타입이어야 한다.
if (getRawType(responseType) != ApiResponse::class.java) {
return null
}
// ApiResponse 또한 제네릭 타입이어야 한다.
if (responseType !is ParameterizedType) {
return null
}
return ApiResponseCallAdapter()
}
위에서 언급했듯이, invoke() 호출 시 Call 로 wrapping 되기 때문에 Call<ApiResponse<T>> 타입을 처리할 수 있다는 조건문으로 부터 시작해서 제네릭 타입까지 마친 후에, 우리가 생각하는 인터페이스 함수가 맞다면 비로소 ApiResponseCallAdapter 를 반환한다.
그럼 CallAdapter 는 어떻게 구성되어 있을까 ?
public interface CallAdapter<R, T> {
/**
* Returns the value type that this adapter uses when converting the HTTP response body to a Java
* object. For example, the response type for {@code Call<Repo>} is {@code Repo}. This type is
* used to prepare the {@code call} passed to {@code #adapt}.
*
* <p>Note: This is typically not the same type as the {@code returnType} provided to this call
* adapter's factory.
*/
Type responseType();
/**
* Returns an instance of {@code T} which delegates to {@code call}.
*
* <p>For example, given an instance for a hypothetical utility, {@code Async}, this instance
* would return a new {@code Async<R>} which invoked {@code call} when run.
*
* @Override
* public <R> Async<R> adapt(final Call<R> call) {
* return Async.create(new Callable<Response<R>>() {
* @Override
* public Response<R> call() throws Exception {
* return call.execute();
* }
* });
* }
*/
T adapt(Call<R> call);
adapt() 는 주어진 Call 을 변조해서 원하는 타입으로 반환할 수 있다는 것은 계속 언급되어 와서 알 것이다. 그 외의 responseType() 은 주석에 쓰여진대로 실질적으로 변환된 Response body 를 반환해주어야한다. 이는 단순하게, Retrofit 이 지원하지 않는 타입이냐 아니냐를 검사하는 정도에만 사용되는듯하다. 이는 CallAdapterFactory 의 get 에서 타입을 추출하고 있기 때문에 이를 CallAdapter 로 전달하면 될 것이다.
이런 요구사항에 맞워서 ApiResponseCallAdapter 를 작성하면 다음과 같다.
class ApiResponseCallAdapterFactory : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
//...
// 마지막으로 ApiResponse 의 제네릭 타입을 꺼낸다.
val actualResponseType = getParameterUpperBound(0, responseType)
return ApiResponseCallAdapter(actualResponseType)
}
class ApiResponseCallAdapter(
private val returnType: Type
) : CallAdapter<R, Call<ApiResponse<R>>> {
override fun responseType(): Type {
return returnType
}
// Call -> ApiResponseCall 로 변조
override fun adapt(call: Call<R>): Call<ApiResponse<R>> = ApiResponseCall(call)
}
// OkHttp3.Call 을 대체하게 될 Call
class ApiResponseCall<T : Any>(private val delegate: Call<T>) : Call<ApiResponse<T>> {
// Call.await() 와 동일한 구조
override fun enqueue(callback: Callback<ApiResponse<T>>) {
delegate.enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
val body = response.body()
if (body == null) {
val invocation = call.request().tag(Invocation::class.java)!!
val method = invocation.method()
val e = KotlinNullPointerException(
"Response from " +
method.declaringClass.name +
'.' +
method.name +
" was null but response body type was declared as non-null"
)
callback.onResponse(
this@ApiResponseCall,
Response.success(ApiResponse.Failure(e))
)
} else {
callback.onResponse(
this@ApiResponseCall,
Response.success(ApiResponse.Success(body))
)
}
} else {
callback.onResponse(
this@ApiResponseCall,
Response.success(ApiResponse.Failure(HttpException(response)))
)
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
callback.onResponse(
this@ApiResponseCall,
Response.success(ApiResponse.Failure(t))
)
}
})
}
override fun isExecuted(): Boolean {
return delegate.isExecuted
}
// 동기 호출을 지원하지 않음
override fun execute(): Response<ApiResponse<T>>? {
throw UnsupportedOperationException("ApiResponseCall doesn't support execute")
}
override fun cancel() {
delegate.cancel()
}
override fun isCanceled(): Boolean = delegate.isCanceled
override fun clone(): Call<ApiResponse<T>> = ApiResponseCall(delegate.clone())
override fun request(): Request = delegate.request()
override fun timeout(): Timeout = delegate.timeout()
}
이렇게 무식하게 파싱 및 매핑하는 OkHttp3.Call 을 대체할 수 있는 ApiResponseCall 을 만들어서 CallAdapter.adapt() 가 이를 반환할 수 있도록 했다. ApiResponseCall 은 OkHttp3.Call 과 마찬가지로 Call 을 상속하고 있으며, enqueue() 및 각종 함수들을 재작성한다. 이는 주어진 Call 을 OkHttp3.Callback 이 아닌 Retrofit.Callback 으로 받으면서, 이미 기본 데이터 타입으로 매핑된 응답을 상황에 따라 적절하게 ApiResponse로 직접 매핑해주었다.
특이한 점은 성공이든 실패든 callback.onResponse() 를 호출하고, Response.success 로 ApiResponse 를 감싸고 있다는 것이다. 왜냐하면 이는 결국엔 또다시 Call.await() 확장함수를 거칠 것이기 때문이다. 이렇게 해야만 Call.await() 가 사용하는 콜백의 onResponse 가 호출될 것이고, isSuccessful 조건문 안에서, 우리가 반환한 값을 그대로 내보낼 것이다.
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(ApiResponseCallAdapterFactory())
.build()
.create(GithubService::class.java)
이제 이렇게 CallAdapterFactory 를 등록해두면, HTTP API 인터페이스 함수의 반환 타입을 ApiResponse<T> 로 두어도 알아서 결과 값이 wrapping 되어 반환된다.
마치며
두 개의 포스팅에 걸쳐 Retrofit 의 Call 에 대해서 알아봤다. 알아본 내용을 요약하자면 다음과 같다.
별도 라이브러리 없이 Call과 enqeueue()를 그대로 사용하지 말자.
- 연쇄, 병렬 호출에 취약하다.
- 에러 처리가 번거롭다.
- 확장함수나 suspend 함수를 사용하자.
개발자가 Call 을 직접 사용하지 않더라도 Retrofit 은 HTTP API 인터페이스 함수를 호출할 때 내부적으로 Call 을 사용한다.
- HttpServiceMothod.invoke() - > OkHttp3.Call 로 wrapping -> HttpServiceMethod.adapt() -> CallAdapter 로 Call 변조 -> Call.await() 로 enqueue()
Retrofit suspend 함수를 사용한다면, Coroutine Context Switching 이 불필요하다.
- 내부적으로 enqueue() 를 사용하기 때문에 백그라운드 스레드에서 실행된다.
에러 캐치가 번거롭다면, seald class 를 통해 성공 및 에러에 대한 결과를 wrapping 한 객체로 변환할 수 있다.
- 확장함수나 CallAdapter 를 사용하자.
CallAdapter/CallAdapterFactory 를 활용하자
- OkHttp3.Call 은 무식하다. 등록된 ResponseBodyConverter 를 사용해 무작정 매핑하려 든다.
- 반환받고 싶은 타입이 기본 데이터타입이 아니고 복잡한 것이라면 (seald class, Flow, Observable ..) CallAdapter 를 고려하자.
- CallAdapterFactory 는 하나 이상 등록할 수 있다.
- get() 함수는 어떤 인터페이스 함수를 처리할 수 있는지 명확히 나타내는 함수이다.
'Android' 카테고리의 다른 글
Retrofit Call, 제대로 사용하기 - (1) (0) | 2022.04.01 |
---|