brunch

You can make anything
by writing

C.S.Lewis

by 서준수 Jul 07. 2019

안드로이드 Handler 알고 쓰자

Message Queue & Looper

안드로이드 개발자라면 UI 작업은 별도의 스레드(이하 워커 스레드(worker thread))가 아닌 메인 스레드(main thread)에서 해야 한다는 이야기를 들어봤을 것이다. 만약 로직상 워커 스레드 내에서 UI 처리를 해야 한다면 해당 스레드와 메인 스레드를 이어주는 핸들러(handler)를 이용하고 있을 것이다.


왜 UI 작업은 Main Thread에서 해야 할까?

하나의 예를 들면 여타 비동기 작업과 마찬가지로 UI 작업을 비동기적으로 처리한다면 반드시 동기화 문제에 마주치게 된다. 안드로이드는 이러한 문제 발생을 예방하기 위해서 메인 스레드와 워커 스레드 사이에 핸들러를 두고 UI 작업은 모두 메인 스레드로 전달하도록 한 것이다.


예를 들어 다음과 같이 하나의 textView에 메인 스레드와 워커 스레드가 동시에 setText를 하려고 하면 문제가 된다. 적절한 갱신에 어려움이 생긴다.

어디로 가야 하오


이런 문제를 해결하기 위해서 워커 스레드는 핸들러를 사용한다. 핸들러의 역할을 간단히 말하면 메시지를 전달하는 것이다. 워커 스레드에서 핸들러는 메인 스레드로 메시지 전달한다. 그러면 메시지를 수신한 메인 스레드에서 적절한 작업을 처리하는 것이다.


메인 스레드만 믿으라구


Main Thread에서 무거운 작업은 하지 말자!

앞서 살펴본 것과 같이 안드로이드는 메인 스레드에서 UI 작업을 처리한다. 그렇기 때문에 시간이 오래 걸리는 작업을 메인 스레드에서 하게 된다면 UI 처리가 늦게 이뤄지게 된다. 이것은 반응성이 떨어진다는 의미이다. 반응성이 떨어지면 사용자는 답답함을 느끼게 될 것이다. 어쩌면 ANR 상태를 맞이하게 될지도 모른다.


Handler, Message Queue, Looper 알아보기

핸들러는 핸들러 객체를 만든 스레드와 해당 스레드의 Message Queue에 바인딩된다. Message Queue는 핸들러가 전달하는 message를 보관하는 FIFO(First In First Out) 방식의 큐이다.


다른 스레드에게 메시지를 전달하려면 수신 대상 스레드에서 생성한 핸들러의 post나 sendMessage 등의 함수를 사용해야 한다. 그래야 수신 대상 스레드의 Message Queue에 message가 저장되기 때문이다.


Message Queue에 저장된 message나 runnable은 Looper 차례로 꺼내서 핸들러로 전달한다. 핸들러가 힘들게 넣은 것을 다시 핸들러에게 전달하는 이유는 해당 message나 runnable을 처리하기 위해서이다.


다음은 Thread, Handler, Message Queue, Looper의 상호 동작을 도식화한 것이다.

예를 들어 Thread #1이 메인 스레드이고 Thread #2가 워커 스레드라고 가정하자. 이때 워커 스레드가 UI 처리를 위해서 메인 스레드에서 생성된 핸들러의 sendMessage()를 통해 메시지를 전달한다. 그러면 해당 메시지는 메인 스레드의 Message Queue에 저장된다. Looper는 차례대로 메시지를 꺼낸다. 워커 스레드가 전달했던 UI 처리를 위한 메시지가 꺼내지면 handleMessage()로 전달된다. handleMessage()는 실제 UI 작업을 수행한다. handleMessage()도 메인 스레드에서 생성된 핸들러에 의해 호출된 것이기 때문에 해당 UI 작업들은 메인 스레드에서 동작하는 것이다. 따라서 문제없이 UI 처리가 가능하다.



Handler 주요 함수

핸들러와 관련된 다양한 함수가 있지만 메시지 전달을 위한 대표적인 두 함수는 다음과 같다.


Handler.sendMessage(Message msg)

Message 객체를 message queue에 전달하는 함수


Handler.sendEmptyMessage(int what)

Message의 what 필드를 전달하는 함수



또 다른 주요 함수로는 post()가 있다. post()는 Message 객체가 아닌 Runnable 객체를 Message Queue에 전달한다.


Handler.post(new Runnable())

Runnable 객체를 message queue에 전달하는 함수


post를 통해서 전달된 Runnable 객체해당 핸들러가 연결된 스레드에서 실행된다. 따라서 UI 작업을 처리하기 위해서는 핸들러를 메인 스레드에서 생성하여 핸들러와 메인 스레드 연결되어 있어야 한다. (SendMessage()를 통해서 Message를 전달하는 것과 다를 게 없다. 어차피 대상 스레드의 Message Queue에 전달되는 것이기 때문이다.) 워커 스레드 내부에서 핸들러를 생성하면 결국 워커 스레드에서 UI 처리를 시도하는 것과 다를 바 없다.


Worker Thread에서 생성된 Handler를 통해서는 UI 처리 불가


위 코드를 보면 메인 스레드가 아닌 워커 스레드 내부에서 핸들러를 생성하였다. 따라서 해당 핸들러를 통해 post()로 전달된 Runnable 객체는 워커 스레드의 Message Queue에 저장된다. 그러면 워커 스레드의 Looper가 Runnable 객체를 꺼내서 run()을 실행한다. run()에서 시도하는 작업이 UI 처리이기 때문에 에러가 발생한다.


이 과정을 간단하게 그림으로 그리면 다음과 같다.

UI 처리 불가능한 구조


정상적으로 UI 작업을 처리하기 위해서는 다음과 같이 구현해야 한다. 이때 mMainHandler는 메인 스레드에서 생성한 것이다.


Main Thread에서 UI 작업을 하기 위한 Handler 사용법


워커 스레드 내부에서 메인 스레드의 핸들러를 통해서 Runnable 객체를 전달한다. 따라서 메인 스레드의 Message Queue에 Runnable 객체가 저장된다. 그러면 메인 스레드의 Looper가 Runnable 객체를 꺼내서 run()을 실행한다. 메인 스레드에서 처리되는 UI 작업이기 때문에 정상적으로 실행된다.

UI 처리 가능한 구조


Handler 예제 살펴보기


첨부파일은 핸들러를 사용한 UI 처리에 대해 알아보기 위한 간단한 예제이다. 실행을 하면 다음과 같은 화면이 나온다. 각 버튼을 누를 때마다 'Hello World!'가 적혀있는 textView의 문구를 변경하는 UI 작업을 한다. ErorrButton들은 워커 스레드에서 UI처리를 시도하도록 하여 다음과 같은 에러가 발생하여 앱이 종료된다.


Only the original thread that created a view hierarchy can touch its views.




Post_Button

앞서 post()에서 정상적으로 UI 작업을 처리하는 것과 동일한 코드이다.


Message_Button

post() 대신에 sendEmptyMessage()를 통해서 Message의 필드 중 하나인 what을 전달한다.

전달된 Message의 what은 다음과 같이 handleMessage()에서 처리한다.


Erorr_Button1

워커 스레드 내부에서 UI 처리를 시도하는 코드이다. 당연히 에러가 발생한다.


Erorr_Button2

앞서 post()에서 살펴본 비정상적 UI 처리 코드이다. 워커 스레드 내에서 핸들러를 생성한다. 해당 핸들러의 post()를 통해서 실행된 run()에서 UI 처리를 시도하기 때문에 결론적으로 워커 스레드에서 UI 처리를 하려는 것과 같다. 에러가 발생한다.


Erorr_Button3

워커 스레드 내에서 핸들러를 생성한다. 해당 핸들러의 sendEmptyMessage()를 통해서 what을 전달한다. 워커 스레드의 Message Queue로 전달될 것이고 handleMessage()는 UI 처리를 시도한다. 결론적으로 워커 스레드에서 UI 처리를 하려는 것과 같다. 에러가 발생한다.


ref.)

realm academy : https://academy.realm.io/kr/posts/android-thread-looper-handler/

금광캐는광부님 블로그 : https://itmining.tistory.com/5

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari