
현재 진행 중인 사이드 프로젝트에선 싱글 모듈 -> 멀티 모듈로 변경하는 작업을 하고 있습니다.
모듈 분리와 함께 코드 리팩토링도 진행 중인데, Retrofit의 CallAdapter를 커스텀하는 부분을 작성하고자 합니다.
아래와 같은 이유로 네트워크 요청 부분의 코드를 가장 먼저 수정하고 싶었습니다.
- LiveData 사용 중
- LiveData도 정말 유용하지만 Flow의 연산자의 활용도가 너무 높기에 Flow를 사용하고 싶었습니다.
- runCatching을 이용한 예외 처리
- 현재 사이드 프로젝트에서는 Repository에서 `runCatching`을 이용하여 네트워크 요청 시 예외를 처리하고 있습니다.
- 하지만 서버의 상태 코드에 따라 분기해야 할 때, 400번대 오류도 `onSuccess`로 처리되는 부분이 마음에 들지 않았습니다.
- 통신에는 성공 했으니 onSuccess로 오는 것이 맞긴하지만, 이는 결국 예외에 대한 처리이므로 유연하지 못하다고 느꼈습니다.
- 따라서 onSuccess / onFailure의 역할을 아래와 같이 정의하고자 하였습니다.
- onSuccess : 통신 성공, 에러 미발생
- onFailure : 통신 성공, 에러 발생
- 통신 실패시 : exceptionHandler에서 처리
- 통신 성공 시에도 데이터가 null인지 체크가 필요
- 서버에서 내려주는 응답 데이터가 누락될 경우를 대비하기 위해 모두 nullable로 받고 있습니다.
- 이로 인해, 아래와 같이 통신에 성공 했을 때도 데이터가 누락되지 않았는지 확인이 필요했습니다.
courseRepository.getMarathonCourse()
.onSuccess { response ->
if (response == null) {
// 에러 처리
}
}
위와 같이 여러 단점을 해결하고자 Retrofit의 CallAdapter를 커스텀하기로 결정하였습니다.
이번 리팩토링의 최종 목표는 아래와 같습니다.
목표
- 네트워크 요청 코드 간소화 (+ ViewModel에서 Flow 연산자 사용)
- onSuccess, onFailure로 구분 (+ 응답 데이터가 null인 경우 Failure로 처리)
- exceptionHandler를 이용한 공통 예외 처리
- flow의 catch를 이용한 커스텀 예외 처리
ApiResult 선언
Retrofit의 응답을 Success, Failure로 구분해서 받기 위한 클래스입니다.
ApiResult는 Retrofit의 응답을 받아 Domain 모듈의 Result로 변환해주는 역할을 합니다.
sealed class ApiResult<out R> {
data class Success<R>(val body: R) : ApiResult<R>()
data class Failure(
val code: Int,
val message: String?,
) : ApiResult<Nothing>()
// ApiResult<BaseResponse> 형태를 DomainResult<BaseModel> 형태로 매핑
fun <D> mapToResult(mapper: (R) -> D): Result<D> = when (this@ApiResult) {
is Success -> Result.Success(mapper(body))
is Failure -> Result.Failure(code, message)
}
}
fun <R, D> Flow<ApiResult<R>>.mapToResult(mapper: (R) -> D): Flow<Result<D>> {
return map { it.mapToResult(mapper) }
}
FlowApiResultCallAdapter 선언
전체 코드
class FlowApiResultCallAdapter<T>(
private val responseType: Type
) : CallAdapter<T, Flow<ApiResult<T>>> {
private val gson = Gson()
override fun responseType() = responseType
// Retrofit의 Call을 Flow<>로 변환
override fun adapt(call: Call<T>): Flow<ApiResult<T>> = flow {
val apiResult = suspendCancellableCoroutine { continuation ->
call.enqueue(object : Callback<T> {
override fun onFailure(call: Call<T>, t: Throwable) {
// 예외 발생시키고 Coroutine 종료
continuation.resumeWithException(t)
}
override fun onResponse(call: Call<T>, response: Response<T>) {
val result = if (response.isSuccessful) {
response.body()?.let {
ApiResult.Success(it)
} ?: ApiResult.Failure(code = response.code(), message = "Response body is null")
} else {
parseErrorResponse(response)
}
continuation.resume(result)
}
})
// Coroutine이 취소 되면 네트워크 요청도 취소
continuation.invokeOnCancellation {
call.cancel()
}
}
emit(apiResult)
}
private fun parseErrorResponse(response: Response<*>): ApiResult.Failure {
val errorJson = response.errorBody()?.string()
return runCatching {
val errorBody = gson.fromJson(errorJson, ErrorResponse::class.java)
val message = errorBody?.run {
message ?: error ?: "알 수 없는 에러가 발생하였습니다."
}
ApiResult.Failure(
code = errorBody.status,
message = message
)
}.getOrElse {
ApiResult.Failure(
code = response.code(),
message = "알 수 없는 에러가 발생하였습니다."
)
}
}
}
우선 CallAdapter를 구현 해주었습니다.
해당 CallAdapter를 이용하면 Retrofit의 응답을 Flow<ApiResult<T>> 형태로 받을 수 있습니다.
FlowApiResultCallAdapter에서 중요한 부분은 아래와 같습니다.
1. 응답 데이터 null 처리
- 통신은 성공하였더라도, 서버 이슈로 인해 데이터는 null로 수신될 수 있습니다.
- 기존 코드에선 응답 데이터가 null인 경우 onSuccess로 분기되고 있습니다.
해당 경우엔 onFailure로 분기 되도록 아래와 같이 작성해주었습니다
response.body()?.let {
ApiResult.Success(it)
} ?: ApiResult.Failure(code = response.code(), message = "Response body is null")
2. 에러 응답 처리
private fun parseErrorResponse(response: Response<*>): ApiResult.Failure {
val errorJson = response.errorBody()?.string()
return runCatching {
val errorBody = gson.fromJson(errorJson, ErrorResponse::class.java)
val message = errorBody?.run {
message ?: error ?: "알 수 없는 에러가 발생하였습니다."
}
ApiResult.Failure(
code = errorBody.status,
message = message
)
}.getOrElse {
ApiResult.Failure(
code = response.code(),
message = "알 수 없는 에러가 발생하였습니다."
)
}
}
'안드로이드 > 이론' 카테고리의 다른 글
[ 안드로이드 ] LocalBroadcaseManager Deprecated 대응 (0) | 2024.07.12 |
---|---|
[ 안드로이드 ] 앱 내에 개발자 모드 추가하기 3 (0) | 2024.05.16 |
[ 안드로이드 ] 액티비티 배경 투명하게 설정 (+ 투명 배경 유지 안되는 이슈 해결) (0) | 2024.03.25 |
[ 안드로이드 ] Multi ViewType RecyclerView ViewHolder 순서 고정하기 (0) | 2024.01.14 |
[ 안드로이드 ] 앱 내에 개발자 모드 추가하기 2 (0) | 2023.12.29 |

현재 진행 중인 사이드 프로젝트에선 싱글 모듈 -> 멀티 모듈로 변경하는 작업을 하고 있습니다.
모듈 분리와 함께 코드 리팩토링도 진행 중인데, Retrofit의 CallAdapter를 커스텀하는 부분을 작성하고자 합니다.
아래와 같은 이유로 네트워크 요청 부분의 코드를 가장 먼저 수정하고 싶었습니다.
- LiveData 사용 중
- LiveData도 정말 유용하지만 Flow의 연산자의 활용도가 너무 높기에 Flow를 사용하고 싶었습니다.
- runCatching을 이용한 예외 처리
- 현재 사이드 프로젝트에서는 Repository에서 `runCatching`을 이용하여 네트워크 요청 시 예외를 처리하고 있습니다.
- 하지만 서버의 상태 코드에 따라 분기해야 할 때, 400번대 오류도 `onSuccess`로 처리되는 부분이 마음에 들지 않았습니다.
- 통신에는 성공 했으니 onSuccess로 오는 것이 맞긴하지만, 이는 결국 예외에 대한 처리이므로 유연하지 못하다고 느꼈습니다.
- 따라서 onSuccess / onFailure의 역할을 아래와 같이 정의하고자 하였습니다.
- onSuccess : 통신 성공, 에러 미발생
- onFailure : 통신 성공, 에러 발생
- 통신 실패시 : exceptionHandler에서 처리
- 통신 성공 시에도 데이터가 null인지 체크가 필요
- 서버에서 내려주는 응답 데이터가 누락될 경우를 대비하기 위해 모두 nullable로 받고 있습니다.
- 이로 인해, 아래와 같이 통신에 성공 했을 때도 데이터가 누락되지 않았는지 확인이 필요했습니다.
courseRepository.getMarathonCourse()
.onSuccess { response ->
if (response == null) {
// 에러 처리
}
}
위와 같이 여러 단점을 해결하고자 Retrofit의 CallAdapter를 커스텀하기로 결정하였습니다.
이번 리팩토링의 최종 목표는 아래와 같습니다.
목표
- 네트워크 요청 코드 간소화 (+ ViewModel에서 Flow 연산자 사용)
- onSuccess, onFailure로 구분 (+ 응답 데이터가 null인 경우 Failure로 처리)
- exceptionHandler를 이용한 공통 예외 처리
- flow의 catch를 이용한 커스텀 예외 처리
ApiResult 선언
Retrofit의 응답을 Success, Failure로 구분해서 받기 위한 클래스입니다.
ApiResult는 Retrofit의 응답을 받아 Domain 모듈의 Result로 변환해주는 역할을 합니다.
sealed class ApiResult<out R> {
data class Success<R>(val body: R) : ApiResult<R>()
data class Failure(
val code: Int,
val message: String?,
) : ApiResult<Nothing>()
// ApiResult<BaseResponse> 형태를 DomainResult<BaseModel> 형태로 매핑
fun <D> mapToResult(mapper: (R) -> D): Result<D> = when (this@ApiResult) {
is Success -> Result.Success(mapper(body))
is Failure -> Result.Failure(code, message)
}
}
fun <R, D> Flow<ApiResult<R>>.mapToResult(mapper: (R) -> D): Flow<Result<D>> {
return map { it.mapToResult(mapper) }
}
FlowApiResultCallAdapter 선언
전체 코드
class FlowApiResultCallAdapter<T>(
private val responseType: Type
) : CallAdapter<T, Flow<ApiResult<T>>> {
private val gson = Gson()
override fun responseType() = responseType
// Retrofit의 Call을 Flow<>로 변환
override fun adapt(call: Call<T>): Flow<ApiResult<T>> = flow {
val apiResult = suspendCancellableCoroutine { continuation ->
call.enqueue(object : Callback<T> {
override fun onFailure(call: Call<T>, t: Throwable) {
// 예외 발생시키고 Coroutine 종료
continuation.resumeWithException(t)
}
override fun onResponse(call: Call<T>, response: Response<T>) {
val result = if (response.isSuccessful) {
response.body()?.let {
ApiResult.Success(it)
} ?: ApiResult.Failure(code = response.code(), message = "Response body is null")
} else {
parseErrorResponse(response)
}
continuation.resume(result)
}
})
// Coroutine이 취소 되면 네트워크 요청도 취소
continuation.invokeOnCancellation {
call.cancel()
}
}
emit(apiResult)
}
private fun parseErrorResponse(response: Response<*>): ApiResult.Failure {
val errorJson = response.errorBody()?.string()
return runCatching {
val errorBody = gson.fromJson(errorJson, ErrorResponse::class.java)
val message = errorBody?.run {
message ?: error ?: "알 수 없는 에러가 발생하였습니다."
}
ApiResult.Failure(
code = errorBody.status,
message = message
)
}.getOrElse {
ApiResult.Failure(
code = response.code(),
message = "알 수 없는 에러가 발생하였습니다."
)
}
}
}
우선 CallAdapter를 구현 해주었습니다.
해당 CallAdapter를 이용하면 Retrofit의 응답을 Flow<ApiResult<T>> 형태로 받을 수 있습니다.
FlowApiResultCallAdapter에서 중요한 부분은 아래와 같습니다.
1. 응답 데이터 null 처리
- 통신은 성공하였더라도, 서버 이슈로 인해 데이터는 null로 수신될 수 있습니다.
- 기존 코드에선 응답 데이터가 null인 경우 onSuccess로 분기되고 있습니다.
해당 경우엔 onFailure로 분기 되도록 아래와 같이 작성해주었습니다
response.body()?.let {
ApiResult.Success(it)
} ?: ApiResult.Failure(code = response.code(), message = "Response body is null")
2. 에러 응답 처리
private fun parseErrorResponse(response: Response<*>): ApiResult.Failure {
val errorJson = response.errorBody()?.string()
return runCatching {
val errorBody = gson.fromJson(errorJson, ErrorResponse::class.java)
val message = errorBody?.run {
message ?: error ?: "알 수 없는 에러가 발생하였습니다."
}
ApiResult.Failure(
code = errorBody.status,
message = message
)
}.getOrElse {
ApiResult.Failure(
code = response.code(),
message = "알 수 없는 에러가 발생하였습니다."
)
}
}
'안드로이드 > 이론' 카테고리의 다른 글
[ 안드로이드 ] LocalBroadcaseManager Deprecated 대응 (0) | 2024.07.12 |
---|---|
[ 안드로이드 ] 앱 내에 개발자 모드 추가하기 3 (0) | 2024.05.16 |
[ 안드로이드 ] 액티비티 배경 투명하게 설정 (+ 투명 배경 유지 안되는 이슈 해결) (0) | 2024.03.25 |
[ 안드로이드 ] Multi ViewType RecyclerView ViewHolder 순서 고정하기 (0) | 2024.01.14 |
[ 안드로이드 ] 앱 내에 개발자 모드 추가하기 2 (0) | 2023.12.29 |