내 코드를 훔쳐보지 마세요 (난독화와 Release 에러)
1. "Debug 땐 멀쩡했는데 배포하니 죽어요."
개발할 땐 완벽했습니다. 그런데 스토어에 올린 앱(Release 빌드)을 실행하자마자 바로 죽어버립니다. 로그를 까보니 외계어가 나옵니다.
java.lang.NullPointerException: ... at a.b.c.d(Unknown Source)
a.b.c.d가 도대체 뭘까요?
이것은 안드로이드의 문지기, R8(구 ProGuard)이 다녀간 흔적입니다.
코드를 압축하고 난독화하면서 클래스 이름을 다 바꿔버린 거죠.
2. 원리 이해 - R8 컴파일러의 '가지치기'
안드로이드 릴리즈 빌드는 기본적으로 R8 컴파일러를 거칩니다. R8은 3가지 일을 합니다.
- Shrink (가지치기): 안 쓰는 코드(Dead Code)를 찾아내서 삭제합니다. (Tree Shaking)
- Optimize (최적화): 함수를 합치거나(Inlining), 클래스를 병합해서 성능을 높입니다.
- Obfuscate (난독화): 클래스/함수 이름을
a,b,c로 바꿔서 리버싱(해킹)을 어렵게 합니다.
문제는 "안 쓴다고 생각해서 지웠는데, 사실은 쓰는 코드"일 때 발생합니다. 특히 Reflection(이름으로 함수/클래스 찾기)을 쓰는 라이브러리(Retrofit, Gson)나 JNI(Native Code) 호출이 여기서 박살납니다.
3. 문제 상황 - 모델 클래스 증발 (JSON Parsing)
서버에서 온 JSON을 User 객체로 바꿀 때, 라이브러리가 User라는 클래스 이름을 찾습니다.
그런데 R8이 User를 a로 바꿔버렸거나, "이 클래스 코드에서 직접 호출 안 하네?" 하고 지워버렸습니다.
그럼 ClassNotFoundException이나 NoSuchMethodError가 납니다.
4. 해결책 1 - @Keep 어노테이션 (추천)
가장 깔끔한 방법입니다. 지켜야 할 클래스 위에 @Keep만 붙이면 됩니다.
이 어노테이션이 붙은 친구들은 R8이 절대 건드리지 않습니다.
import androidx.annotation.Keep
@Keep
data class User(val name: String)
5. 해결책 2 - ProGuard Rules 설정 (전통적 방법)
외부 라이브러리라서 코드를 수정할 수 없다면, 규칙 파일에 적어야 합니다.
android/app/proguard-rules.pro 파일을 열고(없으면 만드세요), 다음 규칙을 추가합니다.
# 1. 특정 패키지 아래의 모든 클래스 유지
-keep class com.example.myapp.models.** { *; }
# 2. Enum 값들은 이름이 바뀌면 안 됨 (valueOf() 때문)
-keepclassmembers enum * { *; }
# 3. Retrofit, Gson 등 인기 라이브러리 보호
-keep class retrofit2.** { *; }
-keep class com.google.gson.** { *; }
그리고 android/app/build.gradle에 등록합니다.
buildTypes {
release {
// ...
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
6. Mapping 파일 활용 (암호 해독기) 더 알아보기
난독화된 에러 로그(a.b.c.d)를 다시 사람이 읽을 수 있게 복구하려면 암호 해독표(Mapping File)가 필요합니다.
빌드할 때마다 build/app/outputs/mapping/release/mapping.txt 파일이 생성됩니다.
이 파일을 Google Play Console이나 Firebase Crashlytics에 업로드하면,
a.b.c.d가 User.fromJson으로 마법처럼 복원되어 보입니다.
팁: usage.txt 파일을 보면 R8이 무엇을 삭제했는지 목록이 나옵니다. 내 코드가 왜 사라졌는지 궁금하면 이걸 보세요.
7. Resource Shrinking (리소스 최적화) 파헤치기
코드만 줄이는 게 아닙니다. R8은 안 쓰는 이미지나 레이아웃 파일도 줄일 수 있습니다.
build.gradle에서 shrinkResources true를 켜면 됩니다.
release {
minifyEnabled true // 코드 난독화 & 축소
shrinkResources true // 리소스 축소 (minifyEnabled가 true여야 작동)
}
주의할 점은, 코드에서 동적으로 리소스 이름(resId)을 만들어 접근하는 경우(예: getResources().getIdentifier()) R8이 "이 리소스 안 쓰네?" 하고 지워버릴 수 있습니다.
이럴 땐 res/raw/keep.xml 파일을 만들어서 명시적으로 보존해야 합니다.
8. 트러블슈팅 체크리스트 제대로 이해하기
배포 전, 혹은 배포 직후 앱이 죽는다면 당황하지 말고 순서대로 확인하세요.
- Logcat 확인:
flutter run --release로 폰에 직접 띄워서 로그를 보세요.ClassNotFound나MethodNotFound가 보이면 100% 난독화 문제입니다. usage.txt검색:build/app/outputs/mapping/release/usage.txt파일을 열어서, 에러가 난 클래스 이름이 있는지 찾으세요. 거기에 있다면 R8이 "안 쓴다"고 판단하고 지운 겁니다.mapping.txt백업: 이 파일은 빌드할 때마다 바뀝니다. 버전 1.0.0의 매핑 파일로 1.0.1의 에러를 해석할 수 없습니다. CI/CD 파이프라인에서 이 파일을 꼭 저장소(S3 등)에 백업하세요.- 최후의 수단: 정 안되면
minifyEnabled false로 끄고 배포하세요. 일단 앱이 켜지는 게 보안보다 중요합니다.
9. 요약
- Release 에러는 90% 난독화 문제다.
@Keep이나-keep규칙으로 모델(DTO)을 보호해라.mapping.txt를 잘 챙겨라. 나중에 에러 디버깅할 때 생명줄이다.usage.txt를 확인해라. R8이 지운 코드가 거기에 적혀있다.
보안도 중요하지만, 앱이 돌아가는 게 먼저입니다. R8과 타협하는 법을 배우세요.