본문 바로가기
안드로이드

안드로이드 컴파일 과정 완전정리 ⑥ - ART란?

by kkomaeng 2026. 2. 24.

이전 글

2026.02.19 - [안드로이드] - 안드로이드 컴파일 과정 완전정리 ⑤ 부록 - DEX 는 알겠고 APK 는요?


안드로이드 컴파일 과정 완전정리 ⑥ - ART란?

이전 글에서 DEX는 Android용 바이트코드 포맷이라고 정리했다.
이제 질문은 하나다.

그 DEX를 실제로 누가 실행시키는가?

정답은 **ART(Android Runtime)**다.

하지만 “ART가 실행한다”는 말은 정확하지 않다.
실제로 실행하는 것은 항상 CPU다.
ART는 그 CPU 실행을 가능하게 만드는 런타임 + 컴파일 시스템이다.


0) 한 문장 정의 (정확한 버전)

ART는 CPU 위에서 동작하는 소프트웨어이며,
DEX 바이트코드를 인터프리터 또는 JIT/AOT 컴파일을 통해
CPU가 실행 가능한 기계어로 연결해주는 실행 환경이다.

이 정의를 CPU 관점으로 다시 풀면 이렇게 된다.

  1. CPU는 ARM64 같은 **기계어(machine code)**만 실행한다.
  2. DEX는 CPU가 이해하지 못하는 **바이트코드(bytecode)**다.
  3. ART가 중간에서
    • 해석(interpreter)하거나
    • 기계어로 변환(JIT/AOT)해서
  4. CPU 실행 경로로 올려준다.

즉 ART는 “실행 주체”가 아니라
**DEX → 네이티브 코드로 이어지는 브리지(bridge)**다.


▶ 더보기 — “바이트코드”와 “기계어” 차이

  • 기계어: CPU가 직접 이해하는 명령어. ARM64 CPU면 ARM64 기계어만 실행한다.
  • 바이트코드: 가상머신(ART/JVM 같은 런타임)이 이해하는 명령어. CPU는 직접 실행 못 한다.

1) ART의 구성: Runtime + Compiler

ART는 크게 **Runtime(실행 시스템)**과 Compiler(코드 생성 시스템) 두 덩어리로 이해하면 된다. “런타임인데 왜 컴파일러가 있냐?”가 첫 혼란 포인트인데, Android는 기기 안에서(온디바이스) 컴파일이 실제로 일어나는 구조라서 둘이 같이 묶여 있다.

1-1) Runtime (libart.so 등)

앱 프로세스가 시작되면 런타임 라이브러리(대표적으로 libart.so)가 로드되고, 그 코드(기계어)가 CPU에서 실행된다. Runtime이 맡는 건 “앱이 돌기 위해 필요한 운영체제급 기능”에 가깝다.

Runtime이 담당하는 것(핵심만):

  • DEX 로딩: APK 안의 classes.dex 또는 분할 DEX들을 열어 “실행 가능한 형태로 읽는” 작업
  • 클래스 로딩/링킹(linking): com.example.Foo 같은 클래스 정의를 찾아 메모리상의 클래스 구조로 만들고, 심볼(이름) 참조를 실제 대상(메서드/필드)로 연결
  • 인터프리터 실행: DEX opcode를 하나씩 읽고 수행(아래 2장에서 더 깊게)
  • JIT 관리: 언제 어떤 메서드를 컴파일할지 결정하고, 컴파일 결과를 실행 경로에 연결
  • GC(가비지 컬렉션): 힙 메모리에서 더 이상 쓰지 않는 객체를 회수
  • 스레드/동기화: Java/Kotlin 스레드 모델을 OS 스레드와 연결하고 모니터 락 같은 동기화 제공
  • 예외 처리: try/catch, 스택 언와인딩(unwind) 같은 제어 흐름 제공

▶ 더보기 — “링킹(linking)”이란

코드가 SomeClass.someMethod()를 호출할 때, 바이트코드에는 보통 “이름/시그니처” 형태로 참조가 들어 있다.
링킹은 이 참조를 “실제 메모리상의 메서드 구현”으로 연결(resolve)하는 과정이다.
이게 끝나야 런타임이 호출을 빠르게 처리할 수 있다.


1-2) Compiler: dex2oat / JIT 컴파일러

Compiler는 “DEX를 네이티브 코드로 만들 수 있는 장치”다. 두 종류로 나뉜다.

  • AOT 컴파일러(dex2oat): 실행 전에 미리 컴파일
  • JIT 컴파일러: 실행 중에 컴파일

여기서부터가 “ART가 단순 인터프리터가 아니다”의 핵심이다.


▶ 더보기 — AOT / JIT 가장 구체적인 정의

  • AOT(Ahead-Of-Time): 실행 전에 DEX(또는 그 일부)를 네이티브 기계어로 컴파일해서 저장해 두는 방식.
    → 장점: 앱 실행 시 바로 네이티브로 갈 수 있어 초기 성능이 안정적일 수 있음
    → 단점: 설치/백그라운드 작업 비용, 저장공간 증가 가능
  • JIT(Just-In-Time): 실행 중에 “자주 실행되는 코드(핫 코드)”만 골라 네이티브로 컴파일하는 방식.
    → 장점: 실제 사용 패턴에 맞게 필요한 부분만 최적화, 저장공간 절약 가능
    → 단점: 처음 실행 때는 인터프리터 경로 비중이 있어 워밍업(warm-up) 시간이 생길 수 있음

2) 왜 ART가 필요한가? (DEX는 CPU가 모른다)

DEX는 바이트코드다.
CPU는 ARM64 명령어 집합만 이해한다.

예:

DEX:

add-int v0, v1, v2

ARM64(예시):

add w0, w1, w2

CPU는 두 번째만 실행 가능하다.

그래서 ART는 두 전략을 가진다.

  1. 인터프리터 실행: “DEX를 그대로 두고” 해석해서 실행
  2. 네이티브 컴파일 실행: “DEX를 기계어로 바꿔서” CPU가 직접 실행

▶ 더보기 — 인터프리터가 “CPU 명령으로 수행한다”는 말의 정확한 의미

인터프리터 자체가 이미 기계어로 된 프로그램이다.
CPU는 인터프리터 프로그램의 기계어를 실행하고, 그 인터프리터가 DEX opcode를 읽어 덧셈/분기/호출을 수행한다.
즉 CPU는 끝까지 기계어만 실행한다.


3) 설치 시점 최적화: AOT (dex2oat)

dex2oat는 “기기 안에서 돌아가는 AOT 컴파일러”다.
빌드 머신에서 이미 .dex가 만들어졌는데도 굳이 기기에서 다시 컴파일하는 이유는, 최종 타깃이 ‘해당 기기 CPU/런타임 조건’이기 때문이다.

AOSP 문서 흐름을 개념적으로 정리하면:

  • 입력: DEX
  • 처리: dex2oat가 기기 ISA(예: ARM64)에 맞게 컴파일
  • 출력: 기기용 실행 산출물(여러 파일일 수 있음)

이 과정은:

  • 앱 설치 시점
  • 또는 백그라운드 dexopt 작업 중

에 일어날 수 있다.


3-1) 내부적으로 무슨 일이 벌어지나? (컴파일러 관점으로 쪼개기)

AOT 컴파일러의 내부는 전형적인 “컴파일 파이프라인”을 따른다. 구현 디테일은 버전에 따라 달라질 수 있지만, 개념적으로는 아래 단계를 밟는다.

  1. DEX 파싱(parsing)
    • DEX의 메서드 코드, 상수 풀(constant pool), 타입/필드/메서드 테이블을 읽는다.
    • “이 메서드의 바이트코드가 어디에 저장돼 있는지”를 찾아낸다.
  2. 검증(verification)
    • 바이트코드가 규칙을 어겼는지 체크한다(예: 타입 규칙, 스택/레지스터 사용 규칙 등).
    • 이 단계가 엄격하면, 잘못된 DEX를 런타임에서 실행하기 전에 잡아낼 수 있다.
  3. IR(중간 표현) 구성
    • 바이트코드를 컴파일하기 쉬운 내부 표현으로 바꾼다.
    • 여기서부터 “최적화”가 가능해진다(예: 상수 전파, 불필요 연산 제거).
  4. 최적화(optimization)
    • 간단한 예: x = 1 + 2는 미리 x = 3으로 바꿀 수 있다.
    • 조건 분기 단순화, 중복 계산 제거 같은 변환이 여기서 들어갈 수 있다.
  5. 코드 생성(codegen)
    • IR을 ARM64 같은 기계어로 내린다.
    • 레지스터 할당(register allocation), 호출 규약(calling convention) 적용 같은 작업이 여기 포함된다.
  6. 산출물 저장(artifact emission)
    • 생성된 코드 및 런타임에 필요한 메타데이터를 파일로 저장한다(OAT/VDEX 등은 다음 장).

▶ 더보기 — IR(중간 표현)이 왜 필요하나

바이트코드는 “실행”에는 적합하지만 “최적화”에는 불편한 형태가 많다.
컴파일러는 보통 IR로 바꿔야 분석/최적화가 쉬워진다.


3-2) “항상 즉시 컴파일되나?”

아니다.

정책은 기기/버전/설정에 따라 달라질 수 있다. 그래서 글에서는:

설치 시점 또는 백그라운드에서 컴파일될 수 있다

라고 쓰는 게 안전하다.


▶ 더보기 — dexopt가 뭔가

“DEX 최적화 작업”을 통칭하는 말로 자주 쓰인다.
실제로는 dex2oat AOT 컴파일, 프로파일 기반 컴파일, 기타 준비 작업이 포함될 수 있다(정책/구현에 따라).


4) 산출물: OAT / VDEX는 무엇인가

독자가 가장 많이 헷갈리는 포인트가 여기다.

AOSP는 이렇게 명시한다.

dex2oat는 하나 이상의 compilation artifact를 만든다.
파일 개수/확장자/이름은 릴리즈마다 바뀔 수 있다.

즉 “확장자 이름” 자체는 본질이 아니다.
본질은 왜 이런 파일이 필요한가다.


4-1) 가장 안전한 역할 정의

  • OAT 계열(개념적으로): “컴파일된 코드(네이티브 코드) + 실행에 필요한 메타데이터”를 담는 컨테이너로 쓰인다.
  • VDEX 계열(개념적으로): “DEX 자체/검증 정보/런타임 보조 정보” 같은 부가 데이터를 담는 쪽으로 쓰일 수 있다.

여기서 “정확히 어떤 바이트 레이아웃이냐”는 버전별로 달라질 수 있으니, 블로그에서는 역할 중심이 안전하다.


4-2) 코드가 보여주는 것

dex2oat 코드에서는:

  • verifier dependencies 기록
  • quickening info 기록

이 동작이 존재한다.

이 말은:

단순 네이티브 코드만 생성하는 게 아니라
런타임 최적화를 위한 부가 정보를 저장한다는 뜻이다.


▶ 더보기 — verifier dependencies(검증 의존성)

“검증 결과가 특정 클래스/메서드 상태에 의존한다” 같은 정보를 담을 수 있다.
목적은 보통 “나중에 재검증/재컴파일할 때 불필요한 일을 줄이거나, 안전성을 유지”하는 쪽으로 이해하면 된다.
(세부 포맷은 버전별로 달라질 수 있으니 역할 중심으로 설명하는 게 안전하다.)

▶ 더보기 — quickening

바이트코드 실행을 더 빠르게 만들기 위해
런타임에서 사용할 수 있도록 준비된 사전 처리 정보.
(정확히 어떤 변환을 하느냐는 버전/구현에 따라 달라질 수 있음)


5) JIT + 프로파일: 실행 중 최적화

ART는 AOT만 사용하지 않는다.

여기서 다시 CPU 관점으로 정리한다.

  • 인터프리터는 “DEX opcode를 읽고 분기”를 매번 반복한다.
  • 이 반복의 핵심 오버헤드가 dispatch 비용이다.
  • 자주 실행되는 경로(핫 코드)는 이 비용이 누적되므로, 네이티브로 바꾸면 큰 이득이 나온다.
  • 그래서 JIT가 필요하다.

5-1) 프로파일링이 왜 필요한가?

예시:

fun renderFrame() {
    physicsStep()
    draw()
}

renderFrame은 초당 수십 번 호출된다.
settingsScreen은 거의 호출되지 않는다.

모든 코드를 똑같이 컴파일하면:

  • 저장공간이 불필요하게 늘거나
  • 설치/업데이트 비용이 커지거나
  • 실제로 안 쓰는 코드까지 최적화하느라 낭비가 생긴다

그래서 **프로파일링(실행 통계 수집)**이 필요하다.


▶ 더보기 — 프로파일링이 수집하는 데이터 예시(개념)

  • 메서드 호출 빈도
  • 루프 반복 빈도
  • 어떤 분기(if/else)가 실제로 자주 타는지
  • 어떤 호출 대상이 자주 오는지(다형성 호출 패턴)

(정확히 무엇을 어느 형식으로 저장하는지는 구현/버전에 따라 달라질 수 있으므로 “대표 예시”로만 이해하면 된다.)


5-2) 내부적으로 무슨 일이 벌어지나? (JIT 파이프라인)

JIT는 보통 이런 흐름을 가진다.

  1. 인터프리터로 시작
    • 앱 첫 실행이나 아직 컴파일되지 않은 경로는 인터프리터가 담당한다.
  2. 카운터/프로파일 누적
    • 메서드 진입 카운터, 루프 백엣지(back-edge) 카운터 같은 형태로 “핫함”을 측정하는 방식이 일반적이다(개념).
  3. 임계값(threshold) 도달
    • “이 메서드/루프는 충분히 자주 돈다” 판단이 서면 JIT 컴파일 트리거.
  4. JIT 컴파일
    • 바이트코드 → IR → 최적화 → 코드 생성
    • AOT와 유사한 파이프라인을 “런타임 중”에 수행한다.
  5. 이후 네이티브 코드 경로 사용
    • 이후 호출은 컴파일된 코드로 갈 수 있다.
    • 흔히 “엔트리포인트가 바뀐다”라고 말하지만, 블로그에서는 이렇게 표현하는 게 안전하다:
    • 동일 메서드 호출이 이후에는 인터프리터 경로가 아니라 컴파일된 코드 경로로 들어갈 수 있다.

▶ 더보기 — dispatch 비용(다시, 더 정확히)

인터프리터가 매 opcode마다 “다음 opcode 읽기 → 분기 → 구현 수행”을 반복할 때 드는 오버헤드.
네이티브 코드로 가면 이 “분기 루프”를 제거할 수 있다.


6) AOT와 JIT의 관계

AOT는 “미리 준비하는 전략”
JIT는 “실제 사용 패턴에 맞춰 조정하는 전략”

둘은 경쟁 관계가 아니라 보완 관계다.

  • AOT: 기본 성능/일관성 확보에 유리할 수 있음
  • JIT: 실제로 많이 쓰는 경로를 더 날카롭게 최적화 가능

여기서 중요한 결론:

ART는 단일 실행 방식이 아니라, 상황에 따라 경로를 바꾼다.


▶ 더보기 — “워밍업(warm-up)”이란

JIT 기반 시스템에서 “처음엔 느리다가, 반복 실행 후 빨라지는” 구간이 생길 수 있다.
그 이유는 핫 코드가 아직 컴파일되지 않았고, 프로파일이 충분히 쌓이지 않았기 때문이다.


7) 내부 구조를 CPU 관점으로 다시 정리

DEX
  ↓
(인터프리터 또는 JIT/AOT)
  ↓
네이티브 기계어
  ↓
CPU

여기서 핵심은:

ART는 CPU 위에서 동작하는 소프트웨어다.
CPU는 끝까지 기계어만 실행한다.

이 문장을 오해 없이 더 직설적으로 풀면:

  • CPU는 ART(기계어 프로그램)를 실행한다.
  • ART는 DEX를 처리해 기계어를 만들거나, 이미 만든 기계어로 연결한다.
  • CPU는 그 기계어를 실행한다.

▶ 더보기 — “엔트리포인트”란

메서드를 호출할 때 “실제로 점프하는 시작 주소”를 말한다(개념).
처음엔 인터프리터 진입점일 수 있고, JIT/AOT 후엔 컴파일된 코드 진입점일 수 있다.
(정확한 내부 구조체/필드명은 버전에 따라 달라질 수 있으니 개념으로 이해하면 된다.)


8) 개발자가 ART를 이해해야 하는 이유

  1. **“왜 첫 실행이 느릴 수 있나”**를 설명 가능
    • 인터프리터 비중이 높은 워밍업 구간이 있을 수 있다.
  2. **“왜 동일 앱인데 기기마다 체감이 다를 수 있나”**를 설명 가능
    • AOT/JIT 정책, 백그라운드 최적화 타이밍, 저장공간/열/전력 정책 등이 다를 수 있다(기기/OEM 차).
  3. **“DEX는 완성본이 아니다”**라는 감각 형성
    • .dex는 “최종 실행물”이 아니라 “ART의 입력”이다.

최종 요약

ART는 단순 인터프리터가 아니다.
단순 컴파일러도 아니다.

DEX를 CPU 실행 경로로 연결하는 하이브리드 런타임/컴파일 시스템이다.

그리고 다시 한 번 정의를 반복한다.

ART는 CPU 위에서 동작하는 소프트웨어이며,
DEX 바이트코드를 인터프리터 또는 JIT/AOT 컴파일을 통해
CPU가 실행 가능한 기계어로 연결해주는 실행 환경이다.

 

 

댓글