retrofit은 HTTP API를 별도 조작 없이 쉽게 응답을 객체로 변환해주는 라이브러리이다. 코틀린을 사용한다면 API 호출 시 내부적으로 요청이 이루어져서 따로 콜백을 정의할 필요없이 응답객체를 받을 수 있다.
그러나 만약 API호출 시 에러가 발생하거나, 기대하지 않는 응답코드가 올 경우 처리하는 경우 매 호출마다 try-catch 예외 처리 지옥에 빠질 수 있다!!!!
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) // 얘는 커스텀으로 구현한 클래스로 아래에 나옴!
}
}
위 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)
}
}
팩토리는 세개의 메소드를 가진다.
: 여기서 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)
완전히 도움이 되어버림.