안드로이드 Looper & Handler 기초 개념

최윤성·2022년 3월 3일
1

안드로이드의 UI 동작

안드로이드를 개발해본 사람들이라면 다들 알다시피,

안드로이드의 UI 처리는 싱글 쓰레드 모델로 동작한다.

즉, 메인 쓰레드가 아닌 다른 쓰레드에서 UI 를 업데이트하는 등의 행위를 하면 안된다.

따라서 메인 쓰레드를 UI 쓰레드라고 부르기도 한다.

왜 UI 는 싱글 쓰레드 모델로 동작할까?

이유는 간단하면서도 당연하다. 멀티 쓰레드 환경이라고 가정했을 때,

여러 쓰레드에서 TextView 의 텍스트를 변경하는 상황이 발생하면 어떤 결과가 나타날 지

예측하기 힘들기 때문이다.

즉, UI 동작의 무결성을 확보하기 위함이다.

이러한 싱글 쓰레드 모델에서 지켜야할 포인트들

  1. `메인 쓰레드 (UI 스레드) 를 블로킹해서는 안 됨
    → 메인 쓰레드를 블로킹한다는 뜻은, 사용자에게 보여지는 UI 동작을 멈춘다는 뜻이다. 메인 쓰레드가 블로킹되어 UI 동작이 멈추게 되면, 사용자는 멈춘 UI만 멀뚱멀뚱 쳐다볼것이다.
    따라서, 시간이 오래 걸리는 동작을 수행하는 등 메인 쓰레드를 블로킹해선 안 된다.

2. UI 관련 동작은 오로지 메인 쓰레드에서만 접근해야 함
→ 이유는 위에서 설명했다. UI 동작의 무결성을 보장하기 위함이다.

시간이 오래 걸리는 무거운 동작들은 따로 돌리자

즉, 무거운 동작들은 메인 쓰레드가 아닌 다른 쓰레드를 생성하여 수행해야 한다.

그런데, 어차피 쓰레드를 별도로 생성하여 시간이 오래 걸리는 동작들을 한다고 해도,

결국 그 동작의 '결과' 는 보통 UI 를 업데이트하는 데에 사용되기 때문에

결코 메모리 낭비가 되지 않는다.

예를 들어 쓰레드를 새로 생성하여, 귀여운 뽀로로의 사진을 제공해주는 서버의 API 를 호출하여 고양이 사진을 받은 뒤 ImageView 에 보여주는 동작을 한다고 하자.

그런데 메인 스레드에서 모두 처리하게 되면 UI스레드를 블로킹하게된다.

또, 아까 별도의 쓰레드에선 UI 관련 동작을 해서는 안 된다고 했다.

그럼 결과로 받은 고양이 사진을 어떻게 사용자에게 보여줄 수 있을까?

가장 먼저 떠오르는 방법은, 다른 쓰레드에서 메인 쓰레드로 결과를 전송하는 방식이다. 즉, 쓰레드간의 통신을 구현하는 것이다.

안드로이드에선 쓰레드간의 통신을 위해, LooperHandler 라는 장치를 제공해준다. 이 녀석들을 활용하여 효율적으로 멀티 쓰레딩 환경을 구축할 수 있다. 이번 포스팅에선 이 녀석들을 알아보고자 한다.

Looper

하나의 쓰레드에는 오직 하나의 Looper 를 가지며, Looper 는 오직 하나의 쓰레드를 담당한다. 안드로이드에선 기본적으로 MainActivity 가 실행됨과 동시에 자동으로 메인 쓰레드의 Looper 가 돌기 시작한다.

각 쓰레드의 Looper 내부에는 MessageQueue 라는 것이 존재하는데,

여기에는 해당 쓰레드가 처리해야 할 동작들이 '메세지' 라는 형태로 하나씩 쌓이게 된다.

마치 회사에서 내가 맡은 일이 수북히 서류로 쌓이는 느낌으로 말이다.

Looper 는 궁극적으로, MessageQueue 에 들어오는 메세지들을 하나씩 꺼내어 이를 적절한 Handler 로 전달하는 역할을 한다.

기본적으로 Looper 는 자신이 어떤 Handler 에 메세지를 전달해야 하는지에 대한 참조를 갖고 있다. (기본값 : 메인 쓰레드의 Handler)

물론 Looper도 바보는 아니다.

MessageQueue 가 비어있을 땐 아무 동작을 수행하지 않는다.

(무한 루프를 돌면서 큐에 쌓여있는 메세지를 Handler 에 전달해주는 동작 특성 상 Looper 라는 이름이 딱 맞는 옷을 입은것 같다.)

특히, 메인 쓰레드의 Looper 는 보통 UI 작업을 위한 메세지를 처리하게 된다.

메시지(Message)란?
Message 는 '하나의 작은 작업 단위' 라고 생각하면 편하다. MessageQueue 에는 이러한 작은 작업 단위를 하나씩 쌓아두고, Looper 가 이를 차례대로 처리하는 것이다.

Message 객체는 내용물이 두 가지 종류로 이루어진다.

  • Runnable 객체로 이루어져있을 수도 있다.

  • 일반적인 경우 Message 객체로 이루어져있을 수도 있다.

Looper 객체가 메세지 큐에서 메세지를 하나를 딱 받았을 때,

- Runnable 객체가 담겨져있을경우

Handler 에 메세지를 전달하지 않고 run() 을 수행하여 해당 Runnble 작업을 바로 시작한다.

- Runnable 객체가 없을 경우

Message 객체 내부에 명시돼있는 Handler 의 handleMessage() 를 수행하여 처리한다.

Handler

Handler 는 명칭에서 알 수 있듯 뭔가를 다루는 녀석인데, 두가지 역할을 하게된다.

  • 특정 메세지를 Looper 의 MessageQueue 에 넣는 역할
  • Looper 가 MessageQueue 에서 특정 메세지를 꺼내어 전달 하는역할

즉, 중간 다리 역할이라 볼 수있는것이다.

Looper 로 메세지를 전달하는 경우

Message 객체를 생성하여 이를 전달하는 방식으로 구현한다.

sendMessage() 메소드를 통해 메세지 큐에 Message 객체를 적재할 수 있다.

post 로 시작하는 메소드들을 통해 Runnable 객체를 직접 적재할 수 있다.

Looper 로부터 메세지를 전달받는 경우

- Runnable 객체가 담겨져있을경우

Handler 에 메세지를 전달하지 않고 run() 을 수행하여 해당 Runnble 작업을 바로 시작한다.

- Runnable 객체가 없을 경우

Message 객체 내부에 명시돼있는 Handler 의 handleMessage() 를 수행하여 처리한다.

전반적인 동작 흐름

  • 다른 쓰레드에서 특정 쓰레드 HandlersendMessage() 를 활용하여 메인 쓰레드 Looper 의 MessageQueue 에 메세지를 전달함

  • 해당 쓰레드의 Looper 는 MessageQueue 에서 loop() 를 통해, 메세지를 하나씩 Handler 에 전달함

  • Handler 에서 handleMessage() 를 통해 메세지 처리함

Handler는 바보다?

MessageQueue 와, MessageQueue 안의 메세지들을 자신에게 전달해주는 Looper 에 의존적인 녀석임을 알 수 있다. Looper 가 없다면 아무것도 못하는 녀석이다.

사용해보기 in kotlin

자, 그럼 Handler 를 생성해보자. HandlerLooperMessageQueue 가 있어야 하는 상당히 의존적인 녀석이기 때문에, 무조건 Looper 가 필요하다.

var handler: Handler? = null
val thread = Thread {  // Runnable 익명 객체 구현

    Looper.prepare()   // ---ㅣ
    handler = Handler() // --ㅣ 1.
    
    Looper.loop() // --ㅣ 2.
}
thread.start()

//하지만, 이렇게 Looper 를 생성하면 Handler 가 암시적으로 Looper 를 선택하게 되는
데, 이 과정에서 특정 작업이 손실되거나 충돌하는 등의 버그가 발생할 수 있어 해당 방식은
deprecated 되었다.

위의 코드는 총 2개의 단계로 나뉠 수 있는데,

  1. Looper.prepare() 를 통해 해당 쓰레드에 종속되는 LooperessageQueue 를 준비해주고, Handler 를 생성해준다. (이 순간 HandlerLooper 가 연결된다.)

  2. 해당 쓰레드가 익명으로 구현한 Runnable 객체의 run() 메소드 마지막에서 Looper.loop() 를 호출해줌으로써 Message 전달을 기다리는 동작을 시작한다.

또한, 안드로이드는 연결할 Looper 를 명시하여 Handler 를 생성하는 방법을 권장한다.

보통 메인 쓰레드와의 통신을 필요로 하기 때문에, 이번 포스팅에선 메인 쓰레드와 외부 쓰레드 의 통신을 다뤄보겠다.

Main Looper 명시하여 Handler 생성하기

안드로이드에선 별도 쓰레드의 결과를 보통 메인 쓰레드에서 처리하기 때문에,

아래와 같이 메인 쓰레드가 갖고 있는 Looper 를 명시하여 Handler 를 생성하면 된다.

이렇게 구현하면 메인 쓰레드의 Message Queue 에 메세지가 쌓이게 되고, 이를 메인 쓰레드의 Looper 가 하나씩 꺼내보게 된다. 또한 해당 Handler 는 메인 쓰레드의 Looper 를 명시하여 생성되었기 때문에, UI 관련 작업이 가능하다.


var handler: Handler? = null
val thread = Thread {  // Runnable 익명 객체 구현
    handler = Handler(Looper.getMainLooper())
    
    // handler 안에 Looper를 명시적으로 넣어주기 때문에 Handler 가 
    암시적으로 Looper 를 선택하게 돼서 일어나는 일이 해결이 된다.
    
}
thread.start()

해당 포스팅에선 Handler 와 Looper 의 개념에 대해서만 간략히 다뤄보았다.

안드로이드에서 멀티 쓰레딩 환경을 완벽히 구현하기 위해서는,

Handler 와 Looper 에 대해서 자세하게 알고 있어야 한다.

profile
웹과 앱을 사랑하는 남자

0개의 댓글