
지난달에 픽셀 7(Pixel 7)에 갓 설치한 앱을 처음 켰는데, 첫 화면 뜨는 데 324ms가 걸리더라고요. 두 번째 실행부턴 멀쩡하고요. 이 첫 실행 페널티를 Baseline Profiles로 손봤습니다.
같은 코드, 같은 기기인데 설치 직후 첫 실행만 통역사를 붙인 것처럼 굼떴어요. 원인 진단부터 Macrobenchmark로 시작 시간 재는 법, 프로파일 생성해서 릴리즈에 태우고 전후 숫자 비교까지, 제가 밟은 순서 그대로 풀어보겠습니다.
안드로이드 콜드 스타트가 왜 내 코드 탓처럼 느껴졌나
증상부터 정리하면 이래요. 설치나 재부팅 직후 첫 실행만 느리고, 두 번째부터는 멀쩡합니다. 코드를 아무리 뜯어봐도 이상한 데가 없어서 한참 헤맸어요.
범인은 제 코드가 아니라 ART(안드로이드 런타임)의 동작 방식이었습니다. 앱을 처음 켤 때 ART는 dex 바이트코드를 미리 컴파일해두지 않고, 일단 한 줄씩 해석해가며(인터프리테이션) 실행해요. 자주 도는 메서드가 감지되면 그제야 JIT로 컴파일하는데, 그 컴파일 비용이 고스란히 첫 화면 그리는 시간에 얹히는 구조더라고요. 코드 품질과 무관한 "첫 실행 페널티"가 존재한다는 얘기입니다.
다만 여기서 못 박고 싶은 게 하나 있어요. 콜드 스타트가 느리다고 무조건 이 도구부터 꺼내면 안 됩니다. Application.onCreate에서 무거운 초기화를 몰아 하거나, 동기 I/O로 파일을 읽거나, 필요도 없는 라이브러리를 시작하자마자 다 띄우고 있다면 진짜 원인은 거기예요. 그런 병목은 지연 초기화(lazy init)나 App Startup 라이브러리로 먼저 잡는 게 순서라고 봅니다. Baseline Profiles는 만능이 아니거든요.

Baseline Profile이 컴파일하는 대상과 Cloud Profile 관계
Baseline Profile을 쉽게 말하면, 핵심 사용자 경로에서 자주 실행되는 메서드와 클래스 목록을 적어둔 파일이에요. 빌드할 때 이 목록이 APK/AAB 안 assets/dexopt/baseline.prof로 들어가고, 설치 시점에 ART가 그 메서드들을 미리 AOT 컴파일해둡니다. 첫 실행 때 해석하고 JIT 돌리는 단계를 건너뛰니까 빨라지는 원리예요.
온디바이스 흐름은 ProfileInstaller가 기기 SDK에 맞게 프로파일을 변환하고, 기기가 놀고 있을 때 bg-dexopt가 실제 AOT 컴파일을 돌리는 식으로 알려져 있어요.
근데 여기서 많이들 빠뜨리는 게 Cloud Profile입니다. 사실 앱은 이미 사용자들을 프로파일링당하고 있어요. 구글 플레이(Google Play)가 실사용자 프로파일을 모아서 집계한 뒤, 뒤에 설치하는 사람들한테 배포해주는 구조예요. 그러니까 널리 깔린 앱이라면 Cloud Profile이 이미 상당 부분 최적화를 해주고 있는 셈입니다.
그럼 Baseline Profile은 왜 필요하냐. 새 버전을 막 배포한 직후엔 그 버전에 대한 Cloud Profile이 아직 없어요. 이 공백기를 메워주는 게 Baseline Profile입니다. 출시 직후 첫 사용자를 위한 보험 같은 존재고, 시간이 지나면 Cloud Profile에 자동으로 병합됩니다. 그래서 최대 수혜자는 "출시 직후 맨 처음 설치한 사람"이라는 얘기가 됩니다.

Macrobenchmark로 콜드 스타트 시작 시간 측정하기
숫자로 찍기 전엔 손대지 않는다, 이게 제 원칙이에요. 그래서 젯팩 Macrobenchmark부터 붙였습니다.
여기서 제일 조심할 건 무엇을 재느냐예요. TTID(첫 프레임이 그려지는 시점)와 TTFD(화면이 실제로 쓸 만하게 다 채워진 시점)는 다른 숫자입니다. 화면에 데이터까지 다 채워졌을 때 reportFullyDrawn()을 호출하느냐 마느냐에 따라 TTFD 숫자가 통째로 바뀌거든요. 이 구분을 안 하면 전후 비교가 엉킵니다.
측정 함정도 몇 개 미리 짚어둘게요.
- debug 빌드에선 효과가 안 나옵니다. release 빌드에
ProfileInstaller가 동작해야 유효해요. - 워밍업 실행을 안 걸러내면 첫 실행 페널티가 희석돼서 오판하게 됩니다.
- 플래그십에서만 재면 "효과 없다"고 착각하기 쉬워요. 저사양 기기일수록 JIT 오버헤드가 커서 개선폭도 큰 경우가 많습니다.
실기기가 여러 대 없어도 괜찮습니다. Gradle Managed Devices(GMD)로 정해진 가상 기기를 띄워서 재현할 수 있어요. 그리고 CompilationMode를 None과 프로파일 적용 상태로 나눠서 돌리면, 다른 변수 섞이지 않은 순수 프로파일 효과만 뽑아낼 수 있습니다. 저는 이 비교부터 돌려보고 나서야 손을 댔어요.

baseline-prof.txt 생성하고 릴리즈 빌드에 태우기
측정 틀이 잡혔으면 이제 프로파일을 만들 차례예요. BaselineProfileRule의 collect에 패키지명을 넘기고, UIAutomator로 핵심 경로를 실제로 구동시킵니다. 첫 화면 진입, 리스트 스크롤처럼 사용자가 실제로 밟는 길을 눌러주면 그때 실행되는 메서드들이 캡처돼요.
@Test
fun generate() = baselineProfileRule.collect(
packageName = "com.example.app"
) {
startActivityAndWait()
// 실제 사용자가 밟는 핫 패스를 그대로 구동
device.findObject(By.res("feed_list")).fling(Direction.DOWN)
}
이걸 돌리면 macrobenchmark 모듈 빌드 산출물로 baseline-prof.txt가 나옵니다. Gradle Baseline Profile 플러그인을 붙여두면 이 프로파일을 release 빌드에 자동으로 번들해줘요. 저는 빌드하고 나서 APK/AAB를 열어 assets/dexopt/baseline.prof가 실제로 들어갔는지 꼭 확인하는 편입니다. 설정만 해두고 안 들어간 채 배포하는 사고가 은근히 나거든요.
솔직하게 유지보수 얘기도 하나 짚을게요. 이 프로파일은 코드가 바뀌면 낡습니다. 핫 패스가 달라지면 예전 프로파일이 엉뚱한 메서드를 컴파일하고 있게 되니까요. CI에 Macrobenchmark나 GMD를 연동해서 자동으로 갱신하지 않으면, 시간이 지날수록 있으나 마나 한 파일이 되더라고요. 소규모 팀이라면 이 셋업 비용과 실행 시간까지 계산에 넣고 시작하시는 게 맞다고 봐요.
전후 지표 비교와 냉정한 결론
제 픽셀 7 기준으로는 콜드 스타트가 324.8ms에서 229.0ms로 줄었습니다. 약 29% 당긴 셈이에요. 스크롤 구간의 버벅임(jank)도 핫 메서드를 미리 컴파일해둔 덕에 같이 부드러워졌습니다.
구글 공식 설명으로는 해당 코드 경로가 약 30% 빨라진다고 하고, 콜드 스타트 기준 2540% 개선이 흔하다고 알려져 있어요. 조마토(Zomato)는 새 코드 한 줄 없이 시작 속도를 20% 넘게 줄였다는 사례도 있고요. 다만 4050%까지 얘기하는 사례들은 조건이 잘 맞았을 때의 최선값이라고 보시는 게 안전합니다. 이미 최적화가 끝난 앱이라면 체감은 한 자릿수에서 30%대 사이일 수 있어요.
맞바꿔야 하는 것도 정직하게 적을게요. AOT로 미리 컴파일한 코드가 디스크를 더 씁니다. 그리고 설치하자마자 곧바로 실행하면 아직 bg-dexopt가 돌기 전이라 100% 최적화된 상태가 아닐 수도 있어요. 그럼에도 저는 이게 "출시 직후 첫 사용자 경험"을 지켜주는 값어치가 있다고 봅니다.
제가 이 작업하면서 제일 크게 배운 건, 프로파일 파일 하나 붙였다고 끝이 아니라는 점이었어요. 개선의 절반은 사실 구글 플레이가 알아서 돌려주는 Cloud Profile 몫이고, Baseline은 그 공백기를 메우는 임시방편에 가깝습니다. 그러니 이걸 도입할지 말지는 "우리 앱이 자주 새 버전을 내는가, 첫 설치 경험이 얼마나 중요한가"로 판단하시면 되겠어요.
저도 처음엔 픽셀 7 하나만 붙들고 재보다가 "생각보다 별거 없네" 하고 김이 샜었거든요. 그날 저녁에 서랍에서 몇 년 묵은 보급형 단말을 하나 꺼내 다시 물려놓고 돌려봤더니, 그제야 숫자가 확 벌어지더군요. 효과가 갈리는 자리는 딱 거기, 낡고 느린 기기 위에 있었습니다.

'Android 개발 > 트러블슈팅' 카테고리의 다른 글
| 별 1개 "배터리 도둑" 리뷰, 일주일간 직접 재본 앱 배터리 소모량 일지 (0) | 2026.10.25 |
|---|---|
| targetSdk 36 안 올리면 8월 31일부터 신규 설치가 막힙니다 — D-12에 챙긴 것들 (0) | 2026.10.24 |
| 코루틴 테스트 runTest로 delay 5초를 밀리초로 줄인 트러블슈팅 기록 (0) | 2026.10.19 |
| 안드로이드 StrictMode로 '가끔 뚝 끊김'을 매번 재현되는 위반으로 바꾼 일지 (0) | 2026.10.18 |
| DataStore 마이그레이션 실전기: 설정값 손실 없이 SharedPreferences 걷어낸 일지 (0) | 2026.10.13 |