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

Debug에선 잘 되는데 Release에서만 죽나요? 범인은 '난독화'입니다. R8의 원리, Mapping 파일 분석, 그리고 Reflection을 사용하는 라이브러리를 지켜내는 방법(@Keep)을 정리해봤습니다.
로그인 화면을 만들었는데 키보드가 올라오니 노란 줄무늬 에러가 뜹니다. resizeToAvoidBottomInset부터 스크롤 뷰, 그리고 채팅 앱을 위한 reverse 팁까지, 키보드 대응의 모든 것을 정리해봤습니다.

프론트엔드 개발자가 알아야 할 4가지 저장소의 차이점과 보안 이슈(XSS, CSRF), 그리고 언제 무엇을 써야 하는지에 대한 명확한 기준.

안드로이드는 오는데 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과 타협하는 법을 배우세요.