(원문: http://developer.android.com/guide/components/processes-and-threads.html)

(위치: Develop > API Guides > App Components
> Processes and Threads
)

(전체 번역 글 목록으로 이동하기)


프로세스와 쓰레드


어떤 앱 컴포넌트가 해당 앱에 대하여 처음 시작되는 경우, 즉 그 앱에 (실행 중인) 다른 앱 컴포넌트가 없는 경우에, 안드로이드 시스템은 리눅스 프로세스를 새로 생성하여 실행하고 쓰레드를 만들어서 앱 컴포넌트를 실행시켜 줍니다. 기본적으로, 동일한 앱의 컴포넌트들은 동일한 프로세스 및 ("메인" 쓰레드라고 불리는) 쓰레드에서 실행됩니다. 앱의 다른 컴포넌트가 이미 존재하여 프로세스가 이미 생성되었다면, 또 다른 앱 컴포넌트가 실행될 때 그 프로세스에서 실행되며 쓰레드도 같습니다. 하지만 기본적인 동작이 그러할 뿐, 내 앱의 컴포넌트들을 서로 다른 프로세스에서 실행되도록 할 수 있으며, 쓰레드도 얼마든지 새로 생성하여 실행할 수 있습니다. 

본 문서에서는 안드로이드 앱에서 프로세스와 쓰레드가 어떻게 동작하는지에 대하여 설명합니다. 


프로세스

기본적으로 앱의 모든 컴포넌트들은 동일한 프로세스에서 실행되며, 대부분의 경우 이를 변경할 필요가 없습니다. 하지만 특정 앱 컴포넌트를 다른 프로세스에서 실행시켜야 할 필요가 있는 경우, 매니페스트 파일에서 그 앱 컴포넌트가 실행될 프로세스를 지정하도록 할 수 있습니다. 

매니페스트 파일에서 앱 컴포넌트들을 선언할 때, <activity>, <service>, <receiver>, <provider> 요소를 사용하는데, 이들은 android:process 속성을 통해 해당 컴포넌트가 실행될 프로세스를 지정할 수 있습니다. 이 속성을 지정함으로써, 각 컴포넌트들이 서로 다른 프로세스에서 실행되도록 할 수도 있고, 대체로 같은 프로세스에서 실행하되 일부만 다른 프로세스에서 실행되도록 할 수도 있으며, 심지어는 서로 다른 앱인데 같은 프로세스에서 실행되도록 할 수도 있습니다. 하지만 서로 다른 앱이 같은 프로세스에서 실행되려면, 해당 앱들이 동일한 리눅스 유저 ID를 공유해야하고, 동일한 인증서(certificates)로 서명되어(signed) 있어야 합니다. 

<application> 요소에서도 android:process 속성을 지정할 수 있는데, 이는 해당 앱의 모든 컴포넌트들에 대한 기본값이 됩니다. 

안드로이드는 메모리가 부족하거나 다른 앱의 컴포넌트가 먼저 메모리를 필요로 하는 경우 프로세스를 종료시킬 수도 있습니다. 그러면 결과적으로 그 프로세스에서 실행 중이던 앱 컴포넌트들도 모두 종료됩니다. 그리고 해당 앱 컴포넌트들이 다시 실행될 때 프로세스도 다시 시작됩니다. 

안드로이드 시스템은 어떤 프로세스를 종료시킬지 결정할 때, 그 프로세스가 사용자에게 얼마나 중요한지를 판단합니다. 예를 들어, 더 이상 화면에 보이지 않는 액티비티를 실행중인 프로세스가, 화면에 보이고 있는 액티비티를 실행중인 프로세스보다 더 우선적으로 종료되는 것입니다. 따라서 어떤 프로세스를 종료시킬지 결정하는 기준은, 프로세스에서 실행중인 앱 컴포넌트들의 상태에 달려있다고 볼 수 있습니다. 그에 대한 자세한 내용은 이어지는 섹션에서 설명합니다. 


프로세스의 생명주기

안드로이드 시스템은 앱의 프로세스를 가능한 한 유지하려고 노력합니다. 하지만 시간이 지날수록 새로운 프로세스나 더 중요한 프로세스를 실행하기 위해 메모리가 필요해지고, 그 때문에 오래된 프로세스는 종료시켜야 할 필요가 생깁니다. 시스템은, 프로세스들 중에 어떤 것을 살리고 어떤 것을 종료시킬지를 결정하기 위해서, 프로세스들을 해당 프로세스에서 실행 중인 컴포넌트들의 상태에 기반한 "중요도 계층"으로 나눕니다. 그리고 부족한 시스템 자원을 채우기 위해서 가장 중요도가 낮은 프로세스들을 먼저 종료시키고, 또 필요하면 그 다음으로 중요도가 낮은 프로세스들을 종료시킵니다. 

중요도 계층은 아래의 5가지로 구분됩니다. 중요도가 가장 높은 것부터 설명합니다. 

1. 전방에 있는 프로세스(Foreground process)

사용자가 현재 뭔가 하고 있는 프로세스입니다. 아래 조건들 중 하나 이상을 만족한다면 그 프로세스는 전방(foreground)에 있는 것으로 간주합니다.

  • 사용자와 상호작용하고 있는 Activity(onResume()이 호출된 Activity)를 실행중인 프로세스
  • 사용자와 상호작용하고 있는 Activity에 바인드된 Service를 실행 중인 프로세스
  • "전방에 있는" Service(startForeground()가 호출된 Service)를 실행 중인 프로세스
  • onCreate(), onStart(), onDestroy()와 같은 생명주기 콜백 메소드가 호출되어 리턴되기 전까지의 Service를 실행 중인 프로세스
  • onReceive() 메소드가 호출되어 리턴되기 전까지의 BroadcastReceiver를 실행 중인 프로세스

일반적으로 전방(foreground)에는 한번에 많은 프로세스가 존재하지 않습니다. 그리고 이러한 프로세스는, 가용 메모리가 너무 적어서 더이상 아무것도 실행할 수 없을 때에만 종료될 수 있습니다. 

2. 보여지고 있는 프로세스(Visible process)

전방에 있는 컴포넌트는 없지만, 사용자에게 보여지는 컴포넌트가 실행 중인 프로세스입니다. 아래 조건 중 하나를 만족하면 사용자에게 보여지는 것으로 간주합니다:

  • 전방에 있지는 않지만 여전히 화면에 보이는 Activity(onPause()가 호출된 Activity)를 실행 중인 프로세스. 예를 들어, 전방에 있는 Activity에서 다이얼로그를 실행한 경우, 다이얼로그가 전방으로 오고, Activity는 전방에서 벗어나 사용자와 상호작용할 수는 없지만 화면에는 보이는 상태가 됩니다. 
  • "보이는" Activity에 바인드 된 Service를 실행 중인 프로세스

"보이는" 프로세스들도 매우 중요하기 때문에, 전방에 있는 프로세스 외에 다른 상태의 프로세스가 없을 때에만 종료시킬 수 있습니다.

3. 서비스 프로세스(Service process)

startService()가 호출되어 Service를 실행 중인 프로세스지만, 위의 전방에 있는 프로세스나 보여지고 있는 프로세스가 아닌 경우에 서비스 프로세스로 구분합니다. 비록 사용자의 눈에 뭔가가 보이는 것은 아니지만, 후방(background)에서 음악을 재생한다거나, 네트워크를 통해 데이터를 다운로드 하는 등의 일을 합니다. 전방에 있는 프로세스와 보여지고 있는 프로세스 외에 다른 상태의 프로세스가 없을 때에만 종료시킬 수 있습니다. 

4. 후방에 있는 프로세스(Background process)

사용자에게 보여지지 않는 Activity(onStop()이 호출된 Activity)를 실행 중인 프로세스입니다. 이러한 프로세스들은 사용자에게 직접적인 영향을 미치지 못하며, 전방에 있는 프로세스나 보여지고 있는 프로세스나 서비스 프로세스를 실행할 때 메모리 복구를 위해 종료될 수 있습니다. 보통 후방에 있는 프로세스는 많이 존재할 수 있기 때문에, 시스템은 최근 사용 목록(least recently used list, LRU list)을 참조하여, 최근에 사용된 Activity를 가장 나중에 종료시킵니다. Activity가 생명주기 콜백 메소드들을 제대로 구현하여, Activity 종료시 현재 상태를 저장하고, 재생성시 현재 상태를 복구하고 있다면, Activity가 종료되고 나서 사용자가 뒤로 버튼을 눌러 다시 돌아왔을 때에 Activity가 재생성 되면서 이전 상태로 복구되기 때문에 사용자에게 비정상적인 경험을 주지는 않습니다. Activity에서 현재 상태를 저장하고 복구하는 방법에 대해서는 Activity 상태 저장하기에서 학습하실 수 있습니다. 

5. 텅 빈 프로세스(Empty process)

활성화된 앱 컴포넌트가 하나도 없는 프로세스입니다. 이러한 프로세스가 존재하는 이유는, 프로세스 자체를 캐싱(caching)함으로써 앱 컴포넌트를 다시 실행해야할 때 프로세스를 새로 생성하지 않기 위함입니다. 시스템은 메모리가 부족한 경우뿐만 아니라, 프로세스 캐시와 커널 캐시 간의 균형을 맞추기 위해 이러한 프로세스들을 종료시키기도 합니다. 

안드로이드 시스템은 프로세스가 실행 중인 앱 컴포넌트들의 상태에 기반하여, 해당 프로세스를 위의 5개 계층의 조건에 부합하는 계층 중 가장 상위 계층으로 지정합니다. 예를 들어, 프로세스가 Service와 보이는(visible) Activity를 실행 중이라면, 그 프로세스는 서비스 프로세스가 아니라 보여지고 있는 프로세스입니다. 

그리고, 프로세스의 계층은 프로세스 간의 의존성에 의해 결정될 수도 있습니다. 어떤 프로세스 B가 다른 프로세스 A에 의존적인 상황이라면, A는 B보다 낮은 계층일 수 없습니다. 예를 들면, 프로세스 A의 컨텐트 프로바이더가 프로세스 B의 클라이언트에게 데이터를 제공하고 있는 상황이거나, 또는 프로세스 A의 Service가 프로세스 B의 컴포넌트에 바인드 되어 있는 상황인 경우, 프로세스 A의 계층은 프로세스 B의 계층 이상이 되어야 합니다. 

Service를 실행 중인 프로세스가 후방에 있는 Activity를 실행 중인 프로세스보다 중요도 계층이 높기 때문에, Activity에서 시간이 오래 걸리는(long-running) 작업을 비동기로 실행시키기 위하여, 쓰레드 보다는 Service를 이용하는 것이 좋습니다. 예를 들어, 어떤 Activity에서 사진을 업로드 해야하는 경우에 Service를 생성하여 사용하면 그 Activity에서 빠져나가더라도 "서비스 프로세스"가 되지만, 쓰레드를 생성하여 사용했다면 Activity에서 빠져나가는 순간 "후방에 있는 프로세스"가 되기 때문에 더 중요도가 낮아지게 되는 것입니다. 그리고 이와 같은 이유로, 브로드캐스트 리시버의 경우에도 시간이 오래 걸리는(time-consuming) 작업은 쓰레드보다 Service를 사용하는 것이 좋습니다. 


쓰레드

앱이 실행되면, 시스템은 "메인 쓰레드"를 생성합니다. 메인 쓰레드는 버튼 같은 사용자 인터페이스의 이벤트 관련 동작 및 화면에 뷰를 그려주는 동작 등을 담당하며, android.widgetandroid.view 패키지에 포함된(안드로이드 UI 툴킷에 포함된) 뷰들과의 상호작용을 담당합니다. 그래서 메인 쓰레드를 UI 쓰레드라고 부르기도 합니다. 

시스템은 각 컴포넌트에 대하여 별도의 쓰레드를 생성하지 않습니다. 동일한 프로세스 내에서 실행되는 모든 컴포넌트들은 하나의 UI 쓰레드에서 실행됩니다. 따라서, onKeyDown() 같은 이벤트 관련 콜백 메소드나 앱 컴포넌트의 생명주기 콜백 메소드 등의 시스템 콜백 메소드는 항상 UI 쓰레드에서 실행됩니다. 

예를 들어, 사용자가 버튼을 터치하면, UI 쓰레드가 터치 이벤트를 버튼에게 보내서 눌려진(pressed) 상태로 변경시킨 후, 새로 그리기(invalidate) 요청을 이벤트 큐에 추가합니다. 그리고 적당한 시점에 UI 쓰레드가 다시 해당 요청을 꺼내서(dequeues) 버튼에게 새로 그리라고 알려주는 것입니다. 

사용자와의 상호작용과 관련된 콜백 메소드에서 많은 일을 해야하는 경우, UI 쓰레드에서만 처리하게 되면 퍼포먼스가 낮아질 수 있습니다. 특히, 네트워크 관련 작업이나 데이터베이스에 질의하는 등의 시간이 오래 걸리는 작업은, 그 시간 동안 전체 UI를 멈춰(block) 버립니다. 이렇게 UI 쓰레드가 멈추면, 어떠한 이벤트도 발생하지 못하고, 화면에 뷰를 새로 그리지도 못하게 되며, 이런 상태로 약 5초 정도 지나면 응답대기(application not responding, ANR) 다이얼로그를 출력하게 됩니다. 이것은 사용자로 하여금 짜증을 유발시킬 수 있고 앱을 삭제하게 만드는 원인이 될 수도 있습니다. 

게다가, 안드로이드 UI 툴킷은 멀티 쓰레드에 안전하지(thread-safe) 않습니다.  따라서 UI 관련 갱신 작업을 워커 쓰레드에서 하면 안됩니다. 간단히 정리하자면, UI 관련 작업과 관련하여 아래 2가지 규칙을 지켜야 합니다.

1. UI 쓰레드를 멈추면(block) 안됩니다.

2. UI 쓰레드가 아닌 쓰레드에서 안드로이드 UI 툴킷에 접근하면 안됩니다.


워커(Worker) 쓰레드

위에서 설명한 바와 같이, UI 쓰레드만 사용하는 경우 쓰레드가 멈추면 앱이 아무런 반응을 할 수 없는 상태가 됩니다. 따라서 시간이 걸리는 작업을 해야하는 경우에는 별도의 쓰레드(후방(background) 쓰레드 또는 워커(worker) 쓰레드)를 생성해야 합니다. 

아래 코드는, 클릭이 발생했을 때 별도의 쓰레드에서 이미지를 다운로드하여 ImageView에 보여주는 것을 나타냅니다:

public void onClick(View v) {
   
new Thread(new Runnable() {
       
public void run() {
           
Bitmap b = loadImageFromNetwork("http://example.com/image.png");
            mImageView
.setImageBitmap(b);
       
}
   
}).start();
}

위의 코드는 네트워크 관련 작업을 처리하기 위해 쓰레드를 생성하고 있기 때문에 제대로 동작할 것처럼 보입니다. 하지만, 위에서 설명한 2가지 규칙 중 하나인 "UI 쓰레드가 아닌 쓰레드에서 안드로이드 UI 툴킷에 접근하면 안된다"는 규칙에 어긋난 코드이기 때문에, ImageView에 이미지를 셋팅하는 작업은 UI 쓰레드에서 하도록 수정해야 합니다. 

워커 쓰레드에서 UI 쓰레드를 실행하는 방법은 아래와 같이 몇가지가 있습니다:

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

따라서, 위의 잘못된 코드는 View.post(Runnable) 메소드를 이용하여 아래 코드와 같이 수정할 수 있습니다:

public void onClick(View v) {
   
new Thread(new Runnable() {
       
public void run() {
           
final Bitmap bitmap =
                    loadImageFromNetwork
("http://example.com/image.png");
            mImageView
.post(new Runnable() {
               
public void run() {
                    mImageView
.setImageBitmap(bitmap);
               
}
           
});
       
}
   
}).start();
}

이제 이 구현은 멀티 쓰레드에 안전(thread-safe)합니다. 네트워크 관련 작업은 별도의 쓰레드를 생성하여 실행하고, ImageView에 이미지를 셋팅하는 작업은 UI 쓰레드에서 실행합니다. 

하지만 복잡도가 증가하면, 코드도 복잡해지고 유지하기 어려워질 수 있습니다. 그리고 UI 쓰레드와 워커 쓰레드 간에 메시지를 전달해야 한다면 Handler를 사용할 수 있으며, AsyncTask를 사용하여 UI와의 상호작용을 위한 워커 쓰레드의 실행을 단순화할 수도 있습니다. 


AsyncTask 사용하기

AsyncTask는 비동기로 작업을 수행하고 나서 UI를 갱신하는 경우에 사용합니다. AsyncTask를 사용하면, 워커 쓰레드에서 시간이 오래 걸릴 수 있는 작업을 수행하고 그 결과를 UI 쓰레드로 보내주는데, 이때 쓰레드를 직접 다루지 않아도 되고, 핸들러(handler)도 따로 사용할 필요가 없습니다.

AsyncTask를 사용하기 위해서는, AsyncTask 클래스를 확장하는 서브클래스를 만들어서 doInBackground() 메소드를 구현해야 합니다. 그리고 onPostExecute() 메소드를 구현함으로써 doInBackground()의 결과를 전달 받아서 UI를 멀티쓰레드에 안전하게(thread-safe) 갱신할 수 있습니다. 마지막으로 AsyncTask 서브클래스에 대한 인스턴스의 execute() 메소드를 호출함으로써 실행할 수 있습니다. 

아래 예제 코드는, 위에서 쓰레드를 사용했던 예제에 대하여 AsyncTask를 사용하는 방식으로 수정한 것입니다:

public void onClick(View v) {
   
new DownloadImageTask().execute("http://example.com/image.png");
}

private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
   
/** The system calls this to perform work in a worker thread and
      * delivers it the parameters given to AsyncTask.execute() */

   
protected Bitmap doInBackground(String... urls) {
       
return loadImageFromNetwork(urls[0]);
   
}

   
/** The system calls this to perform work in the UI thread and delivers
      * the result from doInBackground() */

   
protected void onPostExecute(Bitmap result) {
        mImageView
.setImageBitmap(result);
   
}
}
이제 워커 쓰레드에서 수행할 작업과 UI 쓰레드에서 수행할 작업이 분리되었기 때문에, UI는 안전해졌고 코드는 단순해졌습니다.

AsyncTask에 대한 자세한 내용은 클래스 문서를 참조해야겠지만, 아래와 같이 간단히 요약할 수 있습니다:

  • 매개변수, 프로그레스(progress) 변수, 비동기 처리에 대한 결과값에 대하여 제너릭(generic)을 이용해서 클래스 타입을 지정할 수 있습니다. 
  • AsyncTask를 실행하면 워커 쓰레드에서 doInBackground() 메소드를 자동적으로 실행해줍니다.
  • onPreExecute(), onPostExecute(), onProgressUpdate() 메소드들은 UI 쓰레드에서 실행됩니다.
  • doInBackground()에서 리턴하는 값은 onPostExecute()로 전달됩니다.
  • doInBackground()에서 원하는 시점에 publishProgress()를 호출하면, UI 쓰레드에서 onProgressUpdate()가 실행됩니다.
  • 실행된 태스크는 때(any time)와 장소(any thread)를 가리지 않고 취소시킬 수 있습니다. 

주의사항: 워커 쓰레드를 사용하는 경우 만날 수 있는 또 다른 문제는, 실행 중에 상태가 바뀔 때(예를 들면 디바이스가 가로 또는 세로 모드로 변경되는 경우) 액티비티가 재시작이 되면서 워커 쓰레드가 종료될 수도 있다는 것입니다. 따라서 태스크를 사용하는 경우, 액티비티가 종료될 때 태스크를 취소했다가 액티비티가 다시 시작될 때 태스크를 다시 실행하도록 해야 합니다. Shelves 샘플앱의 소스 코드를 참고하실 수 있습니다. 


멀티쓰레드에 안전한(Thread-safe) 메소드

경우에 따라서는 메소드가 둘 이상의 쓰레드에서 호출될 수도 있기 때문에, 메소드를 구현할 때는 멀티쓰레드에 안전하게(thread-safe) 구현해야 합니다. 

이것은, 액티비티에 바인드 된 서비스의 메소드처럼 원격으로 호출되는 메소드의 경우에 특히 더 그렇습니다. IBinder 인터페이스를 구현한 클래스의 메소드들을 IBinder 구현 객체가 실행되고 있는 동일한 프로세스에서 호출하는 경우, 해당 메소드들은 호출하는 쪽의 쓰레드에서 실행됩니다. 하지만 다른 프로세스에서 호출하는 경우에는, IBinder 구현 객체가 실행되고 있는 프로세스의 쓰레드 풀에서(UI 쓰레드는 제외) 시스템에 의해 선택된 쓰레드에서 해당 메소드들이 실행됩니다. 예를 들어, onBind() 메소드는 서비스가 속한 프로세스의 UI 쓰레드에서 실행되지만, onBind()에서 리턴해주는 IBinder 객체의 메소드들은 (UI 쓰레드가 아닌) 시스템이 선택한 쓰레드에서 실행되는 것입니다. 그리고, 서비스는 둘 이상의 클라이언트에게 바인드 될 수 있기 때문에, IBinder 객체의 메소드들은 둘 이상의 서로 다른 쓰레드에서 실행될 수 있습니다. 따라서 IBinder의 구현 클래스는 멀티 쓰레드에 안전(thread-safe)해야 합니다. 

이와 비슷하게, 컨텐트 프로바이더도 둘 이상의 프로세스로부터 데이터 요청을 받을 수 있습니다. 비록 ContentResolverContentProvider 클래스는 프로세스간 통신에 대한 부분을 숨겨놨지만(캡슐화), ContentProvider의 query(), insert(), delete(), update(), getType()과 같은 메소드들은 ContentProvider가 속한 프로세스의 쓰레드 풀에서 선택된 쓰레드(UI 쓰레드 제외)에서 실행됩니다. 이러한 메소드들도 둘 이상의 쓰레드에서 호출될 수 있기 때문에, 반드시 멀티 쓰레드에 안전(thread-safe)하게 구현되어야 합니다. 


프로세스간 통신(IPC)

안드로이드는 원격 프로시저 호출(RPC) 방식을 사용하여 프로세스간 통신(IPC)을 지원합니다. 이는 원격에서(다른 프로세스에서) 실행되는 액티비티 또는 다른 앱 컴포넌트에서의 호출을 의미하며, 그 호출에 대한 리턴값을 호출한 쪽에 전달해 줄 수 있습니다. IPC를 할 때는 메소드 호출과 그에 따른 데이터들을 OS가 이해할 수 있도록 분해하고, 호출하는 쪽의 프로세스로부터 전송하고, 호출받는 쪽의 프로세스에서 수신되어 재조립 및 재지정의 과정을 거치게 됩니다. 그리고 리턴값을 반대 방향으로 전송합니다. 안드로이드에는 이미 이러한 IPC에 대한 모든 코드가 구현되어 있기 때문에, 내 앱에서는 RPC 인터페이스에 대한 정의 및 구현에만 집중하면 됩니다. 

IPC를 수행하기 위해서는, 내 앱에서 bindService() 메소드를 호출함으로써 서비스를 바인드해야 합니다. 이에 대한 자세한 내용은 서비스에 대하여 문서에서 학습하실 수 있습니다. 


Posted by 개발자 김태우
,