https://developer.android.com/guide/components/processes-and-threads
안드로이드 개발을 하다 보면 자연스럽게 이런 질문을 만나게 된다.
이 글에서는 운영체제 관점과 안드로이드 내부 구조를 함께 엮어, 안드로이드의 실행 모델을 처음부터 끝까지 정리해본다.
운영체제에서 프로세스는 실행 중인 프로그램의 인스턴스다.
각 프로세스는 독립된 메모리 공간을 가지며, 다른 프로세스와 메모리를 직접 공유하지 않는다.
안드로이드에서도 동일하다.
즉, 일반적인 안드로이드 앱은 “앱 하나 = 프로세스 하나” 구조를 가진다.
필요하다면 android:process 속성을 통해 컴포넌트를 분리할 수 있지만, 대부분의 앱에서는 사용하지 않는다.
안드로이드는 메모리가 부족해지면 프로세스를 종료한다.
이때 기준은 “사용자에게 얼마나 중요한가”다.
그래서 안드로이드 앱은 항상 프로세스가 죽고 다시 살아나는 상황을 전제로 설계해야 한다.
프로세스 안에는 하나 이상의 스레드가 존재한다.
안드로이드 앱이 시작되면 가장 먼저 생성되는 스레드가 있다.
안드로이드는 앱 시작 시 메인 스레드(Main Thread)를 하나 생성한다.
이 스레드는 흔히 UI 스레드라고도 불린다.
UI 스레드의 역할은 다음과 같다.
즉, UI 스레드는 앱의 화면과 상호작용을 전담하는 핵심 스레드다.
UI 스레드는 이벤트를 하나씩 처리한다.
이 과정 중 UI 스레드에서 오래 걸리는 작업을 수행하면 어떻게 될까?
5초 이상 UI 스레드가 응답하지 않으면
안드로이드는 ANR(Application Not Responding)을 발생시킨다.
그래서 안드로이드에는 명확한 규칙이 있다.
여기서 자연스럽게 질문이 생긴다.
UI 스레드는 어떻게 앱이 종료될 때까지 계속 살아 있을까?
그 답이 이벤트 루프(Event Loop)다.
UI 스레드는 내부적으로 다음과 같은 구조를 가진다.
메시지가 올 때까지 대기
→ 메시지 하나 꺼내서 실행
→ 다시 대기
이 무한 루프를 담당하는 객체가 Looper다.
Looper 옆에는 항상 MessageQueue가 있다.
그렇다면 누가 큐에 작업을 넣을까?
그 역할을 하는 것이 Handler다.
Handler는 특정 Looper(즉, 특정 스레드)에 연결되어
그 스레드의 MessageQueue에 작업을 전달한다.
Handler(Looper.getMainLooper()).post {
textView.text = "hello"
}
이 코드는 “UI 스레드에서 이 코드를 실행해달라”는 요청이다.
네트워크, DB, 파일 IO 같은 작업은
반드시 UI 스레드가 아닌 백그라운드 스레드에서 수행해야 한다.
하지만 UI 업데이트는 UI 스레드에서만 가능하다.
그래서 일반적인 흐름은 다음과 같다.
이 구조 덕분에 UI 스레드는 안전하게 유지된다.
일반 Thread는 작업이 끝나면 종료된다.
하지만 계속 살아 있으면서 요청을 처리해야 하는 스레드도 필요하다.
이를 위해 제공되는 것이 HandlerThread다.
다만 코드 복잡도가 높고 생명주기 관리가 어렵다.
Coroutine은 Thread를 직접 다루지 않는다.
이미 존재하는 스레드 위에서 실행 흐름만 관리한다.
핵심은 Dispatcher다.
withContext(Dispatchers.Main) {
textView.text = "hello"
}
은 내부적으로 다음과 같다.
Handler(Looper.getMainLooper()).post {
textView.text = "hello"
}
이처럼, Coroutine은 Handler + Looper 구조 위에서 동작하지만, 개발자는 이를 직접 신경 쓰지 않아도 된다.
Dispatchers.Main은 내부적으로
에 작업을 등록한다.
즉, Handler를 직접 쓰는 것과 본질적으로 동일하지만
더 안전하고 가독성이 좋으며 취소와 생명주기 관리가 가능하다.
안드로이드의 실행 구조를 한 줄로 요약하면 다음과 같다.
이 흐름을 이해하면
왜 안드로이드가 이런 구조를 가지는지,
왜 Coroutine이 표준이 되었는지도 자연스럽게 이해할 수 있다.