안드로이드 스레드. 핸들러와 메시지. (Android Thread. Handler and Message)

2019. 5. 27. 15:03


1. 안드로이드 메인 스레드.

[안드로이드 스레드]에서 설명한 내용 중에서 메인 스레드와 관련된 내용을 간단히 정리해볼까요?


스레드란, 프로세스 내에서 "순차적으로 실행되는 실행 흐름"의 최소 단위를 말합니다. 프로그램의 main() 함수로부터 시작되는 최초 실행 흐름 또한 하나의 스레드이며, 이를 메인 스레드라고 부릅니다.

안드로이드 앱에서 메인 스레드는 메시지 큐(Message Queue) 수신을 대기하는 루프를 실행하며, 사용자 입력과 시스템 이벤트, 화면 그리기 등의 메시지가 수신되면 각 메시지에 매핑된 핸들러의 메서드를 실행합니다.


어떠한 경우에 스레드를 사용해야 하는지, 그 판단은, 구현하고자 하는 기능이 메인 스레드와 병행적으로(Concurrently) 실행되어야 하는가를 확인하는 것입니다. 이를 다르게 말하자면, 어떤 기능을 메인 스레드에 구현했을 때, 메인 스레드 동작에 영향을 주는가를 확인하는 것이죠. 실행 시간이 오래 걸린다거나, 외부 데이터를 수신하기 위해 대기 상태에 머물러야 하는 경우, 이는 명백히 메인 스레드의 동작에 영향을 줄 수 있기 때문에 별도의 스레드로 작성해야 합니다.


그런데 메인 스레드와 병행적으로(Concurrently) 실행되어야 하는 기능을 구현하기 위해 스레드를 사용하는 것은 그리 간단한 작업이 아닙니다. 하나의 스레드를 생성하고 실행하기 위해 작성하는 코드의 단순함과는 달리, 두 개 이상의 스레드를 동시에 실행할 때는 상황이 복잡해지는데요, 두 개 이상의 스레드로 프래그래밍하는 것, 즉, 멀티 스레드(Multi Thread) 프로그래밍에서는 고려해야 할 사항과 풀어야 할 이슈들이 싱글 스레드(Single Thread) 프로그래밍에 비해 훨씬 많아지기 때문입니다.


안타깝게도, 멀티 스레드 프로그래밍에서 만날 수 있는 이슈들을 소개하고 해결 방법을 제시하는 것은 몇 개의 문장으로 끝낼 수 있는 분량이 아닙니다. 한 권의 책으로 펴내도 모자랄 정도죠. 그렇다고 미리 겁 먹을 필요는 없습니다. (대용량 서버 프로그래밍이 아닌) 일반적인 안드로이드 앱 개발 범위 내에서는, 몇 가지 이슈들에 대해서만 파악하고 있어도 큰 문제없이 멀티 스레드 프로그래밍을 할 수 있으니까요.


자, 그럼 먼저, 멀티 스레드 프로그래밍에서 가장 쉽게 만날 수 있는 이슈인 "스레드 간 통신"에 대해 살펴보겠습니다. 참고로, 멀티 스레드 프로그래밍이 사용되어야 하는 이유와 스레드 간 통신의 필요성에 대해서는 [안드로이드 스레드 예제. 스레드로 고민해보기]에서, 간단한 예제를 통해 소개하였으므로 먼저 읽어보시길 바랍니다.

2. 스레드(Thread)와 스레드 통신(Thread Communication)

프로그램에서 실행되는 모든 스레드는 기본적으로 독립적인 실행 흐름을 가집니다. 이 말은, 일단 스레드가 실행되고 나면 다른 스레드로부터 어떠한 간섭(?)도 받지 않고, 다른 스레드가 어떻게 실행되고 있는지 관심을 두지 않는다는 것을 의미합니다.


하지만 이러한 독립성은 스레드의 본질적 특성을 얘기하는 것일 뿐, 실제 상황에서는 개발자가 스레드의 실행 상태 또는 결과를 다른 스레드로 전달하도록 의도적으로 구현해야 하는 경우가 훨씬 많습니다.


예를 들어, USB 메모리에 있는 파일을 내부 스토리지로 복사해야하는 상황을 가정해보죠. ([안드로이드 스레드 예제. 스레드로 고민해보기]를 이어, 조금은 억지스러운(?) 예제를 하나 더 들어볼게요.)


파일의 크기가 기가바이트 단위인 경우, 복사 완료까지 많은 시간이 소요되므로 파일 복사 기능은 메인 스레드가 아닌 별도의 스레드를 통해 처리하기로 결정합니다. 파일 복사 스레드는 지정된 파일을 복사하도록 단순하게 구현하였습니다. 이제, 메인 화면에 "COPY" 버튼을 추가하고, 버튼을 누르면 새로운 스레드가 실행되도록 만들면 되겠네요.

스레드 통신 예제 1


자, 이제 앱을 실행한 다음 "COPY" 버튼을 클릭합니다. 그런데 당연하게도, 화면에는 아무런 표시가 되지 않습니다. 파일 복사가 시작되었는지, 얼마만큼 진행되었는지, 복사하는 과정에서 에러는 안 났는지, 복사가 완료되었는지... 도대체 알 수가 없네요. 이는 분명 사용성 측면에서 매우 좋지 않은 결과물입니다. 그래서 이제 기능을 수정하기로 결정합니다. 현재 진행 상태 또는 결과를 화면에 표시하도록 말이죠.

스레드 통신 예제 2


직관적으로 접근하여, 메인 화면에 프로그레스바(ProgressBar)를 추가한 다음, 파일 복사 스레드에서 진행 상황을 프로그레스바에 나타내도록 만들면 되겠죠? 하지만 결과는, 알다시피 "Only the original thread that created a view hierarchy can touch its views." 에러가 발생합니다. [안드로이드 스레드 예제. 스레드로 고민해보기]에서 그 과정을 직접 확인했듯이, 안드로이드에서 화면 UI 뷰 접근은 반드시 메인 스레드에서 실행되어야 하니까요.

스레드 통신 예제 3


결국 우리가 선택할 수 있는 가장 직관적이고 쉬운 방법은, 파일 복사 스레드의 진행 상태를 메인 스레드로 메시지 형태로 전달하고, 메인 스레드에서는 그 값을 사용하여 현재 진행 상태를 화면에 표시하는 것입니다.

스레드 통신 예제 4


이렇게, 하나의 스레드에서 다른 스레드로 데이터를 전달하는 것을 스레드 통신(Thread Communication)이라고 합니다.

3. 안드로이드 스레드 통신. 핸들러(Handler)

안드로이드에서 사용할 수 있는 스레드 통신 방법은 여러 가지가 있지만, 가장 일반적으로 사용할 수 있는 방법은 핸들러(Handler)를 통해 메시지(Message)를 전달하는 방법입니다. 안드로이드 핸들러(Handler)에 대한 내용은 [안드로이드 스레드 - 3.2 안드로이드 메인 UI 스레드]에서 간단하게 언급했었는데요. 안드로이드 프레임워크에 아래 그림과 같은 구조로 구성되어 있습니다. (대략적인 흐름 파악을 위한 용도로만 보시기 바랍니다.)

스레드 통신 흐름


핸들러(Handler)의 동작을 이해하기 위해서는 각 요소들의 역할에 대해 알아둘 필요가 있습니다.

3.1 메시지. (Message, android.os.Message)

스레드 통신에서 핸들러를 사용하여 데이터를 보내기 위해서는, 데이터 종류를 식별할 수 있는 식별자와 실질적인 데이터를 저장한 객체, 그리고 추가 정보를 전달할 객체가 필요합니다. 즉, 전달할 데이터를 한 곳에 저장하는 역할을 하는 클래스가 필요한데요, 이 역할을 하는 클래스가 바로 Message 클래스입니다. (https://developer.android.com/reference/android/os/Message)


하나의 데이터를 보내기 위해서는 한 개의 Message 인스턴스가 필요하며, 일단 데이터를 담은 Message 객체를 핸들러로 보내면 해당 객체는 핸들러와 연결된 메시지 큐(Message Queue)에 쌓이게 됩니다.

3.2 메시지 큐. (MessageQueue, android.os.MessageQueue)

메시지 큐(Message Queue)는 이름 그대로 Message 객체를 큐(Queue) 형태로 관리하는 자료 구조를 말합니다. 큐(Queue)라는 이름답게 FIFO(First In First Out) 방식으로 동작하기 때문에, 메시지는 큐에 들어온 순서에 따라 차례대로 저장됩니다. (First In). 그리고 가장 먼저 들어온 Message 객체부터 순서대로 처리됩니다. (First Out).(https://developer.android.com/reference/android/os/MessageQueue)


안드로이드의 메시지 큐는 MessageQueue 클래스에 구현되어 있으며, 앱의 메인 스레드에서 기본적으로 사용되고 있습니다. 하지만 개발자가 MessageQueue 객체를 직접 참조하여 메시지를 전달하거나, 메시지를 가져와서 처리하지는 않습니다. 메시지 전달은 메시지 큐에 연결된 핸들러(Handler)를 통해서, 그리고 메시지 큐로부터 메시지를 꺼내고 처리하는 역할은 루퍼(Looper)가 수행하기 때문입니다.

3.3 루퍼. (Looper, android.os.Looper)

MessageQueueMessage 객체 리스트를 관리하는 클래스일 뿐, 큐에 쌓인 메시지 처리를 위한 핸들러를 실행시키지는 않습니다. 메시지 루프, 즉, 메시지 큐로부터 메시지를 꺼내온 다음, 해당 메시지와 연결된 핸들러를 호출하는 역할은 루퍼(Looper)가 담당합니다. "루퍼(Looper)"라는 이름에서 알 수 있듯이, 메시지 처리를 위한 메시지 루프(Message loop)를 실행하는 것이죠. (https://developer.android.com/reference/android/os/Looper)


안드로이드 앱의 메인 스레드에는 Looper 객체를 사용하여 메시지 루프를 실행하는 코드가 이미 구현되어 있고, 해당 루프 안에서 메시지 큐의 메시지를 꺼내어 처리하도록 만들어져 있습니다. 메인 스레드에서 메시지 루프와 관련된 코드를 개발자가 추가적으로 작성할 필요는 없는 것이죠. 개발자가 할 일은, 메인 스레드로 전달할 Message 객체를 구성하고, 스레드의 메시지 큐에 연결된 핸들러(Handler)를 통해 해당 메시지를 보내기만 하면 됩니다.

3.4 핸들러(Handler, android.os.Handler)

핸들러(Handler)는 스레드의 루퍼(Looper)와 연결된 메시지 큐로 메시지를 보내고 처리할 수 있게 만들어줍니다. 메인 스레드의 메시지 처리 흐름에서, 메시지 전달과 처리를 위해 개발자가 접근할 수 있는 창구 역할을 수행한다고 할 수 있죠.(https://developer.android.com/reference/android/os/Handler)


스레드와 연관된 핸들러를 얻기 위해서는, 간단하게 new 키워드를 사용하여 Handler 클래스 인스턴스를 생성하기만 하면 됩니다. 그러면 새로운 Handler 인스턴스는 자동으로 해당 스레드와 메시지 큐에 연결(bound)되고, 그 시점부터 핸들러를 통해 메시지를 보내고 처리할 수 있게 됩니다.



여기까지, 핸들러(Handler)를 통해 안드로이드 스레드 통신이 수행될 때 필요한, 메시지 전달 시스템을 구성하는 요소들에 대해 간략히 알아봤는데요. 이제 안드로이드에서 제공되는 핸들러(Handler)를 사용하여 스레드 통신을 구현하는 절차와, 핸들러가 제공하는 추가적인 기능에는 어떤 것들이 있는지 좀 더 자세하게 알아보도록 하겠습니다.

4. 안드로이드 핸들러(Handler) 사용 방법.

일반적으로 핸들러(Handler)를 사용하여 스레드 통신을 수행하는 절차는 아래 그림과 같습니다. (최대한 간단한 설명을 위해, 루퍼(Looper)가 기본적으로 동작하고 있는 메인 스레드에 메시지를 보내는 경우를 가정하였습니다.)

안드로이드 핸들러 사용 절차


4.1 메시지 수신 스레드 : Handler 객체 생성 및 handleMessage() 메서드 오버라이드.

가장 먼저 할 일은 메인 스레드에서 수신 메시지를 처리하기 위해 핸들러 객체를 생성하는 것입니다.


    Handler handler = new Handler() {

    } ;


핸들러는 생성과 동시에, 코드가 실행된 스레드에 연결(bind)됩니다. 좀 더 정확히는, Handler 클래스 생성자에서 현재 스레드의 루퍼(Looper) 및 메시지 큐(MessageQueue)에 대한 참조를 가지게 되는 것인데요, 이후 단계에서 메시지를 보낼 때 이 참조를 사용하여 메시지 큐에 메시지를 넣습니다.


핸들러를 생성하고 나서 다음 할 일은, 핸들러에서 수신한 메시지를 처리하기 위해 handleMessage 메서드를 오버라이드하는 것입니다.


public class MainActivity extends AppCompatActivity {

    // 메시지 종류를 식별하기 위해, what 변수에 전달할 값을 상수로 정의.
    private final int MSG_A = 0 ;
    private final int MSG_B = 1 ;

    private final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_A :
                    break ;
                case MSG_B :
                    break ;
                // TODO : add case.
            }
        }
    } ;
}


handleMessage() 메서드는 메시지 루프를 통해 메시지 큐(MessageQueue)로부터 꺼낸 메시지를 처리할 수 있도록, 루퍼(Looper)에 의해 실행되는 메서드입니다. 당연히, 다른 스레드로부터 전달된 데이터는 msg 인스턴스에 담겨져 있으며, 일반적으로, 정수 타입인 what 변수의 값에 따라 if 또는 switch 등의 조건문으로 처리합니다. (위의 예제 코드 참고)

4.2 메시지 송신 스레드 : 수신 스레드의 핸들러 객체 참조를 통해 메시지 객체 획득.

메시지를 보내는 곳에서는 먼저, 앞서 생성한 수신 스레드의 핸들러 객체 참조를 획득해야 합니다. 메인 스레드인 경우, 액티비티의 클래스 변수로 핸들러 객체를 선언하고, 액티비티 참조를 통해 핸들러 객체를 참조할 수 있습니다. 액티비티 내에서 스레드를 생성했다면, 핸들러 객체를 바로 참조할 수 있습니다.


    class NewThread extends Thread {
        
        Handler handler = mHandler ;

        @Override
        public void run() {

            while (true) {
                // obtain a message.
                Message message = handler.obtainMessage() ;

                // fill the message object.
                message.what = MSG_A ;
                message.arg1 = ... ;
                message.arg2 = ... ;
                message.obj = ... ;

                // send message object.
                handler.sendMessage(message) ;
            }
        }
    }


메시지 객체를 획득하기 위해서는 HandlerobtainMessage() 메서드를 사용합니다. obtainMessage() 메서드는 글로벌 메시지 풀(Global Message Pool)로부터 메시지를 가져오는데, 정적(static)으로 생성된 재사용(recycled) 객체로 관리되기 때문에 new 키워드로 새로운 Message 인스턴스를 만드는 것보다 효율적입니다.


Message 클래스에는 아래와 같은 public 클래스 변수가 존재합니다.


클래스 변수 설명
int what 메시지 종류 식별을 위한 사용자 정의 메시지 코드.
int arg1 메시지를 통해 전달되는 정수 값 저장.
int arg2 메시지를 통해 전달되는 정수 값 저장.
Object obj 수신 스레드에 전달할 임의의 객체 저장.
Messenger replyTo 해당 메시지에 대한 회신 용 메신저.
int sendingUid 메시지를 보낸 uid를 가리키는 필드.


Message 객체를 획득할 때 모든 클래스 변수를 사용해야 하는 것은 아니기 때문에, obtainMessage() 메서드는 사용할 필드 종류에 따라 여러 형태의 메서드가 존재합니다.


메서드 프로토타입 설명
Message obtainMessage() 메시지의 target이 핸들러 자신으로 지정된 Message 객체 리턴
Message obtainMessage(int what) what이 지정된 Message 객체 리턴.
Message obtainMessage(int what, int arg1, int arg2) what, arg1, arg2가 지정된 Message 객체 리턴.
Message obtainMessage(int what, Object obj) what, obj가 지정된 Message 객체 리턴.
Message obtainMessage(int what, int arg1, int arg2, Object obj) what, arg1, arg2, obj가 지정된 Message 객체 리턴.


4.3 메시지 보내기.

obtainMessage() 메서드로 획득한 메시지 객체에 보내고자 하는 데이터를 채우고 나면, 마지막으로 할 일은 Handler.sendMessage() 메서드를 사용하여 메시지 객체를 수신 스레드에 보내는 것입니다.


    // send message object.
    handler.sendMessage(message) ;


sendMessage() 메서드 또한, 파라미터, 메시지가 보내지는 시점, 메시지 큐 내 위치 등에 따라 다양한 프로토타입이 존재합니다.


메서드 프로토타입 설명
boolean sendEmptyMessage(int what) Message 클래스 변수 중 what 멤버만 채워진 Message 객체 전달.
boolean sendEmptyMessageAtTime(int what, long uptimeMillis) uptimeMillis에 지정된 시각에, what 멤버만 채워진 Message 객체 전달.
boolean sendEmptyMessageDelayed(int what, long delayMillis) 현재 시각에서 delayMillis 만큼의 시간 후에, what 멤버만 채워진 Message 객체 전달.
boolean sendMessage(Message msg) Message 객체 전달. 메시지 큐의 가장 마지막에 msg 추가.
boolean sendMessageAtFrontOfQueue(Message msg) Message 객체 전달. 메시지 큐의 가장 처음 위치에 msg 추가.
boolean sendMessageAtTime(Message msg, long uptimeMillis) uptimeMillis에 지정된 시각에, Message 객체 전달.
boolean sendMessageDelayed(Message msg, long delayMillis) 현재 시각에서 delayMillis 만큼의 시간 후에, Message 객체 전달.


리스트에 정리된 메서드들 중 AtTime, Delayed, AtFrontOfQueue 등의 접미사가 붙은 메서드는 메시지가 처리되는 시점을 조절하기 때문에, 메시지 큐 안에서의 메시지 처리 우선순위 또는 스레드 실행 대기 시간 등에 영향을 미칠 수 있습니다. 그러므로 각 메서드의 정확한 동작 방식과 그에 따른 주의사항에 대해 정확히 인지하고 사용하시길 바랍니다.


그리고 리스트에서 따로 정리하지는 않았지만, Handler 클래스에는 메시지를 보낼 때 사용하는 sendMessage() 외에 Runnable 객체를 보낼 때 사용할 수 있는 post() 메서드가 존재합니다. post() 관련 메서드를 사용하면, 상황에 따라 sendMessage() 보다 훨씬 간단하게 스레드 통신을 수행할 수 있습니다. 관련 내용은 이후에 다시 정리하는 글에서 확인하실 수 있습니다.

5. 핸들러와 관련된 몇 가지 참고 사항.

5.1 스레드 통신의 대상은 자기 자신도 포함된다.

스레드 통신을 위해 핸들러를 사용할 때, 오직 다른 스레드에서만 메시지를 보낼 수 있는 것으로 잘 못 이해하는 경우가 있는데요, 스레드 통신의 대상은 자기 자신이 될 수도 있습니다.

스레드 통신의 대상이 자기 자신


"음, 그런데 자기 자신에게 메시지를 왜 보내지? 굳이 메시지를 보내지 말고, 그냥 메서드 호출을 하면 되는 거 아닌가?"라는 의문이 들 수도 있을텐데요. 앱을 만들다보면 가끔 자기 자신에게 메시지를 보내야 하는 경우가 있습니다. 외부 스레드에서 전달되는 메시지 처리를 위해 구현한 기능을 그대로 사용하거나, 순차적으로 실행되어야 하는 코드들 사이에 시스템 이벤트가 고려되어야 하는 상황 등이 바로 그런 경우입니다.

5.2 핸들러는 스레드 당 반드시 하나만 생성해야 하는가?

아닙니다. 핸들러는 여러 개 만들 수 있습니다. 아니, 오히려 하나의 핸들러에서 모든 메시지를 처리하는 것 보다, 메시지 종류 및 기능에 따라 여러 개의 핸들러로 나누어서 처리하는 게 더 낫습니다. 물론, 메시지를 보내는 스레드에서 적절한 핸들러를 선택하여 메시지를 보내는 것은 잊지 말아야 겠지요.


    private final Handler mNetworkHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // TODO : process network message.
        }
    } ;

    private final Handler mDeviceHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // TODO : process device message.
        }
    } ;    

6. 참고.

.END.


ANDROID 프로그래밍/THREAD