
내 코드를 훔쳐보지 마세요 (난독화와 Release 에러)
Debug에선 잘 되는데 Release에서만 죽나요? 범인은 '난독화'입니다. R8의 원리, Mapping 파일 분석, 그리고 Reflection을 사용하는 라이브러리를 지켜내는 방법(@Keep)을 정리해봤습니다.

Debug에선 잘 되는데 Release에서만 죽나요? 범인은 '난독화'입니다. R8의 원리, Mapping 파일 분석, 그리고 Reflection을 사용하는 라이브러리를 지켜내는 방법(@Keep)을 정리해봤습니다.
프론트엔드 개발자가 알아야 할 4가지 저장소의 차이점과 보안 이슈(XSS, CSRF), 그리고 언제 무엇을 써야 하는지에 대한 명확한 기준.

로그인 화면을 만들었는데 키보드가 올라오니 노란 줄무늬 에러가 뜹니다. resizeToAvoidBottomInset부터 스크롤 뷰, 그리고 채팅 앱을 위한 reverse 팁까지, 키보드 대응의 모든 것을 정리해봤습니다.

안드로이드는 오는데 iOS는 조용합니다. 혹은 앱이 켜져 있을 때만 옵니다. Background/Terminated 상태 처리, APNs 인증서, 그리고 Notification Channel 설정까지 완벽하게 해결합니다.

안드로이드는 Xcode보다 낫다고요? Gradle 지옥에 빠져보면 그 말이 쏙 들어갈 겁니다. minSdkVersion 충돌, Multidex 에러, Namespace 변경(Gradle 8.0), JDK 버전 문제, 그리고 의존성 트리 분석까지 완벽하게 해결해 봅니다.

개발할 땐 완벽했습니다. 그런데 스토어에 올린 앱(Release 빌드)을 실행하자마자 바로 죽어버립니다. 로그를 까보니 외계어가 나옵니다.
java.lang.NullPointerException: ... at a.b.c.d(Unknown Source)
a.b.c.d가 도대체 뭘까요?
이것은 안드로이드의 문지기, R8(구 ProGuard)이 다녀간 흔적입니다.
코드를 압축하고 난독화하면서 클래스 이름을 다 바꿔버린 거죠.
안드로이드 릴리즈 빌드는 기본적으로 R8 컴파일러를 거칩니다. R8은 3가지 일을 합니다.
a, b, c로 바꿔서 리버싱(해킹)을 어렵게 합니다.문제는 "안 쓴다고 생각해서 지웠는데, 사실은 쓰는 코드"일 때 발생합니다. 특히 Reflection(이름으로 함수/클래스 찾기)을 쓰는 라이브러리(Retrofit, Gson)나 JNI(Native Code) 호출이 여기서 박살납니다.
서버에서 온 JSON을 User 객체로 바꿀 때, 라이브러리가 User라는 클래스 이름을 찾습니다.
그런데 R8이 User를 a로 바꿔버렸거나, "이 클래스 코드에서 직접 호출 안 하네?" 하고 지워버렸습니다.
그럼 ClassNotFoundException이나 NoSuchMethodError가 납니다.
@Keep 어노테이션 (추천)가장 깔끔한 방법입니다. 지켜야 할 클래스 위에 @Keep만 붙이면 됩니다.
이 어노테이션이 붙은 친구들은 R8이 절대 건드리지 않습니다.
import androidx.annotation.Keep
@Keep
data class User(val name: String)
외부 라이브러리라서 코드를 수정할 수 없다면, 규칙 파일에 적어야 합니다.
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'
}
}
난독화된 에러 로그(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이 무엇을 삭제했는지 목록이 나옵니다. 내 코드가 왜 사라졌는지 궁금하면 이걸 보세요.
코드만 줄이는 게 아닙니다. R8은 안 쓰는 이미지나 레이아웃 파일도 줄일 수 있습니다.
build.gradle에서 shrinkResources true를 켜면 됩니다.
release {
minifyEnabled true // 코드 난독화 & 축소
shrinkResources true // 리소스 축소 (minifyEnabled가 true여야 작동)
}
주의할 점은, 코드에서 동적으로 리소스 이름(resId)을 만들어 접근하는 경우(예: getResources().getIdentifier()) R8이 "이 리소스 안 쓰네?" 하고 지워버릴 수 있습니다.
이럴 땐 res/raw/keep.xml 파일을 만들어서 명시적으로 보존해야 합니다.
배포 전, 혹은 배포 직후 앱이 죽는다면 당황하지 말고 순서대로 확인하세요.
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로 끄고 배포하세요. 일단 앱이 켜지는 게 보안보다 중요합니다.@Keep이나 -keep 규칙으로 모델(DTO)을 보호해라.
mapping.txt를 잘 챙겨라. 나중에 에러 디버깅할 때 생명줄이다.usage.txt를 확인해라. R8이 지운 코드가 거기에 적혀있다.보안도 중요하지만, 앱이 돌아가는 게 먼저입니다. R8과 타협하는 법을 배우세요.