AX 노트 · 측정

R8이 지우지 못한 테스트 광고 ID 5개: listOf().any 한 줄과 dex 건수 검사

광고 단위 넷을 listOf(...).any로 묶은 조건은 R8이 접지 못해 테스트 문자열 5개를 남겼고, 상수끼리 ||로 쓰자 0개가 됐습니다.

작성 SugarMount Android 개발 읽는 시간 9분 최종 업데이트

한판블록 vc2를 업로드하기 전, 실제 광고 ID로 처음 만든 AAB의 dex를 세어 보니 실제 광고 단위가 4개, Google 테스트 광고 문자열이 5개였습니다. 2026년 10월 4일 오후 5시 16분, Gradle 가드를 통과한 빌드였습니다. 이 AAB는 2분 뒤 빌드가 덮어써 올라가지 않았고, 실제로 업로드한 vc2에는 테스트 문자열이 없습니다. 실제 ID가 다 들어 있으면 두 판 모두 앱은 테스트 단위를 고르지 않습니다.

근거: sugar-game의 git 기록(575aaa1, 1aecbc2, 301bc9e), 출시 리허설 문서, 당시 세션 기록, 2026-10-28에 다시 센 AAB 3개·APK 2개와 R8 재현 프로젝트. ID 값은 출력하지 않았습니다.

목차 (8)
  1. Gradle 가드를 통과한 AAB에 테스트 문자열이 5개 있었습니다
  2. listOf로 묶은 조건은 R8이 빌드 때 계산하지 못했습니다
  3. 리뷰 에이전트는 19건을 찾았고 테스트 문자열은 짚지 않았습니다
  4. 테스트 단위를 debug 소스 세트로 옮기기 어려운 이유
  5. 값을 출력하지 않고 dex의 광고 ID를 세는 명령
  6. 게시자 번호 없이 세면 UMP 문자열 1개가 섞입니다
  7. 화살표 매듭풀기는 아직 태스크 이름 가드입니다
  8. 자주 묻는 질문

Gradle 가드를 통과한 AAB에 테스트 문자열이 5개 있었습니다

Money.kt는 디버그 빌드이거나 BuildConfig의 광고 단위 필드가 비어 있으면 Google 테스트 단위 묶음인 AdUnits.TEST를, 아니면 실제 단위를 고릅니다. 낮 12시 25분에 만든 vc1은 배너 필드 하나만 확인했고, 10월 28일에 그 AAB를 세 보니 실제 광고 단위 4개, 테스트 문자열 0개였습니다.

오후 4시 14분, Claude Code 에이전트가 이 조건을 네 단위 전부로 넓혔습니다. 단위가 하나라도 비어 있으면 실제 ID가 없는 빌드로 보고 넷 모두 테스트 단위를 쓰려는 수정이었고, 주석에는 "never a half-real mix"라고 적었습니다. 아래가 그 줄입니다.

units = if (BuildConfig.DEBUG || listOf(BuildConfig.AD_BANNER, BuildConfig.AD_INTERSTITIAL,
        BuildConfig.AD_REWARDED, BuildConfig.AD_APP_OPEN).any { it.isEmpty() }) AdUnits.TEST
    else AdUnits(BuildConfig.AD_BANNER, BuildConfig.AD_INTERSTITIAL, BuildConfig.AD_REWARDED, BuildConfig.AD_APP_OPEN)

같은 시간대에 리뷰 에이전트가 태스크 이름으로 판단하던 Gradle 가드의 우회 경로를 짚자, 에이전트는 검사를 checkReleaseAdIds 태스크로 옮겨 모든 release 변형이 거치는 preReleaseBuild에 걸었습니다. 이 가드가 보는 것은 입력인 ads.properties뿐입니다.

5시 14분에 커밋(575aaa1)하고 실제 ID로 AAB를 만들었고, 가드는 통과했습니다. 리허설 체크리스트의 이 빌드 단계가 요구한 것은 가드 통과와 서명 확인이었는데, 에이전트는 dex 문자열도 grep -c로 함께 셌습니다(값은 출력하지 않음). 자기 게시자 번호가 4, 테스트 게시자 번호가 5였고, AdUnits.TEST의 문자열 5개(배너, 전면, 보상형, 보상형 전면, 앱 오프닝)가 모두 dex에 있었습니다.

16시 14분 수정의 주석과 리뷰 요청은 런타임 동작만 다뤘고, 빌드한 뒤 dex를 세기 전까지는 R8이 그 분기를 지우는지 아무도 확인하지 않았습니다.

단계 그림: 12시 25분 vc1은 실제 광고 단위 4개와 테스트 0개, 16시 14분 조건을 listOf any로 넓힘, 16시 49분 코드 리뷰는 19건을 지적했지만 테스트 문자열은 놓침, 17시 16분 업로드 전 첫 실제 ID AAB는 가드를 통과했지만 테스트 문자열 5개, 17시 18분 상수끼리 or로 바꾸자 테스트 0개.
가드를 통과하고 코드 리뷰에서도 지적되지 않은 첫 실제 ID AAB의 dex에 테스트 문자열이 5개, 2분 뒤 빌드에는 0개였습니다.

listOf로 묶은 조건은 R8이 빌드 때 계산하지 못했습니다

실제 ID 빌드에서 이 조건은 늘 false인데도, R8은 그 값을 빌드 때 정하지 못해 AdUnits.TEST로 가는 분기를 살려 두었습니다. 10월 28일 재현에서는 가짜 문자열로 같은 모양의 BuildConfig와 5개짜리 테스트 묶음을 만들어 두 조건을 Kotlin 2.3.21로 컴파일하고, AGP 9.4.1에 들어 있는 R8 9.4.24로 proguard-android-optimize.txt를 그대로 써서 줄였습니다. listOf 판의 dex에는 테스트 문자열 5개와 실제 4개가 남았고, || 판에는 실제 4개만 남았습니다.

javap로 보면 두 판 모두 kotlinc가 BuildConfig 상수를 문자열 리터럴로 이미 박아 넣었습니다. listOf 판은 그 넷을 배열에 담아 listOf로 리스트를 만들고, Iterator로 돌며 원소마다 length()를 부릅니다. R8 출력 dex에도 listOf가 Arrays.asList로 바뀌었을 뿐 Iterator 루프가 그대로 남았습니다. || 판은 리터럴마다 length()를 바로 부르고, R8은 그 값을 계산해 조건 전체를 false로 접었습니다. 배너 하나만 보던 vc1도 이 모양이라 테스트 문자열이 0개였습니다.

비교 그림: listOf any 모양은 리스트와 Iterator 루프를 거쳐 R8이 조건을 계산하지 못해 테스트 문자열 5개가 남고, 상수끼리 or 모양은 R8이 조건을 false로 계산해 테스트 문자열 0개.
같은 뜻의 조건인데 listOf 판은 테스트 문자열 5개를 남기고, || 판은 0개를 남겼습니다.

테스트용 대체 분기는 BuildConfig 상수끼리 ||로 쓰고, 컬렉션 함수로 묶지 않습니다(잰 것은 any 하나입니다). R8 결과를 숫자로 읽은 다른 기록은 Play Console 'DEX 최적화 39%'의 범인에 있습니다.

리뷰 에이전트는 19건을 찾았고 테스트 문자열은 짚지 않았습니다

4시 49분의 리뷰 에이전트는 코드만 읽고 빌드는 하지 않는 조건으로 변경 전체를 봤습니다. 요청 목록에 "광고 단위 중 하나라도 비면 전부 TEST"와 "R8에서 깨질 것(Review API keep 규칙)"이 있었습니다. 높음 2건, 중간 6건, 낮음 11건을 찾았지만 테스트 문자열이 릴리스에 남는다는 지적은 없었습니다.

Gradle 가드는 입력만 보므로 이 문제를 볼 수 없었습니다. 에이전트는 4시 55분에 측정용(-PallowTestAds) 빌드의 dex를 같은 방식으로 세어 리허설 0-4행에 "실제 0건, 테스트 5건"으로 적어 두었고, 5시 16분에는 같은 확인을 실제 ID 빌드에 붙였습니다. 리허설 문서 머리말은 비밀값을 적지 않게 합니다. 업로드 전 검사 전체는 AI가 만든 Android 앱, 출시 전에 무엇을 검사하나에 있습니다.

5시 18분에 1aecbc2로 고쳤습니다. 에이전트는 이 수정을 "실제 ID 빌드에서 테스트 ID 문자열을 없애는 변경"이라고 설명했습니다. 5시 42분 커밋(301bc9e)에서 실제 4, 테스트 0을 다시 확인했습니다.

테스트 단위를 debug 소스 세트로 옮기기 어려운 이유

테스트 단위를 debug 소스 세트로 옮기면 release에서 아예 빠지지만, 한판블록에는 맞지 않습니다. 기기 측정과 몽키 테스트에 쓰는 -PallowTestAds 빌드가 release 변형이고, 이 빌드는 ads.properties를 읽지 않고 테스트 단위로 돌아야 합니다. 실제 ID 빌드에 자동 탭을 하면 무효 클릭이 되기 때문입니다.

그대로 두면 실제 ID 빌드에 테스트 문자열 5개가 늘 남아 "테스트 0개"를 통과 조건으로 쓸 수 없고, 남은 문자열이 죽은 분기인지를 매번 따져야 합니다. 그래서 조건을 R8이 접는 모양으로 바꿨습니다. 지금은 빌드 전에 입력 가드를 걸고, 빌드 뒤에는 dex 건수를 손으로 셉니다.

값을 출력하지 않고 dex의 광고 ID를 세는 명령

릴리스에 무엇이 들어갔는지는 코드 리뷰로 판정하지 마십시오. 산출물의 dex를 건수로 세십시오. 아래는 10월 28일 재측정에 쓴 Git Bash 명령입니다. 게시자 번호는 파일에서 변수로만 읽고 화면에 찍지 않습니다.

# 1. 자기 게시자 번호를 변수로만 읽는다(출력하지 않음)
MY_PUB=$(sed -n 's/^admob\.banner=[^0-9]*\([0-9]*\)\/.*/\1/p' app/ads.properties)
TEST_PUB=...   # Google 테스트 광고 문서에 나오는 테스트 게시자 번호(16자리)
A=app/build/outputs/bundle/release/app-release.aab   # APK면 'classes*.dex'

# 2. 자기 광고 단위 수: 쓰는 광고 단위 개수와 같아야 한다
unzip -p "$A" 'base/dex/*.dex' | grep -a -o "$MY_PUB/[0-9]*" | sort -u | wc -l
# 3. 테스트 문자열 수: 실제 ID 빌드면 0
unzip -p "$A" 'base/dex/*.dex' | grep -a -o "$TEST_PUB[~/][0-9]*" | sort -u | wc -l

이 명령을 vc1과 vc2, 10월 25일 AAB에 돌리면 4와 0이, 같은 날의 -PallowTestAds APK에 돌리면 0과 5가 나옵니다. 빌드 전 입력 검사는 태스크 이름을 추측하지 말고 preReleaseBuild에 겁니다.

tasks.matching { it.name == "preReleaseBuild" }.configureEach {
    if (!allowTestAds) dependsOn(checkReleaseAdIds)
}

게시자 번호 없이 세면 UMP 문자열 1개가 섞입니다

10월 28일 재측정에서 광고 ID 접두어만으로 세면 AAB 셋 모두 5개였습니다. 넷은 자기 광고 단위('/' 형식)이고, 하나는 다른 게시자의 앱 ID 형식('~') 문자열이었습니다. Gradle 캐시의 Google Play 서비스, 동의 화면 라이브러리 UMP(user-messaging-platform), Firebase 라이브러리 파일 135개를 뒤져 보니 UMP 4.0.0에 같은 문자열이 있었습니다. 자기 게시자 번호와 '/'를 함께 넣어 세야 4가 나옵니다.

-PallowTestAds 빌드는 테스트 문자열 5개가 정상이므로, 이 검사는 실제 ID 빌드에만 겁니다. 빌드 뒤에 세는 방식이라 막을 수 있는 것은 업로드뿐입니다. R8 재현은 9.4.24 한 버전과 BuildConfig 문자열 상수 모양으로만 했고, 광고 ID를 리소스나 원격 설정에서 읽는 구조는 재 보지 않았습니다. 매니페스트의 앱 ID는 dex에 없어서 체크리스트 1-7행에서 따로 확인합니다.

화살표 매듭풀기는 아직 태스크 이름 가드입니다

같은 저장소의 화살표 매듭풀기(app-arrows)는 10월 27일 커밋 기준으로 배너 필드 하나만 확인하고, 릴리스 가드도 태스크 이름에 release가 들어 있는지로 판단합니다. 이 앱을 checkReleaseAdIds 방식으로 옮기고, dex 건수 검사를 저장소 스크립트로 넣는 일이 남았습니다.

자주 묻는 질문

listOf().any로 쓰면 앱 동작이 달라지나요?

달라지지 않습니다. 실제 ID가 모두 있으면 두 판 모두 실제 단위를 고릅니다.

all이나 none으로 묶어도 R8이 분기를 남기나요?

재 보지 않았습니다. 확인하기 전에는 상수끼리 ||로 씁니다.

Windows에서도 이 명령을 쓸 수 있나요?

Git Bash에 unzip, grep, sort, wc가 들어 있어 그대로 돌아갑니다.

이 글은 SugarMount가 직접 측정한 값으로 쓰고, 초안 작성에 AI 코딩 에이전트를 썼습니다.

Android 개발 글 전체 보기 · AX 노트 전체 보기