서버 응답을 받는 도중 통신 모델로 선언한 프로퍼티 값의 타입을 잘못 지정했는데도 통신에 성공하고, 프로퍼티 값에 접근하는데까지 에러가 나지 않았던 경우를 마주했다.
Retrofit이 ConverterFactory에서 에러를 잡아주는 것인지 Retrofit과 같이 쓰고 있는 Rxjava Error Handling 클래스에서 에러를 흘려주는 것인지 궁금증에서 시작된 이번 글은 Retofit 부터 RxJava, 직렬화와 역직렬화까지 마인드맵처럼(..) 알아본다.
1. Retrofit
Retrofit은 HTTP API를 자바 인터페이스 형태로 변환해 안드로이드 개발 시 API를 쉽게 호출할 수 있도록 지원하는 라이브러리이다.
- 서버와 응답하는 데이터 형식은 JSON 형식이기에 Retrofit에서 Call<> 객체 안에 응답될 Body 타입의 데이터 클래스와 호환되지 않는다.
- 따라서 서버에서 응답되는 JSON 포맷의 데이터를 데이터 클래스 객체로 파싱해줘야하고, 클라이언트에서 서버에 데이터를 보낼 때에도 데이터 클래스 객체를 JSON 형식으로 보내야 서버가 받을 수 있다.
- 데이터 클래스와 JSON간의 파싱을 위해 Retofit 라이브러리에 Gson 이라는 Java 라이브러를 추가로 묶어서 사용하면 되며ConverterFactory를 통해 JSON 타입의 응답결과를 객체로 매핑해준다. converter를 설정해주지 않으면 converter 를 설정하지 않았다는 IllegalArgumentException 런타임 에러가 난다.
Caused by: java.lang.IllegalArgumentException: Could not locate RequestBody converter for class 클래스이름
2. 직렬화와 역직렬화
여기서 직렬화와 역직렬화의 개념을 잠시 살펴보자.
Serialization is a mechanism of converting the state of an object into a byte stream. Deserialization is the reverse process where the byte stream is used to recreate the actual Java object in memory.
Java의 I/O 처리는 정수, 문자열, 바이트 단위의 처리만 지원한다. 따라서 복잡한 객체의 내용을 저장, 복원하거나 네트워크 상으로 전송하기 위해서는 객체의 내용을 I/O가 처리할 수 있는 형태로 변환해 줘야 한다.
Java에서 말하는 객체 직렬화는 이처럼 Java의 객체를 외부로 저장, 복원하거나 네트워크상으로 전송할 수 있도록 바이트 형태로 변환하는것을 의미한다. 즉, 객체가 아무리 복잡하여도 직렬화를 통해 객체를 바이트 형태로 변환하여 외부로 전송할 수 있는 것이다. 반대로 역직렬화는 바이트 형태를 다시 객체로 변환시키는 과정이다.
The serialized data is suitable for storage in files and databases, and can be sent to other computer systems across network protocols.
바이트 형태로 변환해서 stream으로..
단어 Serialization 에서도 알 수 있듯이 무언가를 연속적인 데이터로 바꾸는 뜻임을 알 수 있다. 객체에 저장된 데이터를 입출력 ByteStream에 출력하기 위해 연속적인 데이터로 변환하는 작업이라고 이해하면 된다.
객체는 '인스턴스 변수의 집합'이므로 객체를 저장/전송하는 것은 객체의 인스턴스 변수의 값을 저장/전송하는 것과 동일하다. 궁극적으로 객체의 직렬화는 서드파티에서 객체를 사용하기 위한 것이다.
Json 타입의 응답결과를 Gson 으로 converter 시켜주는데, 이 과정을 역직렬화한다고 할 수 있다. 데이터를 읽어서 입력 객체를 만드는 것이다. 이 과정에서 약속된 프로퍼티가 맞지 않는다거나 프로퍼티가 없어도 에러가 나지 않는 현상을 발견했는데 Gson이나 Converter에 기본적으로 내장되어 있다고 한다.
서버에서 받기로 한 프로퍼티가 없는 값을 설정하거나 프로토콜이 다르게 설정해놓으면, type이 Boolean 일 경우 false로 자동으로 출력되고, String이나 객체일 경우는 type을 nullable하게 지정하지 않았어도 null 로 출력이 된다. 또한 Int 일 경우에는 기본값 0으로 출력된다.
서버에서 받기로 한 프로퍼티명을 잘 지정하고, 타입이 다를 경우는? 서버에서 받기로 한 type이 Boolean인데 String으로 지정하면 그래도 Boolean값이 잘 들어온다. 하지만 Boolean 값을 Int 로 받는 것으로 타입 지정을 하면 SyntaxError가 발생한다.
Retrofit의 인스턴스를 생성하는 코드이다. 데이터를 역직렬화해주는 Factory인 GsonConverterFactory까지 보았다.
return Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
우리 회사는 비동기처리로 RxJava를 사용하고 있었고 Retrofit에서 RxJava 를 사용하기 위해서 addCallAdapterFactory라는 함수 또한 제공하고 있기에 쉽게 사용할 수 있다. RxjavaCallAdapterFactory는 무엇이고 어떻게 쓰는지 보자.
3. RxJavaCallAdapterFactory
RxJavaCallAdapterFactory 클래스를 Retrofit에 추가하면 Observable 형태로 받을 수 있다. 인터페이스의 구현체를 살펴보자.
There are three configurations supported for the Observable, Flowable, Single, Completable and Maybe type parameter:
- Direct body (e.g., Observable ) calls onNext with the deserialized body for 2XX responses and calls onError with HttpException for non-2XX responses and IOException for network errors.
- Response wrapped body (e.g., Observable >) calls onNext with a Response object for all HTTP responses and calls onError with IOException for network errors
- Result wrapped body (e.g., Observable >) calls onNext with a Result object for all HTTP responses and errors.
onNext 으로는 역직렬화된 2XX responses 받거나 onError 로는 non-2XX responses , IOException for network errors, 그 외의 에러들 을 흘려준다. call adapter factory에서는 역직렬화된 2XX responses 받아 onNext로 흘려주거나 2XX response가 아닌 것들과 IOException은 onError로 흘려준다. 이 때 데이터를 읽어서 입력 객체를 만드는 과정에서 에러가 나면 onError로 처리하는 부분을 통해 통신단에서 먼저 처리를 해줄 수 있는 것이다.
public void onError(Throwable throwable) {
if (throwable instanceof HttpException) {
// We had non-2XX http error
}
if (throwable instanceof IOException) {
// A network or conversion error happened
}
// We don't know what happened. We need to simply convert to an unknown error
// ...
}
Retrofit을 사용하면 RetrofitError 형식으로 매핑해서 전달한다. retrofit2.adapter.rxjava.HttpException 은 RetrofitError.Kind.HTTP 으로 2XX respponse가 아닌 값을 받는다. java.io.IOException
은 RetrofitError.Kind.NETWORK NetworkError로 직렬화와 역직렬화 과정에서 에러가 발생한 것이다. 이 외에는 RetrofitError.Kind.UNKNOWN 로 빠지게 된다.
이 클래스와 CallAdapter를 상속받아RxErrorHandlingCallAdapterFactory라는 클래스를 만들어보자.
4. The Call Adapter
RxJava는 subscribe 하는 시점에 stream으로 성공과 실패 여부를 stream 으로 발행한다. RetrofitError를 받아 Observable한 형태로 바꿔보자. 또한 throwable onError 메서드를 통해 throwable 클래스를 값으로 받고 이를 통해 우리 서비스에 맞는 에러 클래스로 conversion 하여 가공을 할 수 있다.
class RxErrorHandlingCallAdapterFactory private constructor() : CallAdapter.Factory() {
private val original: RxJava2CallAdapterFactory = RxJava2CallAdapterFactory.create()
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *> {
return RxCallAdapterWrapper(
retrofit, original.get(returnType, annotations, retrofit) as CallAdapter<out Any, *>
)
}
private class RxCallAdapterWrapper<R> internal constructor(
private val retrofit: Retrofit,
private val wrapped: CallAdapter<R, *>
) : CallAdapter<R, Any> {
override fun responseType(): Type {
return wrapped.responseType()
}
override fun adapt(call: Call<R>): Any {
val adaptedCall = wrapped.adapt(call)
if (adaptedCall is Completable) {
return adaptedCall.onErrorResumeNext { throwable ->
Completable.error(asRetrofitException(throwable))
}
}
if (adaptedCall is Single<*>) {
return adaptedCall.onErrorResumeNext { throwable ->
Single.error(asRetrofitException(throwable))
}
}
if (adaptedCall is Observable<*>) {
return adaptedCall.onErrorResumeNext { throwable: Throwable ->
Observable.error(asRetrofitException(throwable))
}
}
if (adaptedCall is Maybe<*>) {
return adaptedCall.onErrorResumeNext { throwable: Throwable ->
Maybe.error(asRetrofitException(throwable))
}
}
throw RuntimeException("Observable Type not supported")
}
private fun getRetrofitException(throwable: Throwable): ServerException {
if (throwable is HttpException) {
val response = throwable.response()
return RetrofitException.httpError(response.raw().request().url().toString(), response, retrofit)
}
return if (throwable is IOException) {
RetrofitException.networkError(throwable)
} else RetrofitException.unexpectedError(throwable)
}
}
companion object {
fun create(): CallAdapter.Factory {
return RxErrorHandlingCallAdapterFactory()
}
}
}
RxJavaCallAdapterFactory에 의해 생성된 CallAdapter를 감싸는 클래스이다. 먼저 call을 받기 위해 wrapped adapter를 사용한다. 그리고 형식에 맞는 Observable 으로 onErrorResumeNext를 통해 에러를 전달한다. 에러가 발생할 때마다 throwable 객체를 RetrofitException 클래스에 converting 해준다.
'💤 RxJava' 카테고리의 다른 글
[RxJava] Single, Maybe and Completable (0) | 2023.09.12 |
---|---|
[RxJava] Observable 생성하기 (0) | 2023.09.12 |
[RxJava] Observable (0) | 2023.09.12 |
[RxJava] subscribe inside subscribe (0) | 2023.09.06 |
[RxJava] Scheduler로 Multi Thread 관리하기 (0) | 2023.07.04 |