✔️ Gson을 Moshi로 마이그레이션하기로 결정한 이유
- 지금까지 Json 직렬화 라이브러리로 Gson을 사용해 왔는데, 되돌아보니 Gson을 선택한 이유가 없었다.
그냥 다들 Gson을 사용한다니까 사용했던 것 같아서, 다른 라이브러리들과의 비교를 통해 Moshi로 마이그레이션 하기로 결정했다. - Gson / Moshi / Jackson을 비교했고, 그 중에서 (1) Kotlin 호환성 (2) 라이브러리 지속성 (3) 런타임 퍼포먼스
위 세 가지를 주요 비교점으로 두었다. - Retrofit의 개발사인 Square사의 라이브러리이며, 셋 중 Kotiln 호환성이 가장 뛰어나고, 런타임 퍼포먼스가 높은 Moshi로 결정하게 되었다.
✔️ Moshi 란?
Square에서 개발한 경량 Json 라이브러리이다.
Kotlin 기반으로 설계되었으며, CodeGen을 사용하여 런타임 성능을 향상시키고, 커스텀 어댑터를 사용하여 높은 확장성을 가진다.
✔️ CodeGen
Moshi의 CodeGen은 컴파일 타임에 Json 어댑터를 생성하는 기능이다.
이를 통해 리플렉션 사용을 최소화함으로써 런타임 퍼포먼스가 개선되며, Json 직렬화/역직렬화 단계에서 발생할 수 있는 오류를 컴파일 타임에 감지할 수 있다.
컴파일 타임에 Json 어댑터를 생성하기 위해, 아래처럼 JsonClass 어노테이션을 추가해줘야 한다.
Kapt가 컴파일 타임에 이를 감지하여 어댑터를 생성하게 된다.
@JsonClass(generateAdapter = true)
data class JoinedRunnerResponse(
@Json(name = "isSuccess") val isSuccess: Boolean = false,
@Json(name = "code") val code: Int = 0,
@Json(name = "message") val message: String? = null,
@Json(name = "result") val result: List<JoinedRunnerResult>
)
✔️ build.gradle
변경 전 - Gson
// https://github.com/square/retrofit/tree/master/retrofit-converters/gson
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
변경 후 - Moshi
// https://github.com/square/moshi
implementation 'com.squareup.moshi:moshi:1.15.0'
implementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
✔️ 응답 객체
변경 전 - Gson
data class GetPostDetailResponse(
@SerializedName("isSuccess") val isSuccess: Boolean = false,
@SerializedName("code") val code: Int = 0,
@SerializedName("message") val message: String? = null,
@SerializedName("result") val result : PostDetail
)
변경 후 - Moshi
@JsonClass(generateAdapter = true)
data class GetPostDetailResponse(
@Json(name = "isSuccess") val isSuccess: Boolean = false,
@Json(name = "code") val code: Int = 0,
@Json(name = "message") val message: String? = null,
@Json(name = "result") val result : PostDetail
)
✔️ NetworkModule
Hilt를 사용한 DI 패턴을 적용중인 코드의 예시이다.
변경 전 - Gson
@Singleton
@Provides
fun provideGson(
zonedDateTimeAdapter: ZonedDateTimeAdapter
): Gson = GsonBuilder()
.registerTypeAdapter(ZonedDateTime::class.java, zonedDateTimeAdapter)
.create()
@Singleton
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okHttpClient)
.build()
변경 후 - Moshi
@Singleton
@Provides
fun provideMoshi(
zonedDateTimeAdapter: ZonedDateTimeAdapter
): Moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
@Singleton
@Provides
fun provideRetrofit(
okHttpClient: OkHttpClient,
moshi: Moshi
): Retrofit = Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.client(okHttpClient)
.build()
✔️ 트러블 슈팅
원래는 아래 BaseResponse 클래스를 모든 응답 클래스들이 상속받는 구조였는데,
상속 구조를 지원하는 Gson에 반해 Moshi는 상속 구조를 지원하지 않아 문제가 발생했다.
open class BaseResponse(
@Json(name = "isSuccess") val isSuccess: Boolean = false,
@Json(name = "code") val code: Int = 0,
@Json(name = "message") val message: String? = null
)
이를 해결하기 위해 아래 data 클래스처럼 모든 응답 클래스들에서 상속 구조를 제거했다.
Moshi는 컴파일 타임에 data 클래스 구조를 고정하기 때문에 런타임 퍼포먼스가 Gson보다 뛰어나지만,
그를 위해 상속 구조를 지원하지 않아 응답 클래스에서 코드 중복이 일어난다는 단점이 있는 것 같다.
@JsonClass(generateAdapter = true)
data class GetPostDetailResponse(
@Json(name = "isSuccess") val isSuccess: Boolean = false,
@Json(name = "code") val code: Int = 0,
@Json(name = "message") val message: String? = null,
@Json(name = "result") val result : PostDetail
)
✔️ 추후 개선점
기존의 상속 구조를 제거함으로써 발생한 중복 코드들을 최소화하는 방향으로 수정이 필요할 것 같다.
BaseResponseWrapper 클래스와 typealias를 사용하는 방법을 생각하고 있다.
data class BaseResponseWrapper<T> (
@Json(name = "isSuccess") val isSuccess: Boolean = false,
@Json(name = "code") val code: Int = 0,
@Json(name = "message") val message: String? = null,
@Json(name = "result") val result: T
)
typealias GetPostDetailResponse = BaseResponseWrapper<PostDetail>
interface GetPostDetailApi {
@GET("/postings/v2/{postId}/{userId}")
suspend fun getPostDetail(
@Path("postId") postId : Int,
@Path("userId") userId: Int
): Response<GetPostDetailResponse>
}
하지만 위 코드처럼 적용해도 최적의 방법은 아닌 것 같다.
- 각 API 인터페이스에서 응답 객체의 구조를 명시적으로 확인할 수 없고
- 여러 typealias들을 하나의 파일에서 관리할 것인가
(result 타입이 Nothing인 API들은 SimpleResponse로 통일해서 사용해도 되겠지만) - 이렇게 적용할 경우에 응답 값을 처리하는 repository에서 응답 데이터 접근 뎁스가 깊어진다..
내가 코드의 중복을 굉장히 싫어해서 Moshi가 상속 구조를 지원하지 않는 점은 굉장히 아쉽다.
현재 진행중인 사이드 프로젝트의 응답 객체 구조가 고정적이기 때문에,
Moshi의 여러 장점들이 있음에도 상속 구조 활용을 위해 Gson을 사용하는게 나을 수도 있겠다는 생각이 든다.
'개발 > Android' 카테고리의 다른 글
[안드로이드] RecyclerView는 어떻게 동작할까? (0) | 2024.06.24 |
---|---|
[안드로이드] EXTRA_PICK_IMAGES_MAX 사용 시 주의할 점 (0) | 2023.09.14 |
[안드로이드] 클린 아키텍처 - (3) 멀티 모듈 패키지 구조 (0) | 2023.05.23 |
[안드로이드] 클린 아키텍처 - (2) 의존성 역전 (0) | 2023.05.16 |
[안드로이드] 클린 아키텍처 - (1) 개념 (0) | 2023.05.16 |