본문 바로가기
안드로이드

안드로이드 컴파일 과정 완전정리 ⑥ 부록 - 앱 프로세스는 어떻게 시작되나?

by kkomaeng 2026. 2. 25.

이전 글

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

 

안드로이드 컴파일 과정 완전정리 ⑥ 부록 - 앱 프로세스는 어떻게 시작되나?

이전 글(ART)에서 DEX는 단순한 바이트코드 파일이 아니라, 설치 시점 또는 백그라운드에서 dex2oat에 의해 AOT 컴파일될 수 있고, 실행 중에는 JIT와 프로파일링에 의해 추가 최적화가 이루어질 수 있다는 점까지 정리했다.

그렇다면 다음 질문이 자연스럽게 따라온다.

ART가 준비한 DEX/AOT 산출물은 언제, 어떤 맥락에서 실제 실행 경로에 올라오는가?

이 부록은 그 연결 고리를 설명한다.

  • 앱이 설치된 뒤,
  • 사용자가 실행을 요청하면,
  • 시스템이 어떤 과정을 거쳐 새 리눅스 프로세스를 만들고,
  • 그 프로세스 안에서 ART 런타임이 올라오며,
  • ClassLoader가 APK 내부의 DEX를 열고,
  • 최종적으로 ART의 실행 엔진(인터프리터/JIT/AOT 경로)이 동작할 준비를 마치는지

를 **“컴파일 산출물이 실제 실행 환경에 연결되는 관점”**에서 따라간다.

이 글의 목적은 프레임워크 구조 자체를 설명하는 것이 아니다.
핵심은 다음이다.

컴파일 단계에서 만들어진 결과물(DEX, oat/vdex 등)이
실제 런타임 프로세스 안에서 어떤 흐름으로 사용되기 시작하는가.

즉, 이 부록은 “프로세스 생성 설명”이 아니라
“컴파일 결과와 실행 환경이 맞물리는 지점”을 정리하는 연결 챕터다.


1) 설치 시 시스템이 하는 일: “서명 검증 + 패키지 등록”이 출발점

APK는 그냥 zip 파일이 아니다. Android는 “서명된 패키지”만 설치 대상으로 인정한다.

  • Android 공식 문서에 따르면, “Android에서 실행되는 모든 앱은 개발자가 서명해야 하고, 서명되지 않은 앱 설치는 거절된다”고 돼 있다. (Android Open Source Project)
  • 같은 문서에서 “APK가 설치될 때 Package Manager가 APK가 올바르게 서명되었는지 검증한다”고 명시한다. (Android Open Source Project)

여기까지가 “프로세스 시작”의 전제다. **설치가 끝났다는 건 ‘패키지 매니저가 이 APK를 신뢰 가능한 앱으로 등록해둔 상태’**라는 뜻이다.

더보기 — PackageManager가 하는 “검증”은 뭘 뜻하나

  • 서명(정확히는 APK에 포함된 인증서/공개키)을 확인해서 “누가 만든 앱인지” 식별하고, 업데이트 시 같은 키로 서명된 APK만 같은 앱으로 교체할 수 있게 만드는 장치다. (Android Open Source Project)
  • 이 검증이 실패하면 설치 단계에서 막히므로, “앱 실행” 단계까지 갈 수가 없다. (Android Open Source Project)

2) 실행 버튼을 누르면: “런처 → system_server”로 요청이 들어간다

홈 화면의 런처(Launcher)는 앱 아이콘을 누르면 “Activity 시작”을 요청한다. 이 요청을 받아서 실제로 프로세스를 만들지 말지 결정하는 쪽이 system_server다.

여기서 system_server는 “안드로이드 핵심 서버 프로세스”이고, ActivityManager 같은 서비스들이 이 프로세스 안에서 돈다. (이 글은 “프레임워크 내부 콜 체인”이 주제라, IPC(Binder) 디테일은 다음 글로 넘기고 흐름만 고정한다.)


3) system_server가 하는 핵심 판단: “프로세스가 이미 있나?”

system_server는 앱 프로세스가 이미 떠 있으면 그 프로세스에 “Activity를 띄워라” 메시지를 보낸다.
없으면 새 프로세스를 만들어야 한다. 여기서 Android가 쓰는 방식이 Zygote다.


4) Zygote: 앱 프로세스의 “루트(root) 프로세스”

AOSP 문서에서 Zygote를 이렇게 정의한다.

  • “Zygote는 Android OS에서 모든 시스템/앱 프로세스의 root 역할을 하는 프로세스” (Android Open Source Project)
  • system_server가 “프로세스가 필요하다”고 판단하면 유닉스 도메인 소켓으로 Zygote에 명령을 보내고, Zygote가 fork로 프로세스를 만든 뒤 PID를 돌려주는 흐름이 문서에 설명돼 있다. (Android Open Source Project)
  • 최근 기기에서는 USAP(unspecialized app process) pool 같은 최적화 경로도 있을 수 있고(미리 대기 중인 프로세스를 “앱용으로 특화”해서 빠르게 쓰는 방식), 이것도 같은 문서에 정리돼 있다. (Android Open Source Project)

더보기 — fork가 왜 중요한가

  • fork는 “부모 프로세스를 복제해서 자식 프로세스를 만드는” 리눅스 메커니즘이다.
  • Zygote는 공용 클래스/리소스 등을 미리 로드해 둔 상태에서 fork를 하기 때문에, 프로세스 생성 비용을 줄이고 메모리 공유(COW 같은 메커니즘 활용)를 기대할 수 있다.
    (이 글에서는 ‘fork 기반 생성’까지만 확정하고, 메모리 공유의 구체 메커니즘은 너무 커지므로 뒤로 뺀다.)

5) “system_server → Zygote” 통신은 실제로 소켓 문자열을 주고받는다

이건 “그럴 것이다”가 아니라 코드에 그대로 박혀 있다.

5-1) ZygoteProcess: 소켓에 args를 쓰고(pid를) 읽는다

android.os.ZygoteProcess는 내부에서

  • BufferedWriter로 메시지를 쓰고 flush 한다
  • DataInputStream으로 pid(int)usingWrapper(boolean) 을 읽는다
  • pid < 0이면 fork 실패로 예외를 던진다

이 동작이 코드로 명확하다. (android.googlesource.com)

5-2) Process.start: “Zygote로 VM 프로세스를 시작한다”가 API 의미다

android.os.Process.start(...)는 내부에서 startViaZygote(...)를 호출한다. 즉 “프로세스 시작 = Zygote 경유”가 기본 경로로 코드에 고정돼 있다. (android.googlesource.com)

그리고 같은 파일에 “현재 wire format은 (a) 인자 개수, (b) newline으로 구분된 인자 문자열들”이고, Zygote가 읽은 뒤 “pid와 usingWrapper를 쓴다”는 주석이 박혀 있다. (android.googlesource.com)

더보기 — “인자(args)”에 뭐가 들어가나

Process.startViaZygote 쪽 코드를 보면 --setuid=, --setgid= 같은 런타임 인자들을 argsForZygote에 쌓는 형태다. 즉 “앱 프로세스의 UID/GID/런타임 플래그/ABI 등”을 Zygote에 전달해서, fork 후 자식 프로세스가 앱 프로세스 조건으로 세팅되게 한다. (android.googlesource.com)


6) 새 앱 프로세스가 뜨면, 그 안에서 “ActivityThread.main()”이 시작점이 된다

여기서부터는 “새로 만들어진 앱 프로세스 내부” 이야기다.
앱 프로세스가 실행을 시작하면, 자바 레벨에서 결국 ActivityThread 쪽이 메인 루프를 만든다.

6-1) 메인(UI) 스레드의 Looper는 ActivityThread.main에서 준비된다

ActivityThread 소스를 보면 main에서:

  • Looper.prepareMainLooper() 호출
  • ActivityThread thread = new ActivityThread();
  • thread.attach(false);
  • 마지막에 Looper.loop();

이 순서가 그대로 들어 있다. (android.googlesource.com)

즉 “메인 스레드가 메시지 루프를 갖는다”는 말은 실제로는:

  • 메인 스레드에서 Looper를 만들고
  • attach로 system_server(정확히는 ActivityManager) 쪽과 연결 절차를 타고
  • loop로 메시지 처리 루프에 들어간다

라는 뜻이다. (android.googlesource.com)

6-2) attach(false) 안에서 system_server에 “attachApplication”을 호출한다

ActivityThread.attach(false) 내부를 보면:

  • IActivityManager mgr = ActivityManagerNative.getDefault();
  • mgr.attachApplication(mAppThread);

이 호출이 들어 있다. 즉 “앱 프로세스가 떴다”는 사실을 시스템 서비스 쪽에 알려서 이후 바인딩/런치 메시지를 받게 되는 구조다. (android.googlesource.com)

더보기 — ActivityThread가 왜 “앱의 메인”인가

  • 앱 개발자가 작성하는 MainActivity.onCreate() 같은 건 ActivityThread의 메시지 루프가 “LAUNCH_ACTIVITY 같은 메시지”를 처리하면서 이어지는 단계다.
  • 그래서 “앱 프로세스 시작”을 프레임워크 관점에서 추적하면 ActivityThread로 귀결된다. (이 글은 프로세스/루프/클래스로딩까지만 다룸)

7) 클래스 로딩: PathClassLoader/BaseDexClassLoader가 “APK의 DEX를 연다”

프로세스가 떴다고 끝이 아니다. 이제 클래스(예: com.example.MainActivity) 를 로드해야 한다. 이게 “APK의 classes.dex가 실제 런타임에서 사용되는 순간”이다.

안드로이드 앱 클래스 로딩의 기본 축은:

  • PathClassLoader (앱용 ClassLoader)
  • 그 기반 구현인 BaseDexClassLoader
  • 그리고 실제 dex path를 관리하는 DexPathList

로 이어진다.

7-1) BaseDexClassLoader는 DexPathList를 만들고, findClass에서 pathList.findClass를 호출한다

BaseDexClassLoader 코드를 보면:

  • 생성자에서 this.pathList = new DexPathList(this, dexPath, librarySearchPath, ...)를 만든다 (android.googlesource.com)
  • findClass()에서 pathList.findClass(name, suppressedExceptions)를 호출해서 클래스를 찾는다 (android.googlesource.com)
  • 못 찾으면 "Didn't find class ... on path: " + pathList 형태의 ClassNotFoundException을 만든다 (android.googlesource.com)

즉 “클래스 로딩”은 추상적인 말이 아니라:

  1. class loader가 자신이 가진 dex path 리스트에서
  2. 해당 클래스 이름을 찾아보고
  3. 없으면 CNFE를 던지는

구조로 고정돼 있다. (android.googlesource.com)

7-2) DexPathList가 말하는 “class path entry”는 APK/ZIP/JAR/DEX다

DexPathList 주석에 class path entry가 뭐가 될 수 있는지가 명확히 써 있다.

  • .jar 또는 .zip (top-level classes.dex가 있을 수도 있고 리소스도 포함 가능)
  • 또는 plain .dex 파일 (android.googlesource.com)

즉 앱 설치 후 런타임에서 “APK(사실 zip) 내부의 classes.dex”를 열어 클래스 정의를 찾는 모델이 코드/주석으로 드러난다. (android.googlesource.com)

7-3) “ART가 class loader hierarchy finalize를 기대한다”는 말이 실제로 있다

BaseDexClassLoader 생성자 주석에:

  • “ART는 dex 파일을 로드하기 전에 class loader hierarchy가 finalized 되어 있길 기대한다”는 설명이 들어 있다. (android.googlesource.com)

이게 의미하는 바는 단순하다.
“클래스 로딩은 런타임의 핵심 경로고, class loader 체인은 성능/정확성에 영향을 주는 전제 조건”이라는 얘기다. (구체 최적화는 8편에서 실행 엔진과 붙여서 다루는 게 자연스럽다.)


8) 전체 흐름을 “한 장짜리”로 다시 조립

(A) 설치 단계

  1. APK 설치 시 Package Manager가 서명 검증을 수행한다. (Android Open Source Project)
  2. 설치가 성공하면 “실행 가능한 패키지”로 시스템에 등록된 상태가 된다. (등록 디테일은 기기/버전에 따라 내부 구조가 커서 이 글에서는 사실로 단정하지 않고 “설치 완료의 의미”로만 둔다.)

(B) 실행 요청 단계

  1. 런처가 Activity 시작을 요청한다 → system_server가 처리한다.

(C) 프로세스 생성 단계

  1. system_server는 프로세스가 없으면 Zygote에 요청한다. Zygote는 앱/시스템 프로세스의 root이고, 소켓 기반으로 fork를 수행한다. (Android Open Source Project)
  2. 실제 구현은 소켓으로 args를 보내고 pid를 읽는 형태로 코드에 있다. (android.googlesource.com)

(D) 앱 프로세스 내부 초기화

  1. 앱 프로세스 안에서 ActivityThread.main()이 prepareMainLooper → attach(false) → Looper.loop() 순으로 메인 루프를 만든다. (android.googlesource.com)
  2. attach 과정에서 system_server(ActivityManager 쪽)에 attachApplication을 호출해 “앱 프로세스가 준비됐다”를 연결한다. (android.googlesource.com)

(E) 클래스 로딩

  1. 앱 ClassLoader는 BaseDexClassLoader/DexPathList 체인을 통해 dex path에서 클래스를 찾는다. (android.googlesource.com)

용어 더보기 묶음

더보기 — system_server

  • 안드로이드에서 핵심 시스템 서비스들이 올라가는 프로세스. ActivityManager 같은 시스템 서비스가 여기서 동작한다.

더보기 — Zygote / USAP

  • Zygote: 앱/시스템 프로세스의 root 역할을 하는 프로세스. system_server가 소켓으로 요청하면 fork로 프로세스를 만든다. (Android Open Source Project)
  • USAP: unspecialized app process pool. 미리 대기 중인 프로세스를 앱용으로 특화해 지연을 줄이는 최적화 경로로 문서에 설명돼 있다. (Android Open Source Project)

더보기 — Looper / Message Loop

  • Looper.prepareMainLooper()로 메인 루퍼를 만들고 Looper.loop()로 메시지 처리 루프를 시작한다. 이 호출이 ActivityThread.main에 실제로 있다. (android.googlesource.com)

더보기 — PathClassLoader / BaseDexClassLoader / DexPathList

  • BaseDexClassLoader는 DexPathList를 만들고 findClass()에서 pathList.findClass()로 클래스를 찾는다. (android.googlesource.com)
  • DexPathList 주석에서 class path entry는 jar/zip(선택적으로 classes.dex 포함) 또는 plain dex가 될 수 있다고 정의한다. (android.googlesource.com)

댓글