안드로이드 런타임(ART), Dalvik, 그리고 Dex 컴파일러
안드로이드 런타임(ART), Dalvik, 그리고 Dex 컴파일러
안드로이드 애플리케이션은 디바이스에서 실행되기 전에 고유한 컴파일 및 실행 파이프라인을 거칩니다. 안드로이드 런타임(ART), ART의 전신인 Dalvik, 그리고 Dex 컴파일러는 각각 애플리케이션 코드를 제한된 하드웨어 환경에서 효율적으로 실행할 수 있도록 최적화된 형태로 변환하는 역할을 담당합니다. 이 세 가지 컴포넌트가 어떻게 상호작용하는지를 이해하는 것은 면접에서 자주 등장하는 주제입니다. 플랫폼이 성능, 메모리, 하위 호환성(backward compatibility)을 시스템 수준에서 어떻게 관리하는지를 파악하고 있는지를 평가할 수 있기 때문입니다.
면접에서 단순히 "ART가 Dalvik을 대체했다" 정도의 답변은 충분하지 않습니다. 왜 대체되었는지, 컴파일 전략의 차이가 앱 성능에 구체적으로 어떤 영향을 미치는지를 설명할 수 있어야 합니다. 이번 면접 질문을 통하여 아래 내용들을 학습하실 수 있습니다.
- AOT(Ahead-of-Time) 컴파일과 JIT(Just-in-Time) 컴파일 전략의 차이점을 명확히 설명할 수 있습니다.
- ART가 Dalvik을 대체한 배경과 그에 따른 성능상의 이점을 서술할 수 있습니다.
- Dex 컴파일러가 생성하는 결과물과
.dex포맷이 존재하는 이유를 설명할 수 있습니다. - 멀티덱스(multi-dex) 지원이 64K 메서드 제한을 어떻게 해결하는지 파악할 수 있습니다.
- 프로파일 가이드 최적화(PGO)가 최신 ART에서 AOT와 JIT를 어떻게 결합하는지 이해할 수 있습니다.
Dalvik의 JIT(Just-in-Time) 컴파일
Dalvik은 안드로이드 1.0부터 4.4까지 사용된 최초의 런타임입니다. 메모리와 연산 능력이 제한된 디바이스를 위해 설계된 레지스터 기반 가상 머신(register-based VM)으로, 스택 기반(stack-based)인 Java 가상 머신(JVM)과는 구조적으로 다릅니다. 레지스터 기반 명령어 방식은 하나의 연산을 수행하는 데 필요한 명령어 수를 줄여주기 때문에, 제한된 하드웨어에서 더 효율적으로 동작할 수 있습니다.
Dalvik은 JIT(Just-in-Time) 컴파일 방식을 사용합니다. 애플리케이션이 실행되면 Dalvik은 바이트코드를 인터프리팅하면서, 자주 실행되는 코드 경로(핫스팟, hot spots)를 런타임에 네이티브 머신 코드로 컴파일합니다. 이는 앱이 시작될 때마다 런타임이 핫 경로를 다시 탐색하고 재컴파일해야 한다는 것을 의미합니다.
// Dalvik은 앱 시작 시마다 이 바이트코드를 인터프리팅하고,
// 자주 실행되는 루프를 런타임에 JIT 컴파일합니다
fun computeSum(items: List<Int>): Int {
var sum = 0
for (item in items) {
sum += item
}
return sum
}
JIT 컴파일은 설치 시점에 컴파일이 발생하지 않으므로 설치 시간을 단축해 줍니다. 하지만 실행 중 CPU 사용량이 증가하고, 런타임이 아직 핫 경로를 식별하여 컴파일하지 못한 상태이므로 시작 속도가 느려집니다. 앱을 재시작할 때마다 이 워밍업 과정을 반복해야 하기 때문에, Dalvik은 여러 차례 실행하더라도 누적적인 성능 향상을 제공하지 못합니다.
또한 Dalvik은 제한적인 트레이스 기반(trace-based) JIT 컴파일러를 사용했습니다. 전체 메서드가 아닌 개별 실행 트레이스(핫 코드 경로)만 컴파일하는 방식이었습니다. 메서드 기반 JIT에 비해 메모리 사용량은 적지만, 컴파일러가 메서드 전체 구조를 분석하여 최적화 결정을 내릴 수 없으므로 생성되는 머신 코드의 품질이 상대적으로 떨어졌습니다.
ART의 AOT(Ahead-of-Time) 컴파일
안드로이드 런타임(ART)은 Android 5.0부터 Dalvik을 대체하여 기본 런타임으로 채택되었습니다. 가장 핵심적인 변화는 dex2oat 도구를 활용한 AOT(Ahead-of-Time) 컴파일의 도입입니다. 앱 설치 시 ART는 전체 애플리케이션 바이트코드를 네이티브 머신 코드로 컴파일하여 .oat 파일에 저장합니다. 덕분에 첫 번째 실행에서부터 런타임 인터프리팅과 JIT 컴파일이 필요 없습니다.
// dex2oat가 설치 시점에 실행된 이후에는,
// 이 함수가 디스크에 네이티브 머신 코드로 존재합니다
fun computeSum(items: List<Int>): Int {
var sum = 0
for (item in items) {
sum += item
}
return sum
}