[android] 루퍼와 핸들러의 이해 #3

개요

1편2편에서 루퍼, 핸들러, 메세지, 메세지큐의 기본내용을 다뤘습니다.
이번에는 메세지큐를 섬세하게 통제하는 것과 메인루퍼 등 고급주제로 넘어가겠습니다.

여기까지의 메세지큐 정리

메세지에 대한 내용은 이미 다뤘으니 메세지를 모아둔 메세지큐에 대해서 상세히 살펴보겠습니다. 여태까지의 글에서 언급한 메세지큐에 대한 정보는

  1. 루퍼 하나당 하나의 메세지큐가 있다.
  2. 핸들러를 통해 메세지큐에 삽입한다.
  3. 삽입한 핸들러의 리스너에게 루퍼가 메세지를 돌려준다.
  4. 메세지큐는 시간순으로 정렬된다.

머 이 정도입니다. 이 이상 알아야할 게 있냐구요? 굉장히 많습니다 ^^

메세지큐 이벤트

메세지큐에 이벤트가 있다면 어떤 이벤트일까요? 메세지가 삽입되었다는 이벤트나 삭제되었다는 이벤트를 생각해볼 수도 있겠지만 핸들러를 통해 명시적인 시점을 알고 있으므로 딱히 정의되어있지 않습니다.
오히려 정의되어있는 이벤트는 MessageQueue.IdleHadler로 굳이 번역하자면 유휴메세지 이벤트쯤 되겠네요.
이를 이해하려면 큐의 유휴상태가 무엇인지 알아야하는데 이는 다시 전달경계라는 개념을 먼저 학습해야만 하죠.

  1. 모든 메세지는 타임스탬프가 들어있습니다.
  2. 메세지큐에 들어간 메세지는 일단 대기상태가 되는데
  3. 루퍼는 메세지큐를 감시하다가 현재시간을 기준으로 타임스템프가 넘어가는 경우
  4. 발송할 대상으로 분류하여 발송을 시작합니다.

바로 루퍼가 발송대상으로 분류할지 말지의 시간 경계가 전달경계인 것입니다. 메세지큐의 메세지를 크게 2그룹으로 분류하자면

  1. 전달경계를 넘어서 전달하기로 분류된 녀석과
  2. 전달경계를 넘지 못하고 대기중인 메세지로 볼 수 있는 거죠.

그럼 이제 약간 생각을 바꿔 메세지큐의 모든 메세지가 다 전달 경계를 넘어 루퍼가 메세지 전달을 해버렸다고 상정합니다.
그럼 메세지큐입장에서는 할 일 없이 새 메세지가 올 때까지 놀게(idle) 됩니다. 이런 노는 시기가 발생할때마다 idle이벤트가 발생하는거죠.

실제로 이 정의는 대충 정한 느낌이 들지 않나요? 메세지를 처리하다가 노는 시기라니..

결과적으로 최초 메세지큐를 만들었을 때, 메세지와 메세지 사이의 시간, 마지막에 메세지를 보낸 이후 등에서 발생합니다. 한마디로 촘촘하게 메세지를 큐에 계속 쌓지 않으면

  • 메세지와 메세지의 시간 사이

에서 계속 발생한다는 겁니다. 이걸 대체 어디에 쓰는걸까요?

어디 쓸 지를 생각하기 전에 쓰레드의 관점에서 생각해보죠. 메세지큐의 리스너는 어떤 쓰레드에서 실행될까요?
정답부터 말하자면 루퍼쓰레드입니다. 당연하게 루퍼쓰레드가 메세지큐를 감시하고 있다가 유휴타이밍이 발생하면 리스너를 호출하는거니 그 주체가 되는 쓰레드가 루퍼 쓰레드인게 당연합니다.
그럼 일단 유휴리스너는 루퍼쓰레드가 처리한다를 확정짓습니다. 근데 루퍼쓰레드가 원래 하는 일은 무엇일까요? 메세지큐를 지속적으로 감시하고 메세지를 리스너에게 전송하는 일입니다. 이 일도 루퍼쓰레드가 하죠.

즉 루퍼쓰레드는

  1. 지속적으로 메세지큐를 감시하여 리스너를 호출하거나 태스크를 실행하는 역할과
  2. 유휴타이밍이 발생할때마다 유휴리스너를 호출하는 일을

처리하는 것입니다. 즉 메세지를 처리할 때도 쓰레드를 활용하고 아닐 때도 활용하자! 라는 거죠. 근데 앞 서 말씀드린대로 이 이벤트는 유휴가 발생하는 시점만 가르쳐줍니다. 해서 실질적으로 이 코드가 유효하려면 계속해서 유휴상태인지를 체크해야합니다.

여태까지 말한 걸 코드로 구현하면 다음과 같을 것입니다.

class LooperThead extends Thread{
    public void run(){
        Looper.prepare();

        //현재 루퍼의 메세지큐를 얻는다 ==;
        final MessageQueue que = Looper.myQueue();
        que.addIdleHandler(new MessageQueue.IdleHandler(){
            @TargetApi(Build.VERSION_CODES.M) //안타깝게도 23버전부터 지원
            public boolean queueIdle(){
                while(que.isIdle()){
                   //유휴시기에 할 일
                }
                return true; //false반환하면 리스너해제
            }
        });
        Looper.loop();
    }
}

isIdle()메소드가 무려 23버전이나 되어야 가능하기 때문에(보통 13+로 설정하는 현실에서..) 유휴시기에 할 일을 따로 정하는건 무의미하여 잘 안쓰게 됩니다만, 실제 용도는 메세지의 흐름을 보고 루퍼를 종료시켜 쓰레드자원을 반납하는 용도로 쓰곤 합니다. 하지만 이 판단을 위해서는 리스너 외부사황을 상당부분 인지해야하기 때문에 복잡한 문제를 일으키는 경우가 대부분입니다.
결론적으로 잘 안쓰게 됩니다. 단지 저 이벤트의 최초시점은 반드시 루퍼가 메세지큐를 활성화시킨 상황입니다. 엄밀한 쓰레드제어와 루퍼의 활성화타이밍을 확정지으려면 다음과 같이 해야합니다.

class LooperThead extends Thread{
    Looper looper;
    @Override
    public void run(){
        Looper.prepare();
        Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler(){
            public boolean queueIdle(){
                looper = Looper.myLooper(); //이시점에 가서야 확실히 루퍼가 안정화되었다!
                return false; //더 이상 리스너가 필요없으니 제거
            }
        });
        Looper.loop();
    }
}

사실 저는 이 용도로 가장 많이 쓰고 있습니다(기분학상..=.=)

메세지큐에서 메세지 제거

메세지큐에 삽입하는게 핸들러인 것처럼 제거할 때도 핸들러를 사용합니다. 아직 루퍼에 의해 처리되지 않은 메세지는 전달경계를 넘었든 안넘었든 무조건 삭제할 수 있습니다. 태스크메세지는 removeCallbacks로 제거하고 데이터메세지는 removeMessages로 제거합니다.

우선 메세지를 식별할 수 있어야 해당 메세지만 제거할 수 있을 것입니다. 메세지를 식별할 방법으로

  1. 태그오브젝트
  2. what(데이터메세지)
  3. runnable(태스크메세지)

등을 복합 식별자로 메세지를 구분할 수 있습니다. 단지 핸들러는 자신을 통해 삽입된 메세지에만 접근할 수 있습니다. 따라서 핸들러가 삭제할 수 있는 메세지는 그 핸들러를 통해 삽입된 메세지 뿐입니다.

여기서 좀 지저분한 문제가 발생합니다. 메세지를 삭제한다는 행위는 굉장히 미묘하다는 것입니다.

  1. 이미 그 메세지가 루퍼에 의해 처리되었으면 삭제할 대상이 없을테고
  2. 그 핸들러가 삽입하지 않은 메세지라면 삭제할 수 없을테고
  3. 이미 삭제해버렸다면 존재하지 않을테니 삭제할 수 없을테고
  4. 처음부터 조건에 맞는 메세지가 메세지큐에 존재하지 않으면 삭제할 수 없을 것입니다.

삭제할 수 없는 경우가 많은데 삭제계열 메소드는 예외를 던지는 것도 아니고 결과값을 주는 것도 아닙니다. 즉 몇 개 삭제했는지는 커녕 삭제를 일단 하긴한 건 지 조차 알 수 없다는 것입니다.
따라서 처음부터 메세지를 메세지큐에 넣기 전에 신중함을 기하는 게 기본적인 전략이 되어야합니다.

메세지큐 자체를 분석하기

그렇다고 메세지큐가 그렇게까지 블랙박스는 아닙니다. 몇 가지 외부에 메세지큐의 내부를 볼 수 있는 방법을 제공합니다. 위에서 살펴본 삭제문제도 그렇고 짜피 큐기 때문에 메세지의 생산과 소비가 어느 정도 균형을 이뤄야하기 때문에 메세지큐를 감시하는 것은 의미가 있습니다.

우선 초창기부터 존재하는 완전무식한 handler.dump()메소드가 있습니다. 이 메소드는 무려 Printer를 인자로 받습니다.
Printer의 경우 println(String x)의 추상 클래스입니다. 무려 문자열로 현재 큐상태를 돌려줍니다. 이 문자열을 물론 다시 파싱하면 메세지큐의 현재 상태를 완전히 복원할 수 있긴 합니다…만 =.=
미친짓이고(순수하게 디버깅용으로 만든듯..) 다른 방법으로는 메세지큐에 있는 메세지를 처리하는 루퍼에게 이벤트리스너를 부착해서 감시하는 방식이 있습니다. 우선 미친 짓을 볼까요?

@Override
protected void onCreate(Bundle savedInstanceState) {
    //...
    //메세지하나 꼽고
    l.h1.sendMessage(l.h1.obtainMessage(1));

    //미친짓
    l.h1.dump(new LogPrinter(Log.INFO, "bs"), "");
}

음 코드를 보고 나서도 전혀 저 파서를 만들고 싶은 생각은 안드는걸 보니 역시 미친 짓인듯.. 이제 루퍼에게 프린터를 부착해서 메세지큐에 무슨 일이 일어나는지 감시하는 쪽을 살펴보죠.

class LooperThead extends Thread{
    Handler h1;
    @Override
    public void run(){
        Looper.prepare();
        h1 = new Handler(){
            public void handleMessage(Message m){
                Log.i("bs","test1");
            }
        };

        //루퍼에 감시리스너를 부착
        Looper.myLooper().setMessageLogging(new LogPrinter(Log.INFO, "bs"));
        Looper.loop();
    }
}

이렇게 루퍼에 setMessageLogging()메소드로 프린터를 부착하고 나면 send할 때마다 마찬가지로 문자열이 옵니다…..하아..
그나마 이 문자열은 좀 파싱이 가능한데 크게 네가지 정보를 담고 있는 문자열입니다.

  1. 해당 메세지의 처리상태(Dispatching 또는 Finished) 실은 문서를 보면 다양한 이벤트가 올 것처럼 써있는데, 거의 저 두 개만 옵니다.
  2. 핸들러의 해쉬값 – {9d9323efa} 같은 식으로 중괄호에 둘러쌓인 핸들러의 해쉬값을 얻을 수 있고 그 핸들러가 어떤 쓰레드 소속인지 알 수 있습니다.
  3. Runnable의 해쉬값 – 태스크메세지라면 Runnable의 해쉬값이 이어서 들어오고 데이터메세지라면 null이라는 문자열이 옵니다.
  4. what값 – 마지막으로 :으로 구분된 뒤에는 숫자로 what값이 옵니다.

그나마 dump보다는 덜 지저분해서 파서를 만들고 싶은 생각이 들기도 합니다. 하지만 이 방식은 현재 일어나고 있는 메세지의 처리를 순수하게 루퍼입장에서 보는 것입니다. 즉 큐에서 꺼내서 처리하는 과정을 보는 것이지 큐를 들여다보는건 아닙니다. 결국 dump()와 setMessageLogging()를 동시에 감시하고 이 두개용 문자열 파서를 작성해야만 메세지큐에서의 메세지유통 전체를 감시할 수 있습니다. 오픈소스를 잘 찾아보시면 덤프파서와 메세지로거파서를 구현한게 여럿있으니 생략하겠습니다 =.=

개인적으로 이 메세지큐를 감시해야하는 코딩을 하는거 자체가 메세지큐의 장점을 다 잃어버리는 구조라고 생각합니다. 블랙박스로 두고 투명하게 쓸 수 있는 아키텍쳐를 만드는 편이 바른 길인거 같습니다.

HandleThread

이해를 돕기 위해 쓸데없이 쓰레드로부터 루퍼를 만들었습니다만 OS레벨에서 이미 루퍼를 내장한 쓰레드를 만들어주는 클래스가 정의되어있습니다. 위에서 열심히 만들었던 LooperThread는 아래와 같이 간단히 대체됩니다.

HandlerThread ht = new HandlerThread("test");
ht.start();
new Handler(ht.getLooper()).post(new Runnable(){public void run(){
   //...
}});

훨씬 손쉽게 처리됩니다. 개인적으로 Thread를 이용해 만들 필요는 전혀 없다고 생각합니다. 커스텀기능이 필요한 경우도 핸들러쓰레드를 상속받아 만드는 편이 훨씬 편리합니다.

Activity.runOnUiThread

액티비티에는 Runnable을 받는 runOnUiThread() 라는 메소드가 있습니다. 잠깐 생각을 해보죠. 만약 현재 실행되고 있는 쓰레드가 메인쓰레드라면 그냥 Runnable을 실행해버리면 됩니다.
그게 아니라 다른 쓰레드라면 메인쓰레드에 있는 루퍼의 메세지큐에 태스크메세지로 넣어야죠. 이를 구현해보면 귀찮습니다.

void run(Runnable r){
   if(Thread.currentThread() == Looper.getMainLooper().getThread()) r.run();
   else new handler(Looper.getMainLooper()).post(r);
}

이를 이미 내장시킨 메소드가 바로 runOnUiThread()입니다. 훨씬 편리하죠.

this.runOnUiThread(new Runnable(){public void run(){
  //...
}});

사실 이 개념은 메인UI용 루퍼가 아니라도 모든 루퍼가 제공해야하는 기능이긴 합니다.

class LooperThread extends HandlerThread{
    public void post(Runnable run){
       if(Thread.currentThread() == getLooper().getThread()) r.run();
       else new handler(this.getLooper()).post(r);
    }
}

결론

루퍼와 핸들러, 메세지, 메세지큐에 대한 내용을 살펴봤습니다. 안드로이드 비동기모델의 가장 기초이기도 하고 이를 바탕으로 나머지 래핑클래스가 구성되므로 선수적인 지식이기도 합니다. 프로세서 경계를 넘거나 서비스와 상호작용하는 방법들도 여러가지 있지만 일단 가장 기초를 튼튼히 하는게 좋겠죠.