Android Process & Threads

Victor·2026년 1월 1일

Android Processes & Threads

Android 어플리케이션의 컴포넌트가 시작될 때 해당 어플리케이션에 실행 중인 다른 컴포넌트가 없다면, Android 시스템은 해당 어플리케이션을 위해 단일 스레드가 포함된 새로운 리눅스 프로세스를 실행시킨다.

기본값으로, 동일 어플리케이션 내부의 모든 컴포넌트는 동일한 프로세스와 동일한 스레드에서 실행되며, 해당 스레드를 main thread라고 한다.

만약 특정 어플 컴포넌트가 시작될 때 이미 해당 어플의 프로세스가 존재한다면, 이는 동일한 어플 내의 다른 컴포넌트가 이미 시작되었다는 뜻이며, 이로 인해 후속으로 시작된 컴포넌트는 이전에 이미 실행된 프로세스와 스레드에서 작업을 실행한다.

하지만 개발자는 동일 어플 내의 다른 컴포넌트를 별개의 프로세스에서 실행시킬 수 있으며, 어떠한 프로세스에서도 추가적인 스레드를 생성할 수 있다.

Android Processes

기본값으로, 어플리케이션의 모든 컴포넌트는 동일한 프로세스에서 실행된다. 대부분의 어플은 이를 유지한다.

그러나 개발자가 특정 컴포넌트가 어떠한 프로세스에 속하도록 설정해야 한다면, Android Manifest 파일에서 설정할 수 있다.

컴포넌트 요소의 각 타입인 Manifest 항목으로는 <activity>, <service>, <receiver>, <provider>가 있다.

(receiver = broadcast receiver, provider = content provider)

이 항목들에서 android:process 속성을 지원한다.

이 속성은 해당 컴포넌트가 실행될 프로세스를 명시한다. 크게 세 가지 방식으로 사용 가능하다.

  1. 해당 속성이 설정된 각 컴포넌트는 그것의 고유 프로세스에서 실행된다.
  2. 해당 속성이 설정된 특정 컴포넌트들이 프로세스를 공유하여 실행된다.
  3. 만약 다른 어플리케이션과 동일한 리눅스 유저 ID를 공유하고 동일한 인증서로 서명되어 있다면, 다른 어플리케이션의 컴포넌트가 실행되는 프로세스에서 실행된다.

Manifest 파일의 <application> 요소에서도 android:process 속성을 지원한다. 기본값을 모든 컴포넌트에 적용할 수 있다.

Android 시스템은 우선순위가 더 높은 프로세스의 실행을 위한 자원이 필요할 때 기존 프로세스를 종료시킬 수 있다. 종료된 프로세스에서 실행 중인 어플 컴포넌트들은 결과적으로 파괴된다. 해당 컴포넌트들이 실행되어야 할 일이 생기면 종료된 프로세스는 다시 시작된다.

Android 시스템은 각 프로세스의 상대적 중요도에 따라 어떤 프로세스를 종료할지 결정한다.
프로세스의 중요도는 컴포넌트의 상태에 의존한다. 컴포넌트의 상태가 디바이스 화면에서 보이는(visible) 상태라면 해당 컴포넌트의 프로세스는 높은 중요도를 갖고, 더 이상 보이지 않는 상태의 컴포넌트가 속한 프로세스는 좀 더 쉽게 종료될 수 있다.

Android Threads

어플이 실행되면, Android 시스템은 해당 어플 실행을 위한 쓰레드를 생성하며, 이를 main 쓰레드라고 한다. main 쓰레드는 이벤트를 적절한 유저 인터페이스 위젯에 전달 (dispatch) 하고, 이벤트를 반영 (drawing) 하는 역할을 한다.

거의 대부분의 경우 main 쓰레드에서 Android UI 툴킷의 컴포넌트와 상호작용을 한다. 이러한 이유 때문에 main 쓰레드는 UI 쓰레드 라고도 불린다.

(특수한 경우에 main 쓰레드가 UI 쓰레드가 아닌 경우가 있다.)

Android 시스템은 각 컴포넌트의 객체마다 분리된 쓰레드를 생성하지 않는다. 동일한 프로세스에서 실행되는 모든 컴포넌트는 UI 쓰레드에서 초기화되며, 각각의 컴포넌트의 시스템 호출 (system call) 은 UI 쓰레드로부터 전달된다. 결과적으로, 시스템 콜백 (system callbacks) 에 반응하는 메서드는 항상 동일한 프로세스의 UI 쓰레드에서 실행된다.

(시스템 콜백에 반응하는 메서드 = 시스템의 특정 시점 실행에 상위 레벨의 메서드 구현체를 실행)

예시

  1. 유저가 화면의 버튼을 터치 (touch)
  2. UI 쓰레드가 터치 이벤트를 해당 위젯에 전달 (dispatch)
  3. 눌린 상태를 설정하고 이벤트 큐에 invalidate request (다시 그리기 요청) 을 발송 (post)
  4. UI 쓰레드는 해당 요청을 큐에서 획득 (dequeue)
  5. 해당 위젯에게 다시 그려지도록 알림 (notify)

앱을 적절하게 구현할 지라도, 이러한 단일 스레드 모델은 유저 상호작용에 따른 무거운 작업을 수행할 때 성능 저하를 일으킬 수 있다. 네트워크 접근 또는 데이터베이스 쿼리 (요청) 과 같은 긴 작업을 UI 쓰레드에서 실행하면 전체적인 UI 가 차단 (block) 된다. UI 스레드가 차단되면 그리는 이벤트를 포함한 어떠한 이벤트도 전달되지 못한다.

유저 관점에서 UI 쓰레드가 차단되면 어플이 멈춘 것처럼 보인다. 만약 UI 쓰레드가 몇 초 이상 차단되면, ANR (Application Not Responding) 에러 문구가 나타난다. 이는 유저가 해당 어플을 종료하거나 삭제하게 만드는 원인이 될 수 있다.


Android UI 툴킷은 thread-safe 하지 않음을 숙지해야 한다. 그러므로 worker 쓰레드로 부터 UI 를 조작해서는 안된다. UI 쓰레드에서 유저 인터페이스를 조작해야 한다.

(thread-safe = 여러[멀티] thread 로부터 접근해도 안전)

Android 단일 스레드 모델의 두 가지 규칙은 다음과 같다.

  1. UI 쓰레드를 막지 않기
  2. UI 쓰레드 밖에서 Android UI 툴킷에 접근하지 않기

단일 쓰레드 모델의 이유로 UI 쓰레드를 막지 않는다는 어플 UI 의 대응은 필수적이다. 만약 작업의 수행이 즉각적이지 않다면, 해당 작업을 개별적인 background 또는 worker 쓰레드에서 실행시키도록 한다.

기억해야 할 점은, UI 또는 main 스레드가 아닌 다른 어떠한 스레드에서도 UI를 업데이트할 수 없다는 것이다.

이 규칙을 따르는 것을 돕기 위해, Android 는 다른 쓰레드에서 UI 쓰레드에 접근할 수 있는 몇 가지 방법을 제공한다.

  • Activity 의 runOnUiThread(Runnable)
  • View 의 post(Runnable)
  • View 의 postDelayed(Runnable, long)

그러나, 작업의 복잡성이 증가함에 따라, 위의 worker thead 사용만으로 유지하는데 복잡함과 여러움이 있다. 더 복잡한 worker 스레드와의 상호작용을 다루기 위해, UI 스레드로부터 전달된 메시지를 처리할 수 있도록 worker 스레드 내에서 Handler를 사용하는 것을 고려할 수 있다.

Thread-safe 메서드

어떤 상황에서는 개발자가 구현한 메서드가 여러 스레드로부터 호출될 수 있으며, 이 경우 해당 메서드는 반드시 스레드 안전(thread-safe)하게 작성되어야 한다.

이는 주로 바운드 서비스(bound service)의 메서드처럼 원격으로 호출될 수 있는 메서드들에 해당한다.

  1. IBinder에 구현된 메서드를 호출할 때, 호출 측이 IBinder가 실행 중인 프로세스와 동일한 프로세스에 있다면 해당 메서드는 호출한 측의 스레드에서 실행된다. (Local Binding)
  2. 하지만 호출 측이 IBinder의 구현된 메서드와 다른 프로세스에서 발생한다면, 해당 메서드는 (IBinder와 동일한) 별개의 프로세스 내에서 관리하는 스레드 풀(thread pool) 중 선택된 스레드에서 실행된다. (IPC)

예를 들어, 서비스의 onBind() 메서드는 해당 서비스 프로세스의 UI 스레드에서 호출되는 반면, onBind()가 반환하는 객체(RPC, 원격 프로시저 호출, 메서드를 구현하는 서브클래스)에 정의된 메서드들은 스레드 풀에서 호출된다. 서비스는 둘 이상의 클라이언트를 가질 수 있으므로, 스레드 풀 내의 여러 스레드가 동일한 IBinder 메서드에 동시에 참여(접근) 가능하다. 따라서 IBinder 메서드는 반드시 스레드 안전(Thread-safe) 하게 구현되어야 한다.

Service 실행 종류

Service 컴포넌트의 사용 방법은 두 가지로 나뉜다.

  1. 호출하여 실행 (start service)
  2. 다른 컴포넌트와 결합하여 동시에 실행 (bind service)

Bound Service 의 경우, 결합하는 다른 컴포넌트가 별개의 어플리케이션에 존재하는 경우 RPC 방식 (IPC) 를 사용

유사하게, Content Provider 는 다른 프로세스에서 발생하는 데이터 요청을 받을 수 있다 (호출 프로세스에서 데이터 작업을 위해 인자값을 넘겨주거나 결과를 반환받음). ContentResolver 와 ContentProvider 클래스는 프로세스 간 통신(IPC)이 관리되는 구체적인 방식을 숨겨주지만, 요청에 응답하는 ContentProvider의 메서드들(query(), insert(), delete(), update(), getType())은 프로세스의 UI 스레드가 아닌 스레드 풀(Thread Pool) 내의 스레드에서 호출된다. 이러한 메서드들은 동시에 여러 스레드로부터 호출될 수 있으므로, 반드시 Thread-safe 하게 구현되어야 한다.

상단 예시 정리

특정 기능의 메서드를 호출할 때, 호출하는 측과 호출되는 메서드가 동일한 프로세스 내에 있다면 해당 메서드는 호출한 측의 스레드에서 즉시 실행된다.

하지만 호출하는 측과 호출되는 메서드가 서로 다른 프로세스 사이에서 발생한다면, 호출되는 측의 시스템(해당 메서드를 갖는 별도의 프로세스) 은 자신이 관리하는 별도의 스레드 풀(thread pool)에서 가용 스레드를 하나 선택하여 해당 기능을 실행한다.

Interprocess Communication

Android 는 RPC 를 사용하여 IPC 매커니즘을 제공한다. RPC는 메서드가 Activity 또는 다른 어플 컴포넌트에 의해 호출되는지만 다른 프로세스에서 원격으로 실행되며, 이에 대한 결과를 호출자에게 전달된다.

이는 메서드를 분해하고 OS 레벨에서 이해할 수 있는 데이터를 수반한다. 이는 로컬 프로세스와 해당 주소 공간에서 원격 프로세스와 그 주소 공간에 전송하는 것이며, 호출자 측에서 재결합하고 재구현하는 것이다.

반환되는 값들은 반대 방향으로 전송된다. 안드로이드는 IPC 처리를 수행하도록 하는 모든 코드를 제공하므로, 개발자는 RPC 프로그래밍 인터페이스를 정의하고 구현하는 것에 집중할 수 있다.

IPC 를 실행시키기 위해, 어플리케이션은 bindService() 메서드를 이용하여 service와 결합(bind) 해야 한다.

Android는 RPC를 사용하는 IPC 메커니즘을 제공한다. RPC에서는 메서드가 액티비티나 다른 앱 컴포넌트에 의해 호출되지만 실행은 다른 프로세스에서 원격으로 이루어지며, 그 결과가 호출자에게 반환된다.

  1. 메서드 호출과 데이터를 OS가 이해할 수 있는 수준으로 분해 (Marshalling) 하는 과정을 수반하며
  2. 호출과 데이터를 로컬 프로세스 및 주소 공간에서 원격 프로세스 및 주소 공간으로 전송한 뒤
  3. 원격 프로세스에서 다시 조립 (Unmarshalling) 하여 실행하는 과정을 거친다.
  4. 실행 결과의 반환 값은 반대 방향으로 전송된다.

안드로이드는 이러한 IPC 트랜잭션을 수행하는 모든 코드를 제공하므로, 개발자는 RPC 프로그래밍 인터페이스를 정의하고 구현하는 것에만 집중할 수 있다. IPC를 수행하려면 애플리케이션은 bindService()를 사용하여 서비스에 결합 (bind) 해야 한다.


참고

Processes and threads overview  |  App quality  |  Android Developers

Processes and app lifecycle  |  App architecture  |  Android Developers

Services overview  |  Background work  |  Android Developers

Binder overview  |  Android Open Source Project

0개의 댓글