EEltaCrew
블로그

R8 을 켜고 빌드는 통과했는데 앱이 깨졌다: 15개 앱에서 실제로 걸린 런타임 함정 5가지

게시 2026-09-16실측 기준 2026-09-12읽는 시간 약 5분elta · EltaCrew

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 을 만들었고, 캐스팅에서 터진 것입니다. fromJsonTypeToken 호출을 전부 찾아 대상 클래스가 있는 패키지를 모두 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. 그 밖에 빌드 단계에서 걸린 것

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 규칙을 좁힐 때 이 순서로 봅니다.

  1. fromJson / TypeToken 을 검색해 대상 클래스의 패키지를 전부 keep.
  2. getIdentifier 를 검색해 걸리면 res/raw/keep.xml.
  3. Room 을 쓰면 **_Impl keep. WorkManager 를 같이 쓰면 필수.
  4. 문자열로 클래스를 로드하는 라이브러리(암호, 직렬화, DI)의 provider 패키지 keep.
  5. non-debuggable 빌드를 실기기에 설치하고 크래시가 없어도 화면을 눈으로 봅니다. 2번 유형은 눈으로만 잡힙니다.

남은 불확실한 점

구글 문서의 "25% coverage" 가 크기 감소인지 클래스 비율인지 정의가 없습니다. 단정하지 않습니다. R8 기본 설정이면 어느 해석이든 넘습니다. 그리고 R8 을 켰다는 것과 Play 가 요건 충족으로 판정하는 것은 다른 문제라, 최종 확인은 Play Console 의 앱 품질 개요에서 봐야 합니다.

AndroidR8Google Play코드 최적화

함께 읽기

판매자 계정 하나 켰다가 앱 21개 출시가 전부 막혔다: 한국 개인 개발자와 Play Console 「추가 정보 필요」후원용 인앱 상품을 테스트하려고 판매자 계정을 만든 직후, Play Console 이 사업자 등록 번호·통신판매업 신고번호를 요구하며 전 앱의 출시를 막았습니다. 상품을 지워도, 지급 계좌를 지워도 풀리지 않았고 지원팀의 최종 답은 "되돌릴 수 없다" 였습니다. 개인 개발자가 이 함정을 피하는 방법을 씁니다.데이터 보안 양식, 선언 0 인 앱이 적발되기 시작했다: 앱 21개 전수 감사와 CSV·API 로 고치는 법사용자가 손으로 입력한 남의 전화번호도 구글 기준 "전화번호 수집" 이었습니다. 거부를 한 번 받고 앱 21개의 데이터 보안 선언을 소스와 대조하니 11개가 어긋나 있었고, 그중 3개는 선언이 아예 0 이었습니다. 광고만 쓰는 앱도 선언 0 이면 안 되는 이유와, 5단계 마법사 대신 CSV 와 API 로 고치는 방법을 씁니다.Play 검색 1위 앱의 제목은 검색어 문자열 그대로다: 가운뎃점 하나가 검색 매칭을 통째로 죽인 이야기한국 Play 검색 결과 157개 검색어를 실측해 보니, 1위 앱은 예외 없이 제목에 그 검색어를 띄어쓰기까지 그대로 넣고 있었습니다. 우리 앱은 예쁘게 쓰려고 넣은 가운뎃점(·) 때문에 QR 검색어에 한 번도 안 걸리고 있었습니다. 13개 앱 제목을 바꾼 근거를 씁니다.