
서론
처음 설치한 앱은 실행하면서 코드를 그때 그때 컴파일하기 때문에 처음 몇 번은 느리고 쓸수록 빨라진다. Baseline Profile을 사용하면 중요한 코드를 첫 실행 전에 미리 번역해 두므로 설치 직후부터 빠른 성능을 낼 수 있는데, Baseline Profile의 원리를 이해하기 위해 기초적인 개념부터 시작해 하나씩 이해해 보겠다.
안드로이드 앱이 실행되기까지의 과정
컴파일이란?
컴파일이란 사람이 이해하는 고수준 언어(e.g kotlin, java...)를 컴퓨터가 이해할 수 있는 0과 1로 이루어진 저수준 언어(기계어)로 번역하는 과정을 말한다. 코틀린으로 작성한 코드가 스마트폰에서 실행되려면 총 3단계를 거친다.
1단계 : Kotlinc Compile

개발자가 작성한 코틀린 코드(.kt) 파일은 컴퓨터가 바로 이해할 수 없기 때문에 JVM이 실행할 수 있는 형태인 JVM 바이트코드(. class)로 만들어야 하며 이 역할을 하는 것이 바로 Kotlinc Compiler이다.
필자가 헷갈렸던 부분은 바이트코드는 아직 기계어가 아니라 JVM이라는 Virtual Machine이 읽는 중간 언어일 뿐 아직 완전한 기계어 형태가 아니라는 점이다. 바이트코드라는 이름은 JVM 명령어의 opcode가 1바이트 단위로 표현되는 데서 왔다. 다만 실제 instruction 전체 길이는 opcode 뒤에 붙는 operand 유무에 따라 더 길어질 수 있다.

IDE의 기능을 활용하면 다음과 같이 Kotlin Bytecode를 확인할 수 있다. Kotlinc Compiler의 컴파일 과정은 다음 글에서 자세히 다뤄보겠다.

2단계 : R8 Compile

1단계에서 생성된 .class 바이트코드는 안드로이드 기기에서 그대로 실행되지 않기 때문에 DEX 형식으로 변환되어야 한다. 이 과정은 기본적으로 D8이 담당하며 릴리즈 빌드에서는 R8이 코드 축소·난독화·최적화를 수행하면서 DEX 생성 과정까지 함께 담당한다.
릴리즈 빌드에서는 필요에 따라 isMinifyEnabled = true로 R8의 축소·난독화·최적화를 적용하여 AAB(Android App Bundle) 형태로 배포되며 Google Play는 이를 바탕으로 기기별 최적화 APK를 생성해 전달한다.

AAB는 설치 가능한 형식이 아닌 게시 포맷으로 모듈식 구조를 사용해 다른 구성에 대한 리소스와 코드를 별개의 번들로 분리한다. Google Play는 이 모듈식 구조를 사용하여 다운로드 시점에 사용자 기기에 최적화된 APK를 생성하여 앱 크기를 줄인다.
3단계 : AOT / JIT

Google Play Store에서 앱을 설치하면 3단계가 시작된다. 앱 설치 및 실행 이후에는 ART(Android Runtime)가 DEX 코드를 해석하거나 필요에 따라 JIT/AOT 컴파일을 통해 기기에서 효율적으로 실행되도록 최적화한다.
안드로이드 앱 실행 방식의 변화
안드로이드의 앱 실행 방식은 버전이 올라가면서 계속 발전해 왔다. 초기에는 코드를 실행할 때마다 한 줄씩 해석하며 실행하는 방식에 가까웠고, 이후에는 자주 실행되는 코드를 미리 번역해 재사용하는 방식, 더 나아가 설치 시점이나 사용자 사용 패턴을 바탕으로 중요한 코드를 선별적으로 최적화하는 방식으로 발전했다.
- Android 1.0 (2008)
인터프리터 방식으로 앱을 실행할 때마다 코드를 한 줄 한 줄 읽고 번역하면서 실행했다. - Android 2.2 Froyo (2010)
JIT(Just-In-Time) 컴파일러 도입
JIT는 앱이 실행되는 도중 자주 호출되는 코드를 감지해 그 부분을 기계어로 컴파일한 뒤 메모리에 저장해 재사용하는 방식이다. 덕분에 이전보다 실행 성능은 크게 좋아졌지만 앱 실행 중에 컴파일 작업이 함께 일어나기 때문에 그만큼 추가적인 런타임 오버헤드가 존재했다. - Android 4.4 Kitkat (2013)
ART(Android Runtime) 실험적으로 도입
ART는 기존 Dalvik과 달리 AOT(Ahead-Of-Time) 컴파일 방식을 사용했는데, 앱을 설치할 때 중요한 코드를 미리 기계어로 변환해 두고 실행 시에는 이미 번역된 결과를 사용하는 방식이다. 이 방식은 앱 실행 성능을 높이는 데 유리했지만 설치 시점의 부담이 커진다는 특징이 있었다. - Android 5.0 Lollipop (2014)
Dalvik이 완전히 사라지고 ART가 기본 런타임으로 대체
이로 인해 앱 실행 속도와 전반적인 반응성은 개선되었지만 앱 설치 시 전체 코드를 미리 컴파일해야 했기 때문에 설치 시간이 길어지고 저장 공간 사용량도 늘어나는 문제가 있었다. - Android 7.0 Nougat(2016)
JIT와 AOT를 함께 사용하는 하이브리드 방식 도입
앱은 우선 빠르게 설치되고 실행되며 실행 중에는 어떤 코드가 자주 사용되는지 프로파일링한다. 이후 자주 사용되는 코드는 기기가 유휴 상태이거나 충전 중일 때 AOT로 추가 최적화하고 그렇지 않은 코드는 JIT에 맡기는 방식이다. - Android 9.0 Pie (2018)
Cloud Profile 도입
Google Play는 많은 사용자의 실행 패턴을 바탕으로 프로파일 데이터를 집계하고 이를 앱 설치 시 최적화에 활용할 수 있도록 했다. 덕분에 사용자는 앱을 처음 설치한 직후에도 다른 사용자들의 실제 사용 데이터를 기반으로 어느 정도 최적화된 상태에서 앱을 시작할 수 있게 되었다.
Cloud Profile은 분명 유용한 방식이지만 충분한 프로파일 데이터가 수집되지 않은 상황에서는 한계가 있다. 예를 들어 앱을 처음 출시했거나 사용자 수가 적은 초기 단계라면 실제 사용자 실행 데이터를 기반으로 한 최적화 효과를 충분히 기대하기 어렵다.
이 경우 앱을 처음 설치하거나 업데이트한 직후에는 중요한 코드 경로가 아직 충분히 AOT 최적화되지 않은 상태일 수 있다. Android는 과거처럼 설치 시점에 앱 전체를 AOT 컴파일하지 않고 런타임 실행 정보와 프로파일을 활용해 점진적으로 최적화를 진행한다.
그래서 초기 실행에서는 ART가 DEX 바이트코드를 해석(interpreter)하며 실행하고 실행 중 자주 호출되는 코드는 JIT 컴파일을 통해 점차 최적화된다. 이후 기기가 적절한 조건에 있을 때 누적된 프로파일 정보를 바탕으로 추가적인 AOT 최적화가 수행되어 다음 실행부터는 더 나은 성능을 낼 수 있다.
하지만 이런 방식은 결국 사용자가 첫 실행이나 초기 몇 번의 실행에서 상대적으로 느린 성능을 경험할 수 있다는 한계가 있다. 이 문제를 완화하기 위해 사용하는 것이 바로 Baseline Profile이다. Baseline Profile을 앱에 포함하면 개발자가 미리 지정한 Critical User Journey에 대해 AOT 최적화를 더 일찍 적용할 수 있어 첫 실행부터 더 빠른 시작 속도와 부드러운 동작을 제공할 수 있다.
Baseline Profile
Baseline Profile은 앱의 중요한 코드 경로를 프로파일로 제공해 앱 설치 후 ART가 해당 경로를 AOT 최적화할 수 있게 해 주는 기술이다. 쉽게 말해 앱이 처음 실행될 때 더 빠르게 동작하도록 미리 컴파일해야 할 코드 목록을 만들어 플레이스토어에 배포한다. 이를 기반으로 Cloud Profile 수집 전이라도, 심지어 앱을 처음 실행하는 사용자에게도 첫 실행부터 빠른 성능을 보장할 수 있다.

이를 프로파일 기반 최적화(PGO, Profile Guided Optimization)이라 하며 Jank 현상을 감소시키고 전반적인 성능 개선을 통해 사용자에게 더 나은 경험을 제공할 수 있다.
Baseline Profile을 사용하기 위해선 Baseline Profile Generator를 사용해 baselineprofile 모듈을 생성해 준다.

모듈을 생성하면 두 개의 클래스가 자동으로 만들어진다.
BaselineProfileGenerator
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule
val baselineProfileRule = BaselineProfileRule()
@Test
fun generate() = baselineProfileRule.collect(
packageName = "패키지네임",
) {
// defines the app's critical user journey
pressHome()
startActivityAndWait()
// 1. Wait until the content is asynchronously loaded
waitForAsyncContent()
// 2. Scroll the feed content
scrollSnackListJourney()
// 3. Navigate to detail screen
goToSnackDetailJourney()
}
}
앱이 처음 실행될 때 더 빠르게 동작하도록 미리 컴파일해 두면 좋은 코드 목록을 만드는 클래스다. 개발자가 사용자의 앱을 사용하는 흐름을 시나리오 형태로 작성하면 그 흐름을 따라 앱을 실행하면서 Baseline Profile을 생성한다.
이렇게 만들어진 프로파일은 baseline-prof.txt 파일로 저장되고 이후 앱 설치 시 AOT 컴파일 최적화에 활용된다. Baseline Profile 생성은 앱의 대표적인 User Journey를 기반으로 작성해야 하는데 공식 코드랩에는 아래처럼 사용자 흐름을 함수로 나눠 작성한다.
- waitForAsyncContent() - 앱 시작
앱이 켜지자마자 바로 측정을 시작지 않고 목록과 실제 콘텐츠가 화면에 나타날 때까지 기다리는 역할을 한다. 네트워크 호출 같은 비동기로 데이터를 불러오는 화면에서 사용자가 실제로 볼 수 있는 상태까지 도달했는지를 확인하는 코드다.
@Composable
private fun SnackCollectionList(
snackCollections: List<SnackCollection>,
filters: List<Filter>,
onSnackClick: (Long) -> Unit,
modifier: Modifier = Modifier
) {
var filtersVisible by rememberSaveable { mutableStateOf(false) }
Box(modifier) {
LazyColumn(
modifier = Modifier.testTag("snack_list"),
) { ... // }
}
}
fun MacrobenchmarkScope.waitForAsyncContent() {
device.wait(Until.hasObject(By.res("snack_list")), 5_000)
val contentList = device.findObject(By.res("snack_list"))
contentList.wait(Until.hasObject(By.res("snack_collection")), 5_000)
}
- scrollSnackListJourney() - 스낵 목록 스크롤
사용자가 목록 스크롤을 재현하며 스크롤 중 자주 실행되는 UI 코드와 렌더링 관련 경로가 프로파일에 포함된 된다.
fun MacrobenchmarkScope.scrollSnackListJourney() {
val snackList = device.findObject(By.res("snack_list"))
snackList.setGestureMargin(device.displayWidth / 5)
snackList.fling(Direction.DOWN)
device.waitForIdle()
}
- goToSnackDetailJourney() - 스낵 상세 화면 이동
스낵 목록의 아이템 하나를 눌러 상세 화면으로 이동한다. 단순히 클릭만 하는 것이 아니라 목록 화면이 사라질 때까지 기다리고 화면 전환이 실제로 완료되었는지 확인한다. 이 과정에서 상세 화면 진입 과정에서 쓰이는 코드도 Baseline Profile에 반영된다.
fun MacrobenchmarkScope.goToSnackDetailJourney() {
val snackList = device.findObject(By.res("snack_list"))
val snacks = snackList.findObjects(By.res("snack_item"))
val index = (iteration ?: 0) % snacks.size
snacks[index].click()
device.wait(Until.gone(By.res("snack_list")), 5_000)
}
StartupBenchmarks
Jetpack Macrobenchmark를 사용해 Baseline Profile이 실제로 앱 시작 성능을 얼마나 개선하는지 측정하는 클래스다. 쉽게 말해 BaselineProfileGenerator가 프로파일을 생성하는 단계이면 StartupBenchmarks는 그 프로파일이 실제로 앱 시작 성능을 얼마나 개선했는지 측정하는 단계다.
@RunWith(AndroidJUnit4::class)
@LargeTest
class StartupBenchmarks {
@get:Rule
val rule = MacrobenchmarkRule()
@Test
fun startupCompilationNone() =
benchmark(CompilationMode.None())
@Test
fun startupCompilationBaselineProfiles() =
benchmark(CompilationMode.Partial(BaselineProfileMode.Require))
private fun benchmark(compilationMode: CompilationMode) {
rule.measureRepeated(
packageName = "패키지명",
metrics = listOf(StartupTimingMetric()),
compilationMode = compilationMode,
startupMode = StartupMode.COLD,
iterations = 10,
setupBlock = {
pressHome()
},
measureBlock = {
startActivityAndWait()
}
)
}
}
CompilationMode
앞서 설명한 것처럼 APK 파일들은 DEX 파일들의 집합이다. 이 DEX 파일들이 언제 어떤 방식으로 컴파일(AOT or JIT) 되는지 알 수 없다. 이러한 환경 속에서 ComplitationMode는 AOT나 JIT 컴파일 방식을 지정해 주는 파라미터다.
| None | - 사전 컴파일 없이 측정 - Baseline Profile 미적용 상태를 시뮬레이션 |
| Partial | - Baseline Profile을 기반으로 AOT 컴파일 - 실제 배포 환경과 가장 유사한 상태 |
| Full | - AOT만 사용해 컴파일 - 실제 기기에서는 거의 발생하지 않아 벤치마크 참고용으로만 사용 |
Partial에는 Baseline Profile 사용 여부를 제어하는 BaselineProfileMode 옵션이 존재한다.
| Require | - Baseline Profile이 반드시 존재해야 하며 없으면 테스트가 실패 - Profile이 정상적으로 포함됐는지 검증할 때 사용 |
| UseIfAvailable | - Baseline Profile이 있으면 사용하고 없으면 그냥 실행 - 프로파일 유무와 관계없이 측정을 이어가고 싶을 때 사용 |
| Disable | - Baseline Profile이 있어도 무시하고 JIT 방식으로만 실행 - Profile 미적용 상태를 명시적으로 재현할 때 사용 |
Metrics
metrics는 measureRepeated에서 무엇을 측정할지 지정하는 파라미터다. 리스트 형태로 여러 개를 동시에 측정할 수 있다.
StartupTimingMetric
앱 시작 시간을 측정하는 Metric으로 내부적으로 두 가지 시간을 측정한다.
- timeToInitialDisplay (TTID)
Choreographer가 UI 렌더링을 시도하며 앱 시작 후 첫 프레임이 화면에 그려질 때까지 걸린 시간을 측정한다. View의 생명주기인 onAttachedToWindow( ) -> onMeasure( ) -> onLayout( ) -> onDraw( ) 중 onDraw( )가 호출됐을 때를 의미한다.
💡Choreographer
하드웨어가 주기적으로 발생시키는 Vsync 신호 수신해 UI 업데이트, 애니메이션등 UI 렌더링 타이밍을 조율하는 클래스
💡VSync(Vertical Synchronization) 신호
디스플레이 하드웨어가 화면을 위에서 아래로 한 줄씩 그리며 한 프레임을 다 그리면 다음 프레임 준비를 위해 발생시키는 타이밍 신호다. VSync가 없으면 GPU가 아무 타이밍에나 화면에 새 프레임을 밀어 넣기 때문에 디스플레이가 한 프레임을 그리는 도중에 GPU가 다음 프레임을 덮어써버리는 Tearing 현상이 발생한다
- timeToFullDisplay(TTFD)
앱 시작 시점부터 측정되며 첫 번째 프레임 렌더링 완료 이후 화면에 필요한 각종 데이터가 UI에 로딩되어 사용자가 앱을 온전히 사용할 수 있기까지의 시점을 의미한다. 단, 안드로이드 시스템은 이 시점을 정확히 알 수 없기 때문에 개발자가 직접 reportFullyDrawn()을 호출해야 측정된다.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp(
onContentReady = {
// 콘텐츠가 완전히 로드된 시점에 호출
reportFullyDrawn()
}
)
}
}
}
reportFullyDrawn()을 호출하지 않으면 TTID만 측정되고 TTFD는 결과에 포함되지 않는다.
StartupTimingMetric 외에도 다양한 Metric이 존재한다.
| FrameTimingMetric | 프레임 렌더링 시간 측정. Jank 발생 여부 확인에 사용 |
| TraceSectionMetric | 특정 커스텀 트레이스 구간의 실행 시간 측정 |
| MemoryUsageMetric | 앱의 메모리 사용량 측정 |
| PowerMetric | 배터리 소모량 측정 (API 29 이상, 실제 기기 필요) |
StartupMode

startupMode는 앱을 어떤 상태에서 시작할지 지정하는 파라미터다.
| COLD | - 앱 프로세스를 완전히 종료한 후 시작 - 가장 오래 걸리며 최악의 시나리오를 측정 |
| WARM | - 앱이 백그라운드에 있다가 다시 포그라운드로 올라오는 상태 - 프로세스는 살아있지만 Activity는 재생성 |
| HOT | - 앱과 Activity 모두 메모리에 남아있는 상태에서 재개 - 가장 빠르며 최선의 시나리오를 측정 |
Baseline Profile은 주로 앱을 처음 실행하는 사용자의 경험을 개선하는 것이 목적이기 때문에 StartupBenchmarks에서는 COLD 모드로 측정하는 것이 일반적이다.
마무리
지금까지 Android 앱의 컴파일 과정부터 시작해 Baseline Profile이 무엇인지, 어떻게 생성하고 측정하는지까지 전체적인 흐름을 살펴봤다. 정리하자면 Android는 Dalvik의 인터프리터 방식에서 출발해 JIT, AOT를 거쳐 현재의 하이브리드 방식으로 발전해 왔다.
그 과정에서 Cloud Profile과 Baseline Profile이 등장했고 Baseline Profile은 개발자가 직접 Critical User Journey를 정의해 첫 실행부터 빠른 성능을 보장할 수 있다는 점에서 강력한 도구다. 앱 시작 속도 개선, Jank 감소, 사용자 이탈률 감소로 이어질 수 있기 때문에 프로덕션 앱이라면 적용을 적극적으로 고려해 볼 만하다.
다음 글에선 실제 프로젝트에서 적용하는 방법에 대해 소개하겠다.
Reference
- [DroidKnights 2025] 송상윤 - Benchmark와 BaselineProfile을 사용해 LazyColumn 스크롤 성능을 75% 개선하기까지의 여정
- MacroBenchmark & Baselin Profile을 사용한 성능 개선 여정 - 1편
- Google Baseline Profile
- Google Baseline Profile Codelab
- 안드로이드 런타임 나무위키
- Android Runtime wikipedia
- Android Baseline Profiles Overview
'Android' 카테고리의 다른 글
| Android Bitmap과 메모리 최적화 (0) | 2026.02.22 |
|---|---|
| Android ExifInterface를 활용해 촬영한 사진이 회전하는 문제 해결하기 (0) | 2026.02.14 |
| Compose 디자인 시스템 설계하기 (0) | 2026.01.22 |
| HiltViewModel 의존성 주입 원리 (0) | 2025.11.06 |
| 아주 쉽게 알아보는 뷰가 그려지기까지의 여정 (0) | 2025.10.29 |
