올해 2~3월에 CMC에서 활동을 진행하면서 FieldMate 라는 앱을 개발하고 출시했다. 개발에 있어 가장 애먹었던 부분이 네트워크 통신 과정에서의 에러 처리였다.
네트워크 통신 과정 중 에러가 나면 서버에서 받는 json 데이터의 형식은 다음과 같았다.
{
"errorCode": "에러코드명",
"message": "에러메시지",
"cause": "에러원인 클래스명"
}
이중 에러메시지를 알림창에 띄워야 하기 때문에 고민했었고 최대한 보일러 플레이트
코드를 줄여서 개발하고자 했다.
이를 위해 해당 글의 에러 처리 방식을 응용했다.
https://blog.canopas.com/retrofit-effective-error-handling-with-kotlin-coroutine-and-result-api-405217e9a73d
최종적인 응답 처리 형태는 다음과 같다.
fun login(id: String, password: String) {
viewModelScope.launch {
loginService.login(id, password).onSuccess {
// TO-DO: 로그인 성공 시 동작 처리
}.onFailure {
showErrorDiaglog(errorMessage = it.message!!)
}
}
}
CallAdapter는 Retrofit2에서 API 호출 결과를 처리하는 방식을 지정하는 인터페이스이다. API 호출이 성공했거나 에러가 났을 경우 코틀린에서 제공하는 Result
로 감싸기 위해 이를 사용할 것이다.
Result
형태로 통신 결과를 반환하기 위해 먼저, Retrofit의 Call interface를 구현해야 한다.
override fun enqueue(callback: Callback<Result<T>>) {
delegate.enqueue(
object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
}
override fun onFailure(call: Call<T>, t: Throwable) {
}
}
)
}
enqueue
메소드는 Retrofit에서 비동기적으로 API 호출을 실행하는 메소드이다. 해당 메소드는 두 가지 Callback 메소드를 가지고 있는데, 하나는 onReponse
, 다른 하나는 onFailure
이다.
onResponse: 서버로부터 성공적인 응답이 돌아왔을 때, 호출된다.
onFailrue: 네트워크 오류가 발생하거나 API 호출이 실패했을 때 호출된다.
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
callback.onResponse(
this@ResultCall,
Response.success(
response.code(),
Result.success(response.body()!!)
)
)
} else {
val gson = Gson()
val errorResponse =
gson.fromJson(response.errorBody()?.string(), ErrorRes::class.java)
callback.onResponse(
this@ResultCall,
Response.success(
Result.failure(
Exception(
errorResponse.message
)
)
)
)
}
}
onResponse의 경우 서버에서 성공적으로 응답이 돌아온 상황이다.
따라서 response.isSuccessful()
을 사용하여 응답이 성공적이면, Result.success
로 response.body()
를 감싸서 반환해주었다.
data class ErrorRes(
@SerializedName("errorCode")
val errorCode: String,
@SerializedName("message")
val message: String,
@SerializedName("cause")
val cause: String
)
반대의 경우 미리 만들어둔 Error 클래스로 파싱 후, Result.failure
로 에러메시지를 감싸서 반환해주었다.
fun login(id: String, password: String) {
viewModelScope.launch {
loginService.login(id, password).onSuccess {
// TO-DO: 로그인 성공 시 동작 처리
}.onFailure {
showErrorDiaglog(errorMessage = it.message!!)
}
}
}
이 때 받은 Result.failure(Excpetion(errorResponse.message)
의 errorMessage는 다음과 같은 형태로 사용할 수 있다.
override fun onFailure(call: Call<T>, t: Throwable) {
val errorMessage = when (t) {
is IOException -> NETWORK_CONNECTION_ERROR_MESSAGE
else -> t.localizedMessage
}
callback.onResponse(
this@ResultCall,
Response.success(Result.failure(Exception(errorMessage)))
)
}
onFailure의 경우 네트워크 오류가 나거나 API 응답을 성공적으로 받지 못한 상황이다. 실패한 경우 예외를 throw하지 않고 Result
의 onFailure 콜백 메소드를 통해 처리할 것이기 때문에 Response.success
로 감싸서 반환해주었다.
CallAdapter에서 두 가지 메소드를 구현해야 한다.
responseType: CallAdpater가 처리하는 API 호출의 응답 유형을 반환
adapt: Call 객체를 특정 유형으로 변환하여 반환 (일반적으로responseType
에서 반환한 유형과 일치하도록 변환 작업 수행)
val upperBound = getParameterUpperBound(0, returnType)
>
return if (upperBound is ParameterizedType && upperBound.rawType == Result::class.java) {
object : CallAdapter<Any, Call<Result<*>>> {
override fun responseType(): Type = getParameterUpperBound(0, upperBound)
override fun adapt(call: Call<Any>): Call<Result<*>> =
ResultCall(call) as Call<Result<*>>
}
} else {
null
}
반환 타입에 대한 상한 타입을 추출하고, 이를 기반으로 적절한 CallAdapter
를 생성하기 위해 getParameterUpperBound
메소드를 사용한다. 이를 통해 API 호출 결과를 다양한 유형으로 변환하고 처리할 수 있다.
해당 CallAdpapter는 API 호출 결과가 Result
인지 판단하고 아닐 경우 null을 반환한다.
CallAdpaterFactory를 만들기 위해 get
메소드를 구현해야 한다.
CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit)
get 메소드는 다음과 같은 형태로 매개변수값의 의미는 다음과 같다.
returnType: API 호출의 반환 타입
annotations: API 메소드에 적용된 어노테이션 배열
retrofit: Retrofit 인스턴스
주어진 반환 타입과 API 메소드에 적용된 어노테이션을 기반으로, 적절한 CallAdapter를 생성 후 반환한다.
class ResultCallAdapterFactory : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
if (getRawType(returnType) != Call::class.java || returnType !is ParameterizedType) {
return null
}
val upperBound = getParameterUpperBound(0, returnType)
return if (upperBound is ParameterizedType && upperBound.rawType == Result::class.java) {
object : CallAdapter<Any, Call<Result<*>>> {
override fun responseType(): Type = getParameterUpperBound(0, upperBound)
override fun adapt(call: Call<Any>): Call<Result<*>> =
ResultCall(call) as Call<Result<*>>
}
} else {
null
}
}
}
해당 ReusultCallAdapterFactory에서 get은 반환타입이 Result
클래스인지 체크하고 아니면 null을 반환할 것이다.
Retrofit.Builder()
.client(okHttpClient)
.baseUrl(BuildConfig.BASE_URL)
.addCallAdapterFactory(ResultCallAdapterFactory())
.build()
Retrofit 인스턴스를 만들 때 addCallAdapterFactory 메소드를 통해 CallAdapterFactory를 적용하면 된다.
https://github.com/DongChyeon/Android-Samples/tree/master/ComposeMVVMSample
https://github.com/CMC-12th-Hana/FieldMate-Android