안드로이드 위클리 공부 노트

#750

Grab은 R8과 프로필 최적화로 ANR을 32%, 시작 시간을 35% 줄였다

원문 Grab Reduces ANRs by 32% and improves startup time by 35% with R8 and Profile guided optimization, Ajesh Pai · Nguyen Van Minh · Tracy Agyemang

한 줄 요약

동남아시아의 슈퍼앱 Grab이 10년 묵은 1,000만 줄짜리 Android 앱에서 R8 최적화를 단계적으로 켜고(호환 모드 → 전체 모드 → 유지 규칙 다듬기), Baseline Profile과 Startup Profile을 함께 넣어 사용자가 체감하는 ANR(앱 응답 없음)을 약 32%, 내려받기 크기를 약 30%, 콜드 스타트(앱이 완전히 꺼진 상태에서 켜는 시간)를 약 35% 줄인 사례다. 핵심은 한 번에 다 켜지 않았다는 것, 그리고 R8이 얼마나 일할 수 있는지를 점수로 재서 CI에서 지킨다는 것이다. Google Android 개발자 블로그의 사례 글이며 코드는 없다.

원문 그림 4장(표지·인용 카드·수치 카드·CI 대시보드)은 원문 서버 주소를 걸어 보여 준다. 원문에는 코드가 없으므로, 이 글의 코드는 모두 Android 개발자 문서의 설명을 따라 직접 쓴 스케치다.

먼저 알아 둘 말

서론 — 10년 된 슈퍼앱의 세 가지 문제

왼쪽에 연두색 강조로 Grab Reduces ANRs by 32% and improves startup time by 35% with R8 and Profile guided optimization 문구, 오른쪽에 Grab 앱 첫 화면이 떠 있는 휴대폰. 화면에는 검색창, 잔액 50.20, 포인트 95,544, Invest·Food·Loans·Mart·Express·Transport·Gift Cards·More 아이콘이 있다
원문 표지. "ANR 32% 감소, 시작 시간 35% 개선"을 크게 쓰고, 옆에 투자·음식·대출·장보기·택배·교통 같은 서비스가 한 화면에 모인 Grab 앱 첫 화면을 보여 준다. 원문 이미지, 원문 서버에서 불러옴 (원문)

1. 출시 직후 ANR이 치솟던 문제를 없애다

연두색 배경의 인용 카드. 왼쪽 흰 상자에 We consistently experienced ANR rate spikes with every weekly app release, Nguyen Van Minh, Lead Software Engineer, Grab. 오른쪽에 산을 배경으로 선 노란 재킷 차림의 인물 사진
인용 카드. Grab 승객 앱 코어 팀의 리드 엔지니어 Nguyen Van Minh은 "매주 앱을 낼 때마다 ANR 비율이 치솟았다"고 말한다. 원문 이미지, 원문 서버에서 불러옴 (원문)

2. 1단계 — R8 최적화 켜기 (2025년 출시)

3. 2단계 — R8 전체 모드로 옮기기 (2026년 출시)

4. 3단계 — 유지 규칙 다듬기

5. 결과

R8 최적화를 단계별로 낸 뒤의 핵심 지표다.

최적화 단계 ANR 크기 콜드 스타트
1단계: R8 최적화 켜기 (2025년) 약 25% 줄었다 16% 줄었다 약 27% 좋아졌다
2단계: R8 전체 모드 (2026년) 10% 더 줄었다 14% 더 줄었다 10% 더 좋아졌다
유지 규칙 다듬기 이 단계의 수치는 원문에 없다 3.6% 더 줄었다 이 단계의 수치는 원문에 없다
전체 누적 약 32.5% 줄었다 약 30.4% 줄었다 약 35% 좋아졌다

6. 기술적인 어려움 세 가지

7. R8이 남긴 장기 효과 — 코드 한 줄의 값이 싸졌다

왼쪽에 초록색 아래 화살표와 40%, reduction in the cost of each line of code. 오른쪽 연두색 칸에 Before R8 optimization every line of production Kotlin cost about 18.7 bytes of download size on average, After the R8 optimizer and full mode this was about 11.2 bytes per line. A 40% reduction in how much each line of code costs going forward to the download size
수치 카드. R8 최적화 전에는 제품 코드 Kotlin 한 줄이 내려받기 크기를 평균 약 18.7바이트 차지했는데, R8 최적화와 전체 모드 뒤에는 약 11.2바이트가 됐다. 앞으로 쓰는 코드 한 줄의 크기 비용이 40% 줄었다는 뜻이다. 원문 이미지, 원문 서버에서 불러옴 (원문)

8. 유지 규칙 관리와 안전장치

어두운 대시보드에 그래프 세 개가 위아래로 있다. 위부터 Shrinking score, Optimization score, Obfuscation score이고 모두 the higher, the better라고 적혀 있다. 가로축은 6월 초부터 9월 하순까지 날짜, 점은 앱 버전 5.416.0부터 5.430.0까지 색으로 구분된다. 축소와 최적화 점수는 72 근처에서 시작해 8월 초와 9월 초에 계단처럼 올라 79 근처에 이르고, 난독화 점수는 81 근처에서 8월에 79 아래로 내려갔다가 9월 초 82 근처로 오른다
그림 1. Grab 빌드의 R8 축소·최적화·난독화 점수를 시간에 따라 보여 주는 CI 대시보드. 앱 버전 5.416.0부터 5.430.0까지 버전마다 점이 찍힌다. 축소·최적화 점수는 8월 초와 9월 초 두 번 크게 오르고, 난독화 점수는 8월에 잠시 떨어졌다가 9월 초에 다시 오른다. 이런 하락을 출시 전에 보려는 것이 이 대시보드의 목적이다. 원문 이미지, 원문 서버에서 불러옴 (원문)

9. 결론

코드로 보기 — Aide 구조로 옮긴 스케치

원문에는 코드가 없다. 아래는 공식 문서를 따라 Aide에 맞춰 직접 쓴 스케치이고, Aide 코드는 고치지 않았다.

① 릴리스 최적화 켜기 — 지금 Aide는 꺼 두었다

지금 Aide의 app/build.gradle.kts 에는 Android Studio 템플릿이 넣어 준 설정이 그대로 있다.

// 지금 Aide (app/build.gradle.kts)
buildTypes {
    release {
        optimization {
            enable = false      // 릴리스에서도 R8이 일하지 않는다
        }
    }
}

공식 문서의 AGP 9.3 이상 방식으로 켜면 이렇다.

// app/build.gradle.kts — AGP 9.3 이상 (직접 쓴 스케치)
buildTypes {
    release {
        optimization {
            enable = true       // 코드 최적화 + 리소스 줄이기를 함께 켠다.
                                // proguard-android-optimize.txt 에 해당하는 기본 규칙이 함께 들어간다
        }
    }
}
# app/src/main/keepRules/aide.keep (직접 쓴 스케치)
# 지금은 비워 둔다. Hilt·Room·kotlinx-serialization·Compose 는 자기 규칙을 함께 배포한다.
# 하나를 더할 때마다 "왜 필요한가"를 이 파일에 한 줄로 적는다 (Grab의 '이유 없는 규칙 금지')

② Baseline·Startup Profile 만들기

Android Studio의 Baseline Profile Generator 모듈 템플릿이 아래 모양의 테스트를 만들어 준다. 처음 출시엔 "앱을 켜서 첫 화면까지"와 "알림을 눌러 일정 화면으로 들어오기" 두 길이면 충분하다.

// baselineprofile/src/main/kotlin/.../AideBaselineProfileGenerator.kt (직접 쓴 스케치)
@RunWith(AndroidJUnit4::class)
@LargeTest
class AideBaselineProfileGenerator {

    @get:Rule
    val rule = BaselineProfileRule()

    @Test
    fun startup() = rule.collect(
        packageName = "com.giwon.aide",
        includeInStartupProfile = true,      // 이 경로는 Startup Profile 에도 넣어 첫 DEX 에 모은다
    ) {
        pressHome()
        startActivityAndWait()               // 런처에서 켜서 첫 화면(AI 홈)이 그려질 때까지
    }
}

③ 설정 점수 보기 — AGP 9.3 이상

# 앱을 다 만들지 않고 보고서만 빠르게 만든다 (공식 문서의 작업 이름)
./gradlew :app:analyzeReleaseR8Config
# → app/build/reports/r8/r8-config-analyzer-release.html

배운 점

Aide에 적용하면

Aide는 아직 출시 전이고, app/build.gradle.kts 의 릴리스 빌드는 optimization { enable = false } 로 R8을 꺼 둔 상태다. Task 1 가이드는 이 줄을 "app만의 출시 설정. 출시 Task 17에서 다시 본다"며 남겼다. 그런데 1-a 계획의 Task 17(내부 테스트 트랙 배포)에는 서명·워크플로·Play Console만 있고 R8·프로필 이야기가 없다. 이 글이 그 빈칸을 채운다.

  1. 릴리스 R8 최적화를 켠다 — 가져온다. 시점은 Task 17, 처음부터 전체 모드로.
    • Grab은 10년 쌓인 리플렉션 때문에 호환 모드부터 갔다. Aide는 새 앱이고 AGP 8.0부터 전체 모드가 기본값이라, 처음부터 전체 모드로 켜는 것이 가장 싸다. android.enableR8.fullMode=false 는 넣지 않는다.
    • Aide의 주요 의존성(Hilt·Room·Compose·Navigation 3·kotlinx-serialization)은 모두 소비자 유지 규칙을 함께 배포한다. 그래서 처음엔 직접 쓰는 유지 규칙이 0줄일 수 있다. Grab에서 Retrofit·Firebase·Compose가 "손댈 것이 없었다"고 한 것과 같다.
    • 확인: 계획서의 실기기 확인 방식대로, 서명한 릴리스 AAB를 내부 테스트 트랙으로 설치해 첫 화면 → 캘린더 → 알림 눌러 들어오기 → 설정을 한 번씩 지나간다. 리플렉션 문제는 대개 "켜자마자 죽음"으로 드러난다.
  2. 어느 DSL로 켤지 — AGP 버전과 부딪힌다. 결정이 필요하다.
    • 공식 문서는 optimization { enable = true } + src/main/keepRules/*.keep 을 AGP 9.3 이상의 방식으로 설명한다. Aide는 9.2.1이고, 이미 optimization {} 블록을 쓰고 있다. 9.2에서 이 블록이 9.3과 똑같이 동작하는지는 문서에 없다(확인하지 않았다).
    • 2026-10-20 결정은 "최신화(Kotlin 2.4 · compileSdk 37)는 Task 4 뒤 별도 PR"이다. 그 최신화 PR에서 AGP도 9.3 이상으로 올리고, Task 17에서는 9.3 방식으로 켜는 것을 추천한다. 그러면 옛 방식(isMinifyEnabled)을 썼다가 다시 바꾸는 일이 없다.
    • 최신화가 Task 17보다 늦어지면, 그때는 9.2에서 enable = true 로 빌드해 보고 동작을 확인한다.
  3. Baseline·Startup Profile — 가져온다. Task 17에서 R8과 같은 PR로.
    • Aide는 Hilt를 쓴다. Grab이 Cloud Profile만으로는 못 덮었다고 한 "시작 때 Dagger 그래프 초기화" 가 Aide에도 그대로 있다. 게다가 개인 앱이라 설치 수가 적어 Cloud Profile이 거의 만들어지지 않는다. 직접 만든 Baseline Profile의 값이 큰 앱이다.
    • 처음 경로는 두 개면 된다: 런처에서 켜서 AI 홈까지, 그리고 리마인더 알림을 눌러 들어오는 길(Aide의 핵심 기능이 알림이다). 둘 다 includeInStartupProfile = true.
    • 새 모듈(baselineprofile)이 하나 생긴다. 이 모듈은 실기기나 에뮬레이터에서 돌아야 하므로 지금 CI(assembleDebug test lint)에는 넣지 않는다. 출시 전에 손으로 돌려 프로필 파일을 커밋한다. CI에서 돌리는 것은 나중 일이다.
    • 계측 테스트(androidTest)를 1-a에서 쓰지 않는다는 Task 1 가이드의 판단과는 조금 부딪힌다. 프로필 생성기는 기기에서 도는 테스트라서다. 다만 이 모듈은 "앱이 맞게 동작하는가"를 검사하는 테스트가 아니라 프로필을 만드는 도구라, 그 결정의 취지는 해치지 않는다고 본다. 사용자 판단이 필요하다.
  4. R8 Configuration Analyzer — 가져온다. 다만 AGP 9.3 이후.
    • ./gradlew :app:analyzeReleaseR8Config 는 AGP 9.3.0 이상이 필요하다. 2번의 최신화 PR 뒤에 첫 점수를 한 번 기록해 두는 것으로 시작한다.
    • CI에 점수 검사를 붙이는 것은 지금은 안 한다. Grab도 아직 "계획 중"이고, 혼자 쓰는 앱에서 유지 규칙이 늘어나는 속도는 느리다. 대신 1번의 aide.keep 파일에 규칙마다 이유를 한 줄 적는 습관으로 Grab의 "이유 없는 규칙 금지"를 대신한다.
  5. mapping 파일 보관 — 바꿔서 가져온다.
    • Aide는 Crashlytics를 쓰지 않는다(수집 없음 정책). 그래서 Play Console에 올리는 AAB에 mapping이 함께 들어가는 것만 확인하고(AGP가 AAB에 넣어 준다), Task 17의 출시 워크플로가 mapping.txt 를 GitHub Actions 산출물(artifact)로 남기게 한다. 출시 뒤 충돌 기록을 원래 이름으로 읽을 수 있어야 한다.
  6. "QA 빌드를 출시와 같은 R8 상태로" — 이미 그렇게 된다.
    • Aide는 Task 17 이후 내부 테스트 트랙의 릴리스 빌드를 직접 써 보며 확인한다(Task 18 1주 실사용). 그 빌드가 곧 R8을 켠 출시 빌드라 따로 할 일이 없다.
  7. app-sizer로 "줄당 비용" 재기 — 지금은 안 한다.
    • 1,000만 줄짜리 앱에서 의미 있는 지표다. 아직 작은 Aide에서는 R8을 켜기 전과 후의 AAB 크기를 한 번 비교해 기록하는 것으로 충분하다.

출처