R8 을 켜고 빌드는 통과했는데 앱이 깨졌다: 15개 앱에서 실제로 걸린 런타임 함정 5가지
Play 의 2027년 코드 최적화 요건에 맞춰 앱 15개에 R8 을 켰습니다. 빌드는 전부 통과했지만 7개 중 2개가 실행 중에 깨졌습니다. Gson 역직렬화, getIdentifier 리소스 조회, Room _Impl, 디버그 빌드의 무효화 등 크래시 로그에 안 잡히는 유형을 정리합니다.
2026년 8월 26일 구글이 Play 신규 품질 요건을 발표했습니다. 2027년 2월부터 DEX 가 앱 10MB(게임 50MB)를 넘는 앱은 코드 최적화(R8)가 필수이고, 지원 문서에는 "선택 사항이 아니며 노출과 게시 기능에 영향을 줄 수 있다"고 적혀 있습니다. 대상 앱 15개에 R8 을 켜고 keep 규칙을 앱마다 맞춰 릴리즈 빌드를 통과시켰습니다. DEX 는 71.6%에서 87.7%까지 줄었습니다.
문제는 그다음이었습니다. 빌드가 통과한 앱 7개를 릴리즈 빌드로 설치해 화면을 눌러 보니 2개가 깨졌습니다. R8 은 "빌드 성공 = 정상"이 성립하지 않습니다.
1. Gson: 역직렬화 대상 패키지를 전부 keep
반려동물 앱이 실행 즉시 죽었습니다.
ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to d82
dto 패키지만 keep 했는데 실제 모델 클래스는 data.repository 에 있었습니다. 제네릭 타입 정보가 지워지자 Gson 이 목표 타입을 몰라 LinkedTreeMap 을 만들었고, 캐스팅에서 터진 것입니다. fromJson 과 TypeToken 호출을 전부 찾아 대상 클래스가 있는 패키지를 모두 keep 해서 해결했습니다. 백업 파일을 읽는 클래스의 내부 클래스(BackupManager$*)까지 포함해야 했습니다.
2. getIdentifier: 리소스 키가 사용자 화면에 그대로 보인다
스크린타임 앱 화면에 tpl_nightowl_2_wit, title_dawn_scroller 같은 문자열이 찍혔습니다. 크래시는 없었습니다. getResources().getIdentifier() 로 이름을 조회하는 코드를 R8 이 보지 못해 리소스 축소가 그 문자열 리소스를 지웠고, id == 0 이면 이름을 그대로 돌려주는 폴백 때문에 조용히 깨진 것입니다.
이 유형은 크래시 감시로는 절대 못 잡습니다. 사용자 눈에만 보입니다. 해결은 res/raw/keep.xml 에 해당 리소스 이름 패턴을 적는 것이고, 전수 검색해 보니 로또 앱(ball_*)과 에러코드 앱(logo_*)도 같은 패턴을 쓰고 있어 예방 조치했습니다. 근본적으로는 getIdentifier 대신 평범한 분기로 매핑하는 편이 낫습니다.
3. Room: _Impl 은 이름으로 찾는다
리모컨 앱 릴리즈를 실기기에 설치하니 앱이 뜨기도 전에 죽었습니다.
Unable to get provider androidx.startup.InitializationProvider
Failed to create an instance of androidx.work.impl.WorkDatabase
Room 은 데이터베이스 클래스 이름 뒤에 _Impl 을 붙여 Class.forName 으로 찾습니다. R8 이 이 참조를 못 보고 삭제했고, WorkManager 가 androidx.startup 프로바이더에서 WorkDatabase 를 만들기 때문에 첫 화면이 그려지기 전에 프로세스가 죽습니다. -keep class **_Impl { *; } 한 줄이 필요합니다. Room 과 WorkManager 를 같이 쓰면 이게 제일 먼저 터집니다.
4. 그 밖에 빌드 단계에서 걸린 것
- 일출·일몰 앱: 사용하는 라이브러리가 컴파일 전용 어노테이션을 참조해
-dontwarn필요. - 리모컨 4종:
R.id를switch에 쓰고 있어 non-final 리소스 ID 와 충돌. 최적화 리소스 축소를 끄고(android.r8.optimizedResourceShrinking=false) 우회했습니다. DEX 축소에는 영향이 없습니다. - 암호 라이브러리(BouncyCastle)는 알고리즘을 클래스명 문자열로 로드합니다. provider 패키지를 keep 하지 않으면 빌드는 통과하고 Wi-Fi 페어링이 런타임에 터집니다.
5. 확인용 빌드는 반드시 non-debuggable
이건 제가 만든 인계 문서가 틀렸던 사례입니다. "디버그 빌드에 R8 만 켠 별도 빌드로 확인하라"고 적어 뒀는데, debuggable 빌드 타입에 isMinifyEnabled = true 를 켜면 AGP 는 경고 한 줄만 내고 난독화와 최적화를 꺼 버립니다.
All code optimizations and obfuscation are disabled for debuggable builds
그 상태로 테스트하면 위의 함정이 하나도 재현되지 않아 "통과"로 오판합니다. 올바른 형태는 release 를 상속하되 isDebuggable = false 로 두고 서명만 디버그 키로 바꾼 별도 빌드 타입입니다. 광고 ID 는 테스트용으로 바꿉니다. 이러면 실기기에 설치할 수 있으면서 R8 은 실제 릴리즈와 같게 돕니다.
같은 이유로, Android Studio 기본 Variant 가 debug 라서 무심코 debug 로 설치해 확인하면 R8 이 아예 안 걸린 앱을 보는 것입니다.
확인 순서
새 앱에 R8 을 켜거나 keep 규칙을 좁힐 때 이 순서로 봅니다.
fromJson/TypeToken을 검색해 대상 클래스의 패키지를 전부 keep.getIdentifier를 검색해 걸리면res/raw/keep.xml.- Room 을 쓰면
**_Implkeep. WorkManager 를 같이 쓰면 필수. - 문자열로 클래스를 로드하는 라이브러리(암호, 직렬화, DI)의 provider 패키지 keep.
- non-debuggable 빌드를 실기기에 설치하고 크래시가 없어도 화면을 눈으로 봅니다. 2번 유형은 눈으로만 잡힙니다.
남은 불확실한 점
구글 문서의 "25% coverage" 가 크기 감소인지 클래스 비율인지 정의가 없습니다. 단정하지 않습니다. R8 기본 설정이면 어느 해석이든 넘습니다. 그리고 R8 을 켰다는 것과 Play 가 요건 충족으로 판정하는 것은 다른 문제라, 최종 확인은 Play Console 의 앱 품질 개요에서 봐야 합니다.