안드로이드에서 Retrofit2으로 서버와 통신하는 경우가 많은데, 에러 처리를 제대로 해주지 않아 앱이 강제 종료 되는 경우가 많다.
모든 에러를 다 처리해줄 수도 없고, 서버에서 어떤 문제가 발생할 지도 모르기 때문에 Retrofit 요청을 안전하게 보내는 방법을 작성하고자 한다.
Response를 위한 공통 클래스 작성
- 안전한 통신을 위해 몇 가지 필요한 작업이 있다.
Parcelize
- 일단 기본적으로 서버에서 받은 응답을 직렬화하기 위해서 Parcelable를 사용한다.
- @Parcelize는 @Serialize와 비슷한데, 직렬화를 가능하게 해주는 인터페이스인 Parcelable을 구현해주는 어노테이션이다.
(Parcelize 설정 방법)
build.gradle (앱 수준)
plugins {
id 'com.android.library'
id 'kotlin-android'
id 'kotlin-parcelize'
}
BaseResponse (Data Layer)
- 응답 클래스는 모두 Parcelable를 사용할 것이기 때문에, Parcelable을 구현하는 BaseResponse 인터페이스를 작성해준다.
import android.os.Parcelable
interface BaseResponse : Parcelable
❓ BaseResponse 인터페이스를 만드는 이유
BaseResponse 인터페이스는 Parcelable을 그대로 상속만 하는데 왜 만드는 건지 궁금할 수도 있다.
일반적으로 서버에서 응답으로 주는 데이터들을 앱에서 그대로 사용하는 것은 좋지 않다. 서버의 응답 형식이 변할 때 영향을 크게 받을 수 있기 때문에 앱에선 사용할 데이터만 뽑아서 Entity 클래스를 작성해주는게 일반적이다.
응답으로 받은 Response 클래스를 필요한 데이터만 뽑은 Entity 클래스로 변경하기 위해서 DataMapper를 작성할 예정인데, 이 DataMapper에는 BaseResponse의 구현체만 받을 수 있도록 작성할 것이다.
BaseResponse를 생성하지 않고 그냥 Parcelable로 받게 되면 Parcelable을 구현하는 모든 클래스가 들어올 수 있게 된다.
DataMapper에선 Response 클래스들만 받아서 사용할 수 있도록 범위를 제한하고자 BaseResponse 인터페이스를 작성한 것이다.
이렇게 되면 DataMapper에는 Parcelable를 구현하고 있는 클래스는 들어오지 못하며, BaseResponse를 구현하는 클래스만 올 수 있게 된다.
DataMapper (Data Layer)
- 위에서 말한 것처럼, 서버에서 받은 응답 (BaseResponse)를 필요한 데이터만 뽑은 Entity 클래스로 변환하기 위한 DataMapper 인터페이스를 작성한다.
- BaseResponse가 응답의 최상위 타입이라면, BaseModel은 Entity들의 최상위 타입이라 보면 된다.
- BaseResponse 처럼 Parcelable만 상속 받는 인터페이스로 작성해주면 된다.
interface DataMapper<in R: BaseResponse, out D: BaseModel> {
fun R.toDomainModel(): D
}
ApiResponse (Data Layer)
- 서버의 응답을 성공/실패로 구분할 수 있도록 ApiResponse 클래스를 작성한다.
- 성공적으로 응답을 받았다면 Success 클래스에 응답 데이터를 넣어 반환하고, 오류가 발생한 경우 Error 클래스에 오류 내용을 담아서 보낸다.
sealed class ApiResponse<out T> {
data class Success<T>(val data: T): ApiResponse<T>()
data class Error(val error: ErrorResponse): ApiResponse<Nothing>()
}
💡 ErrorRepsonse
Error는 기본적으로 형식이 동일하기 때문에 Wrapper 클래스로 만들어준다.
@Parcelize
class ErrorResponse(
val timestamp : String? = null,
val status : String? = null,
val error : String? = null,
val code : String? = null,
val message : String? = null,
) : BaseResponse {
companion object: DataMapper<ErrorResponse, NetworkError> {
override fun ErrorResponse.toDomainModel(): NetworkError {
return NetworkError(
error = error ?: "null",
code = code ?: "null",
message = message ?: "알 수 없는 에러"
)
}
}
}
ApiResponseHandler (Data Layer)
- 위에서 작성한 클래스들은 응답을 받기 위한 클래스들이었다.
- 해당 클래스는 응답을 성공과 실패로 구분해서 위에서 구현한 클래스에 맞게 분류해 주는 역할을 할 것이다.
class ApiResponseHandler {
suspend fun<T> handle(call: suspend ()-> Response<T>): Flow<ApiResponse<T>> {
return flow{
val response = call.invoke()
// 요청이 성공적으로 수행된 경우
if(response.isSuccessful && response.body() != null) {
// ApiResponse.Success 클래스에 응답 데이터를 담아서 반환
emit(ApiResponse.Success(response.body()!!))
} else { // 오류가 발생한 경우
val errorBody = response.errorBody()?.string()
val message =
if (errorBody.isNullOrEmpty()) response.message()
else errorBody
// ApiResponse.Error 클래스에 오류 내용을 ErrorResponse로 감싸서 반환
emit(
ApiResponse.Error(
ErrorResponse(
code = response.code().toString(),
message = message ?: "알 수 없는 오류가 발생했습니다."
)
)
)
}
}
}
}
- 이제 서버로 보내는 모든 요청은 ApiResponseHandler로 전달해주면 된다. 어떤 오류가 발생해도 여기서 처리가 되기 때문에 안전하게 통신을 할 수 있게 된다.
ResponseState (Domain Layer)
- 이 클래스는 위에서 작성했던 ApiResponse와 동일한 클래스이다. (클린 아키텍쳐를 적용하지 않았다면 패스해도 된다.)
- Clean-Architecture의 domain 계층은 다른 계층에 접근하지 않기 때문에, ApiResponse 클래스를 이용할 수 없다.
- 그래서, domain 계층만의 ApiResonse 클래스를 만들어준다고 보면 된다.
sealed class ResponseState<out T> {
data class Success<T>(val data: T): ResponseState<T>()
data class Error(val error: NetworkError): ResponseState<Nothing>()
}
NetworkError (Domain Layer)
@Parcelize
data class NetworkError(
val error: String = "",
val code: String = "",
val message: String = "알 수 없는 오류가 발생하였습니다."
) : BaseModel
전체 소스 코드
'안드로이드 > 이론' 카테고리의 다른 글
[ 안드로이드 ] 클릭 가능한 모든 뷰에 반투명 효과 넣기 (0) | 2023.05.20 |
---|---|
[ 안드로이드 ] Retrofit2를 이용하여 서버와 통신하기 (2) - 예시 (0) | 2023.05.04 |
[ 안드로이드 ] Room DB - Dao 사용법 (0) | 2023.04.08 |
[ 안드로이드 ] RecylcerView + Filterable을 이용하여 실시간 검색 기능 구현하기 (0) | 2023.04.07 |
[ 안드로이드 ] BottomSheetDialog 테두리 둥글게 설정하기 (0) | 2023.04.04 |