
최근 Gson을 사용하다가 난독화와 관련된 이슈가 발생하였는데요,
Kotlin 버전을 올렸더니 ` Missing field or token 'xxx'` 과 같은 에러가 발생하였습니다. 🤔
처음엔 단순히 "코틀린 버전 때문인가?" 싶었지만, 실제 원인은 코틀린 → Gradle Plugin → R8 → 난독화 → Gson으로 이어지는 복합적인 문제였습니다.
어떤 이유로 이렇게 연결이 되는 문제인지 하나씩 살펴보려 합니다.
👀 Kotlin 버전을 올렸더니 Gson이 깨진 이유?
Kotlin 버전을 올렸더니 앱 실행 시점에서 Gson 파싱이 깨졌습니다.
예를 들어 아래와 같은 클래스가 있을 때,
data class User(
val userId: String,
val userName: String
)
런타임에 Gson은 해당 필드를 찾지 못했습니다.
이유는 바로 userId 필드명이 난독화 과정에서 변경된 것이었습니다.
처음엔 “코틀린 버전을 올렸더니 깨졌다”고 느꼈지만, 사실 이건 Kotlin 버전 → Gradle Plugin 버전 → R8 내부 버전이 함께 바뀌면서 생긴 연쇄적인 구조 변화 때문이었습니다.
🔗 왜 Gradle Plugin 버전도 함께 올라가야 할까?
Kotlin은 Gradle Plugin과 호환 버전이 묶여 있습니다.
Kotlin 컴파일러·플러그인과 Android 빌드 툴(AGP/Gradle)은 서로 의존하는 여러 레이어(버전·기능·출력 포맷)를 공유하기 때문에, Kotlin 쪽을 올리면 빌드 시스템 쪽도 최소한 호환을 맞추기 위해 함께 올려야 합니다.
예를 들어 아래와 같이 짝을 지어 버전이 올라가게 됩니다.
| Kotlin | Android Gradle Plugin (AGP) |
| 1.8.x | 8.0 이하 |
| 1.9.x | 8.1~8.2 |
| 2.0.x | 8.5 이상 |
해당 내용을 조금 더 정리해보자면,
- Kotlin을 프로젝트에 올린다는 건 보통 `Kotlin 표준 라이브러리(kotlin-stdlib)` 와 `kotlin-gradle-plugin (KGP)` , 그리고 `코틀린 컴파일러(kotlinc)` 버전을 올리는 것을 의미합니다.
- kotlin-gradle-plugin은 Gradle 빌드 과정에 깊숙이 통합되어서 컴파일(병렬 빌드, incremental compile), kapt, Kotlin Scripting, Multiplatform, IR backend 등 다양한 기능을 제공합니다.
- 즉, Kotlin 버전은 단지 언어 사양뿐 아니라 빌드 플러그인의 동작을 바꿉니다.
즉, 코틀린을 올리면 Gradle Plugin도 같이 올라가야 하고,
Gradle Plugin에는 내부적으로 사용하는 R8 버전이 포함되어 있습니다.
그래서 결과적으로 R8의 동작 방식이 바뀌는 것이고, 이것이 난독화 규칙이 달라지는 핵심 이유입니다.
🔗 AGP와 R8의 관계
AGP(Android Gradle Plugin)는 빌드 파이프라인 전체를 관리합니다.
javac, kotlinc, dex, R8, shrinker 같은 모든 단계를 orchestration합니다.
- R8은 ProGuard의 후속 버전으로, 코드 압축(shrinking), 최적화(optimization), 난독화(obfuscation) 를 모두 담당합니다.
- R8은 Gradle Plugin 내부에 내장되어 있으며, AGP 버전에 따라 자동 업데이트됩니다.
그렇기 때문에 결국, Kotlin 업데이트 → Gradle Plugin 업데이트 → R8 업데이트 → 난독화 로직 변경 이라는 도미노가 발생합니다.
🕵🏻 R8의 난독화 정책 변화
🔎 예전 R8
예전 버전의 R8은 꽤 관대했습니다.
“리플렉션으로 접근할 수도 있으니까, 혹시 모르니 필드 이름은 남겨둘까?”
즉, 코드에서 명시적으로 쓰지 않아도, 리플렉션 접근 가능성이 있다고 판단되면 필드 이름을 남겨두는 경향이 있었습니다.
🔎 새 R8
하지만 최근 R8은 보수적으로 바뀌었습니다.
“리플렉션으로 접근한다고 명시 안 했네?
그럼 이 필드 이름은 바꿔버리자.”
즉, Gson이 내부적으로 리플렉션으로 접근하더라도 개발자가 그걸 Proguard/R8에 명시하지 않으면 그 필드는 난독화 되어버립니다.
결과적으로 Gson이 해당 필드를 찾지 못하게 되는 것입니다.
* 여기에서 "리플렉션(reflection)" 이란 런타임에 클래스나 필드에 접근하는 기술입니다.
Field field = User.class.getDeclaredField("userId");
field.setAccessible(true);
field.set(userInstance, "abc123");
예를 들어 Gson은 런타임에 위와 같이 동작합니다.
즉, Gson은 `User의 실제 소스 코드 이름("userId")` 을 기반으로 매핑합니다.
그런데 난독화가 userId -> a로 바뀌면?
getDeclaredField("userId")에서 필드를 찾지 못하고, 결과적으로 파싱 실패가 발생하게 됩니다.
그래서 "리플렉션으로 접근한다고 명시"를 해주어야 하는데요, 이건 바로 Proguard/R8 설정에 다음을 추가하는 것을 의미합니다.
# Gson이 리플렉션으로 접근하는 모델 클래스는 이름을 유지해야 함
-keep class com.example.model.** { *; }
-keepattributes Signature
-keepattributes *Annotation*
또는 필드 단위로 어노테이션을 붙이는 방법도 있습니다.
data class User(
@SerializedName("userId")
val userId: String,
@SerializedName("userName")
val userName: String
)
이렇게 하면 Gson은 난독화된 필드 이름 대신 `@SerializedName` 값을 그대로 사용하기 때문에 안전합니다.
즉, 문제의 본질은 “Kotlin 버전”이 아니라 R8이 리플렉션을 더 이상 관대하게 보지 않게 된 것에 있습니다!
📚 참고
https://github.com/google/gson/issues/2646
IllegalStateException: TypeToken must be created when using GSON 2.10.1 and R8 on Android · Issue #2646 · google/gson
Gson version 2.10.1 Java / Android version Java 17, Android 34 Description I just updated my Android project to use latest GSON library 2.10.1 and I am getting following exception, when using this ...
github.com
https://google.github.io/gson/Troubleshooting.html
Troubleshooting Guide
A Java serialization/deserialization library to convert Java Objects into JSON and back
google.github.io
https://r8.googlesource.com/r8/%2B/refs/heads/master/compatibility-faq.md
R8 FAQ
R8 FAQ R8 uses the same configuration specification language as ProGuard, and tries to be compatible with ProGuard. However as R8 has different optimizations it can be necessary to change the configuration when switching to R8. R8 provides two modes, R8 co
r8.googlesource.com
🍀 마무리
Kotlin을 올리면 Gradle Plugin이 바뀌고,
그 안의 R8이 바뀌며,
그 결과 리플렉션을 쓰는 Gson의 동작이 달라진다.
이 작은 체인 하나하나가 프로젝트 빌드의 안정성에 큰 영향을 줄 수 있다는 걸 다시 한 번 느꼈습니다.
이번 트러블 슈팅을 통해 빌드 구조를 다시 한 번 들여다 볼 수 있는 기회였던 것 같습니다.
'Android' 카테고리의 다른 글
| [Android] Build Variant는 왜 필요할까: 안드로이드 빌드 관리하기 (0) | 2026.01.11 |
|---|---|
| [Android] 결제에서 Consume은 왜 필요할까? (0) | 2025.12.28 |
| [Android] api와 implementation, 무엇이 다를까? (0) | 2025.10.12 |
| [Android] Portrait? Landscape? screenOrientation 이해하기 (0) | 2025.09.26 |
| [Android] 안드로이드 스튜디오에서 웹뷰 스크롤 늘려보기 (1) | 2025.09.14 |