[Android] Serialization API 파싱과 Navigation

Geun·2026년 5월 7일

Android

목록 보기
12/12
post-thumbnail

Moshi에서 Kotlinx Serialization으로 — API 파싱과 Navigation까지 한 번에 정리

1. 들어가며

기존 스터디 프로젝트에서는 JSON 파싱은 Moshi, Navigation은 문자열 라우트 + sealed class 조합으로 구성하고 있었습니다. 동작 자체에는 문제가 없었지만, 새 화면을 추가할 때마다 "detail/{id}" 같은 문자열 라우트와 navArgument 보일러플레이트를 반복적으로 작성해야 한다는 단점이 있었다.

현재 회사 프로젝트에서는 이미 Serialization 을 사용하고 있는데, 실무에서 써보니 Navigation Compose 2.8+Type-Safe Navigation까지 같은 라이브러리로 자연스럽게 통합되는 점이 인상적이었다. 그래서 이번 기회에 스터디 프로젝트에도 동일하게 반영해 JSON 파싱과 Navigation을 한 라이브러리로 통일하는 마이그레이션을 진행하였다.

이 글에서는 다음 두 가지를 정리

  • 왜 요즘은 Kotlinx Serialization를 많이 쓰는지 (Gson, Moshi와 비교)
  • 실제 프로젝트에서 Moshi → Kotlinx Serialization + 문자열 라우트 → Type-Safe Navigation 으로 마이그레이션한 과정

2. Gson / Moshi / Serialization 뭐가 다른가?

Android에서 JSON 파싱 라이브러리는 크게 세 가지가 쓰여왔는데 각자의 특징을 먼저 짚고 가면 왜 요즘 Serialization 을 쓰는지 자연스럽게 이해가 된다.

1) Gson (2008~)

Google이 만든 라이브러리로, Android 초창기부터 표준처럼 쓰였다.

data class User(val id: Int, val name: String)

val gson = Gson()
val user = gson.fromJson(json, User::class.java)

한계점

  • 리플렉션 기반 — 런타임에 클래스 정보를 읽어서 필드를 채우게 되어 느리게 되고 R8/ProGuard 사용 시 규칙 추가가 필수
  • Kotlin null safety 무시val name: String(non-null)인데 JSON에 name이 없으면 그냥 null을 넣어버림
  • Kotlin 기본값 무시val age: Int = 20으로 선언해도 JSON에 없으면 0이 들어감
  • 생성자 호출 안 함 — 리플렉션으로 객체 만들고 필드만 채우기 때문에 init 블록 검증 로직이 안 돌아감

2) Moshi (2016~)

Square(Retrofit 만든 곳)에서 만든 라이브러리. Gson의 Kotlin 관련 단점을 보완하려고 등장

@JsonClass(generateAdapter = true)
data class User(val id: Int, val name: String)

개선점

  • Kotlin null safety 인식 — non-null 필드가 누락되면 파싱 실패로 명시적 에러
  • 코드 생성(KSP) 방식 지원 — 리플렉션 없이 컴파일 타임에 어댑터 생성 → 빠르고 R8 친화적
  • 기본값 존중 — Kotlin 생성자 기본값을 정상적으로 사용

남은 한계점

  • 기본 모드는 여전히 리플렉션 기반KotlinJsonAdapterFactory를 쓰면 Gson과 마찬가지로 kotlin-reflect의존. 리플렉션을 피하려면 @JsonClass(generateAdapter = true) + KSP 코드 생성 모드를 별도로 설정해야 함
  • 별도 의존성 + KSP 처리기 필요 (moshi-core, moshi-kotlin, moshi-kotlin-codegen)
  • JVM 전용 — Android/Java 환경에서만 동작

3) Kotlinx Serialization (2020~)

JetBrains가 직접 만든 Kotlin 공식 라이브러리
어노테이션 처리기가 아닌 Kotlin 컴파일러 플러그인으로 동작

@Serializable
data class User(val id: Int, val name: String)

핵심 차별점

  • 리플렉션도 KSP도 아님 — 컴파일러가 직접 직렬화 코드를 생성. 가장 빠르고 가벼움
  • Kotlin Multiplatform 지원 — Android, iOS, 어디서든 동일하게 동작
  • Kotlin 공식 — 언어 기능(sealed class, value class, default 값 등)과 100% 호환
  • Navigation Compose 2.8+ Type-Safe Navigation이 이 라이브러리를 사용 → 라우팅과 API 파싱을 한 라이브러리로 통합 가능

📊 한눈에 비교

항목GsonMoshiKotlinx Serialization
출시200820162020
만든 곳GoogleSquareJetBrains (Kotlin 공식)
동작 방식리플렉션리플렉션 또는 KSP 코드 생성컴파일러 플러그인
Kotlin null safety
Kotlin 기본값
sealed class 지원△ (어댑터 직접 작성)✅ (네이티브)
Multiplatform❌ (JVM 전용)❌ (JVM 전용)
R8 / ProGuard 친화❌ (규칙 필요)△ (codegen 모드만 친화적)
Navigation Compose 연동

🎯 왜 요즘은 Kotlinx Serialization인가

정리하면 세 가지 이유로 압축

  1. Kotlin 공식 라이브러리 — 언어 기능과 항상 동기화되며 JetBrains이 유지보수
  2. 컴파일러 플러그인 방식 — 리플렉션도 KSP도 아닌 효율적인 코드 생성 방식
  3. 생태계 확장 — Navigation Compose, Ktor 등이 Kotlinx Serialization을 표준으로 채택. 한 라이브러리로 더 많은 영역을 커버 가능

3. Before — Moshi + 문자열 라우트

(1) Moshi 설정 (NetworkModule.kt)

@Provides
@Singleton
fun provideMoshi(): Moshi =
    Moshi.Builder()
        .add(KotlinJsonAdapterFactory())
        .build()

@Provides
@Singleton
fun provideRetrofit(moshi: Moshi, okHttpClient: OkHttpClient): Retrofit =
    Retrofit.Builder()
        .baseUrl("...")
        .addConverterFactory(MoshiConverterFactory.create(moshi))
        .client(okHttpClient)
        .build()

(2) 문자열 기반 Navigation (Routes.kt)

sealed class Screen(val route: String) {
    data object Home : Screen("home")
    data object Detail : Screen("detail/{id}") {
        fun createRoute(id: Int) = "detail/$id"
        const val ID_ARG = "id"
    }
}

composable(
    route = Screen.Detail.route,
    arguments = listOf(
        navArgument(Screen.Detail.ID_ARG) { type = NavType.IntType }
    )
) { backStackEntry ->
    val id = backStackEntry.arguments?.getInt(Screen.Detail.ID_ARG) ?: return@composable
    DetailScreen(id = id, ...)
}

이 코드의 단점

  • "detail/{id}" 같은 문자열 라우트는 오타가 나도 컴파일러가 잡아주지 않음
  • navArgument + NavType 보일러플레이트가 화면마다 반복
  • 파라미터 추출 시 arguments?.getInt(...) ?: return처럼 nullable 처리가 필요

4. After — Kotlinx Serialization 적용

(1) Gradle 의존성 변경

# libs.versions.toml
[versions]
kotlinx-serialization = "1.7.3"

[libraries]
retrofit-converter-kotlinx-serialization = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version.ref = "retrofit" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization" }

[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
// build.gradle.kts
plugins {
    alias(libs.plugins.kotlin.serialization)
}

dependencies {
    implementation(libs.kotlinx.serialization.json)
    implementation(libs.retrofit.converter.kotlinx.serialization)
}

(2) DTO에 @Serializable 추가

@Serializable
data class MessageDto(
    val id: Int,
    val title: String,
    // ...
)

(3) Retrofit Converter 교체

@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit =
    Retrofit.Builder()
        .baseUrl("...")
        .addConverterFactory(
            Json { ignoreUnknownKeys = true }
                .asConverterFactory("application/json; charset=UTF-8".toMediaType())
        )
        .client(okHttpClient)
        .build()

Moshi 인스턴스 Provider가 사라져 NetworkModule이 훨씬 가벼워지고 ignoreUnknownKeys = true로 서버에서 새 필드가 추가되어도 앱이 깨지지 않게 방어적 코드 가능

(4) Type-Safe Navigation

@Serializable
data object Home

@Serializable
data class Detail(val id: Int)

NavHost(navController = navController, startDestination = Home) {
    composable<Home> {
        HomeScreen(onItemClick = { id -> navController.navigate(Detail(id)) })
    }
    composable<Detail> { backStackEntry ->
        val detail: Detail = backStackEntry.toRoute()
        DetailScreen(id = detail.id, ...)
    }
}

@Serializable 클래스 자체가 라우트가 되며 navArgument, NavType, 문자열 헬퍼가 모두 사라짐


5. 무엇이 좋아졌나

항목BeforeAfter
JSON 파싱 라이브러리MoshiKotlinx Serialization
컴파일러 지원KSP 어댑터 생성컴파일러 플러그인
Navigation 라우트문자열 ("detail/{id}")타입 안전 객체 (Detail(id))
파라미터 추출arguments?.getInt(...)backStackEntry.toRoute()
Routes.kt 코드량38줄27줄 (-29%)

특히 Navigation 쪽 변화가 체감상 가장 컸다. 라우트 정의 → 등록 → 파라미터 추출까지 모두 타입 안전한 객체 하나로 끝나기 때문에 새 화면을 추가할 때 신경 쓸 게 절반으로 줄어듬


6. 마치며

JSON 파싱과 Navigation은 서로 무관해 보이지만 둘 다 "객체 ↔ 문자열" 직렬화라는 공통점이 있다. Kotlinx Serialization을 도입하면서 이 두 영역의 직렬화 방식을 한 라이브러리로 통일할 수 있었고 결과적으로 코드량이 줄고 타입 안전성이 올라갔다.

특히 Navigation Compose 2.8+의 Type-Safe Navigation은 Kotlinx Serialization을 전제로 설계되어 있어서 새 프로젝트라면 처음부터 Kotlinx Serialization으로 시작하는 게 가장 깔끔한 선택이라고 느꼈다.

실제 마이그레이션 커밋: GEUN-TAE-KIM/Mvi_Orbit_Study @ 41ca94f

0개의 댓글