[Android] Looper & Handler 기초 개념

H43RO·2021년 9월 20일
18

Android 와 친해지기

목록 보기
10/26
post-thumbnail

안드로이드의 UI 동작

안드로이드를 개발해본 사람들이라면 다들 알다시피, 안드로이드의 UI 처리는 싱글 쓰레드 모델로 동작한다. 즉, 메인 쓰레드가 아닌 다른 쓰레드에서 UI 를 업데이트하는 등의 행위를 하면 안된다. 따라서 메인 쓰레드를 UI 쓰레드라고 부르기도 한다.

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

이유는 간단하면서도 당연하다. 멀티 쓰레드 환경이라고 가정했을 때, 여러 쓰레드에서 TextView 의 텍스트를 변경하는 상황이 발생하면 어떤 결과가 나타날 지 미지수이기 때문이다. 따라서 동작의 무결성을 보장하기 위해 타 쓰레드에서는 UI 를 건드릴 수 없고, 오로지 메인 쓰레드에서만 UI 관련 동작을 할 수 있게끔 하는 것이다.


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

1. 메인 쓰레드 (UI 스레드) 를 블로킹해서는 안 됨

→ 메인 쓰레드를 블로킹한다는 뜻은, 사용자에게 보여지는 UI 동작을 멈춘다는 뜻이다. 메인 쓰레드가 블로킹되어 UI 동작이 멈추게 되면, 이는 표면적으로 앱 퍼포먼스 저하를 유발하게 되며 사용자 경험상 악영향을 끼친다. 따라서, 시간이 오래 걸리는 동작을 수행하는 등 메인 쓰레드를 블로킹해선 안 된다.

2. UI 관련 동작은 오로지 메인 쓰레드에서만 접근해야 함

→ 이유는 위에서 설명했다. UI 동작의 무결성을 보장하기 위함이다.


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

즉, 무거운 동작들은 메인 쓰레드가 아닌 다른 쓰레드를 생성하여 수행해야 한다. 그런데, 어차피 쓰레드를 별도로 생성하여 시간이 오래 걸리는 동작들을 한다고 해도, 그 동작의 '결과' 는 보통 UI 를 업데이트하는 데에 사용된다.

예를 들어 쓰레드를 새로 생성하여, 고양이 사진을 제공해주는 서버의 API 를 호출하여 고양이 사진을 받은 뒤 ImageView 에 보여주는 동작을 한다고 하자. 그런데 아까 별도의 쓰레드에선 UI 관련 동작을 해서는 안 된다고 했다. 그럼 결과로 받은 고양이 사진을 어떻게 사용자에게 보여줄 수 있을까?

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

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


Looper

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

각 쓰레드의 Looper 내부에는 MessageQueue 라는 것이 존재하는데, 여기에는 해당 쓰레드가 처리해야 할 동작들이 '메세지' 라는 형태로 하나씩 쌓이게 된다. (큐 라는 이름에서 알 수 있듯 당연히 FIFO 방식이다)

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

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

물론 MessageQueue 가 비어있을 땐 아무 동작을 수행하지 않는다. 무한 루프를 돌면서 큐에 쌓여있는 메세지를 Handler 에 전달해주는 동작 특성 상 Looper 라는 이름이 제격이다.

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

메세지는 또 뭔데 아 ㅋㅋ

Message'하나의 작은 작업 단위' 라고 생각하면 편하다. MessageQueue 에는 이러한 작은 작업 단위를 하나씩 적재해두고, Looper 가 이를 차례대로 처리하는 것이다.

Message 객체는 내용물이 두 가지 종류로 이루어진다. Runnable 객체로 이루어져있을 수도 있고, 일반적인 경우 Message 객체로 이루어져있을 수도 있다. Looper 객체가 메세지 큐에서 메세지를 하나 딱 까봤을 때, Runnable 객체가 담겨져있으면 Handler 에 메세지를 전달하지 않고 run() 을 수행하여 해당 Runnble 작업을 바로 시작하고, Runnable 객체가 없을 경우 Message 객체 내부에 명시돼있는 HandlerhandleMessage() 를 수행하여 처리한다. Handler 개념을 살펴보며 더 이해해보자.


Handler

Handler 는 명칭에서 알 수 있듯 뭔가를 다루는 녀석인데, 특정 메세지를 Looper 의 MessageQueue 에 넣거나, Looper 가 MessageQueue 에서 특정 메세지를 꺼내어 전달하면 이를 처리하는 기능을 수행한다. 중간 다리 역할을 수행한다.


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

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

  • sendMessage() 메소드를 통해 메세지 큐에 Message 객체를 적재할 수 있다.
  • post 로 시작하는 메소드들을 통해 Runnable 객체를 직접 적재할 수 있다.

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

Looper 가 메세지 큐에서 메세지 하나를 딱 꺼냈을 때,

  • Runnable 객체가 담겨있다면
    → 해당 Runnablerun() 메소드를 호출하여 작업을 실행할 수 있다.

  • Message 객체가 담겨있다면
    → 해당 메세지 내부의 Handler 가 갖고 있는 handleMessage() 메소드를 호출함으로써 해당 Handler 가 메세지를 전달받을 수 있다.


전반적인 동작 흐름

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

  2. 해당 쓰레드의 LooperMessageQueue 에서 loop() 를 통해, 메세지를 하나씩 Handler 에 전달

  3. Handler 에서 handleMessage() 를 통해 메세지 처리

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


개념 끼워맞춰보기

위와 같은 동작 흐름을 통해 HandlerLooper 를 통해 쓰레드간 통신이 가능하다는 사실을 알게 되었다. 이 개념을 그대로 '메인 쓰레드 ↔ 다른 쓰레드' 상황으로 옮겨보자.

당연하게도, 다른 쓰레드에서 메인 쓰레드에 종속되는 Handler 를 활용하게 되면 다른 쓰레드의 특정 결과를 메인 쓰레드에서 받아볼 수 있게 되는 것이다.

즉, 메인 쓰레드가 갖고있는 Looper 의 MessageQueue다른 쓰레드의 결과를 활용한 작업들을 담아두면, 메인 쓰레드의 Handler 가 이를 차례대로 수행하게 되는 것이다.

사용해보기

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

예제 코드들은 Kotlin 을 기준으로 합니다!

HandlerLooper 생성은 다음과 같이 구현하게 된다.

var handler: Handler? = null
val thread = Thread {  // Runnable 익명 객체 구현
    Looper.prepare()
    handler = Handler()
    Looper.loop()
}
thread.start()

Looper.prepare() 를 통해 해당 쓰레드에 종속되는 Looper 와 MessageQueue 를 준비해주고, Handler 를 생성해준다. 이 순간 HandlerLooper 가 연결된다. 그리고 해당 쓰레드가 익명으로 구현한 Runnable 객체의 run() 메소드 마지막에서 Looper.loop() 를 호출해줌으로써 Message 전달을 기다리는 동작을 시작한다.

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

안드로이드는 연결할 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())
}
thread.start()

해당 포스팅에선 HandlerLooper 의 개념에 대해서만 간략히 다뤄보았다. 안드로이드에서 멀티 쓰레딩 환경을 완벽히 구현하기 위해서는, HandlerLooper 에 대해서 빠삭하게 알고 있어야 한다.

이러한 기본 개념을 바탕으로, 다음 포스팅에선 이들의 활용 방법에 대해 예제 코드를 기반으로 설명해보려 한다.

profile
어려울수록 기본에 미치고 열광하라

5개의 댓글

comment-user-thumbnail
2021년 10월 19일

글 너무 재밌게 잘 보고 있습니다!
한 가지 질문이 있는데 메시지 큐에서 runnable객체 일 경우 루퍼를 통해 핸들러에게 runnable객체를 전달하지 않고 바로 run()하는게 맞나요? 다른 곳에서는 둘 다(message,runnable) 핸들러에게 전달한다고 되어있어서 혼란스럽습니다ㅜㅜ

1개의 답글
comment-user-thumbnail
2023년 1월 2일

선생님 너무 유익합니다... 서비스 컴포넌트 배우다가 Looper와 Handler 개념이 나와서 검색해봤는데 바로 이해했습니다..

답글 달기

완전 도움이 되었습니다...! 루퍼와 핸들러에 대해 제대로 알게 된 것 같습니다.
질문하고 싶은 것이 있는데, 제가 이해한 것이 맞다면, 쓰레드를 생성하여 핸들러를 만들어주고 Looper.getMainLooper()로 연결해주면, 이전 코드인 Looper.loop()를 해주지 않아도 Message를 받으면 처리해주는 것이죠?
그리고, 만약 두 개 이상의 쓰레드를 생성하여 핸들러를 만들어서 각각 Looper.getMainLooper()로 메인스레드와 연결해준다면, 여러 쓰레드의 Message들을 순서대로 받아서 동시에 처리하는 효과를 받을 수 있는 거겠죠?
이상 궁금한 점이고 대답해주시면 감사하겠습니다. 좋은 글 감사합니다!

답글 달기