[안드로이드] Retrofit API 응답 및 에러 처리

동현·2023년 5월 8일
1
post-thumbnail

올해 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!!)
        }
	}
}

1. CallAdapter란?

CallAdapter는 Retrofit2에서 API 호출 결과를 처리하는 방식을 지정하는 인터페이스이다. API 호출이 성공했거나 에러가 났을 경우 코틀린에서 제공하는 Result 로 감싸기 위해 이를 사용할 것이다.

2. Result Call 만들기

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 호출이 실패했을 때 호출된다.

2-1. onResponse 구현

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.successresponse.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는 다음과 같은 형태로 사용할 수 있다.

2-2. onFailure 구현

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로 감싸서 반환해주었다.

3. CallAdapter 만들기

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을 반환한다.

4. CallAdapterFactory 만들기

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을 반환할 것이다.

5. CallAdapterFactory 적용하기

Retrofit.Builder()
	.client(okHttpClient)
	.baseUrl(BuildConfig.BASE_URL)
	.addCallAdapterFactory(ResultCallAdapterFactory())
	.build()

Retrofit 인스턴스를 만들 때 addCallAdapterFactory 메소드를 통해 CallAdapterFactory를 적용하면 된다.

6. 응용 사례

https://github.com/DongChyeon/Android-Samples/tree/master/ComposeMVVMSample
https://github.com/CMC-12th-Hana/FieldMate-Android

profile
https://github.com/DongChyeon

0개의 댓글