[Android]UI 스레드와 메시지 큐

H.Zoon·2025년 3월 31일
post-thumbnail

도입부

안드로이드 앱 개발에서 UI 스레드가 단일 스레드 모델을 사용하기 때문에 비동기 작업 처리가 필수적이다.
메시지 기반의 비동기 처리를 위해 안드로이드에서는 Looper와 Handler라는 핵심 컴포넌트를 제공한다.
이 글에서는 Looper와 Handler의 개념부터 사용 방법, 동작 흐름, 실전 활용 사례 및 비교와 주의사항을 정리해보려 한다.

안드로이드의 메시지 루프 개념

UI 스레드와 메시지 큐

안드로이드 애플리케이션은 메인(UI) 스레드를 통해 화면 갱신 및 사용자 이벤트를 처리한다.
단일 스레드 모델이기 때문에 긴 작업이 UI 스레드에서 실행되면 앱이 멈춘 것처럼 보이는 ANR(Application Not Responding) 현상이 발생한다.
이를 방지하기 위해 메시지 큐(MessageQueue)에 작업을 등록하고 순차적으로 처리하는 메시지 루프(Message Loop) 구조를 사용한다.

메시지 루프(Message Loop)

메시지 큐에 담긴 메시지 또는 Runnable 객체를 꺼내어 실행하는 반복 구조를 메시지 루프라고 한다.
Looper는 이 메시지 루프를 관리하여 대기 중인 작업을 하나씩 처리한다.
메인 스레드에는 시스템에서 자동으로 Looper가 준비되고 루프가 실행된다.

Looper란 무엇인가?

역할

Looper는 특정 스레드의 메시지 큐를 지속적으로 감시하면서 메시지나 Runnable 작업을 꺼내어 실행하는 클래스로 정의된다.
메인 스레드뿐만 아니라 백그라운드 스레드에서도 직접 Looper를 생성하여 메시지 루프를 운영할 수 있다.

사용 방법

백그라운드 스레드에서 Looper를 사용하려면 먼저 Looper.prepare()로 메시지 큐를 생성한다.
이후 Handler를 생성하거나 작업 등록을 마친 뒤 Looper.loop()를 호출하여 메시지 루프를 시작한다.
작업 처리가 끝나면 Looper.quit() 또는 Looper.quitSafely()로 루프를 종료할 수 있다.

Thread thread = new Thread(() -> {
    Looper.prepare();                      // 메시지 큐 준비  
    Handler handler = new Handler();       // 현재 스레드의 Looper와 연결된 Handler 생성  
    Looper.loop();                         // 메시지 루프 시작  
    // 루프 종료 시점 이후 코드 실행  
});
thread.start();

Handler란 무엇인가?

역할

Handler는 특정 Looper(스레드)의 메시지 큐에 Message 또는 Runnable 작업을 등록하는 도구로 동작한다.
이를 통해 스레드 간 안전한 통신과 UI 업데이트 예약이 가능하다.

주요 API

  • post(Runnable r): Runnable을 즉시 메시지 큐에 등록한다.
  • postDelayed(Runnable r, long delayMillis): 지정 시간 후 Runnable을 실행하도록 등록한다.
  • sendMessage(Message msg): Message 객체를 메시지 큐에 등록한다.
  • sendMessageDelayed(Message msg, long delayMillis): 지연 후 Message를 처리하도록 등록한다.
  • handleMessage(Message msg): Message가 도착했을 때 로직을 구현하는 콜백 메서드이다.

사용 예시

메인 스레드 UI 업데이트용 Handler 생성 예제이다.

val mainHandler = Handler(Looper.getMainLooper())
mainHandler.post {
    // UI 업데이트 작업 실행  
}

백그라운드 스레드에서 작업 후 UI 업데이트를 위해 메인 스레드 Handler를 활용한다.

Thread {
    // 백그라운드 연산 수행  
    mainHandler.post {
        // 백그라운드 결과를 UI에 반영  
    }
}.start()

Looper와 Handler 동작 흐름

  1. 작업 등록: Handler를 통해 Runnable 또는 Message를 메시지 큐에 등록한다.
  2. 메시지 큐 대기: Looper가 무한 루프를 돌며 큐에 쌓인 작업을 대기한다.
  3. 작업 실행: Looper가 큐에서 작업을 꺼내 Handler의 handleMessage() 또는 Runnable.run()을 호출한다.
  4. 반복 처리: 큐에 작업이 없어도 루프는 계속 대기하며, 새 작업이 들어오면 다시 실행한다.

Message와 Runnable의 차이

1. Message

  • Message는 안드로이드 프레임워크에서 제공하는 데이터 컨테이너이다.
  • what, arg1, arg2, obj 등의 필드에 정수나 객체를 담아 전송할 수 있다.
  • Handler.sendMessage(Message msg)로 메시지 큐에 등록한 뒤, HandlerhandleMessage(Message msg) 메서드에서 처리한다.
  • 재사용을 위해 내부적으로 객체 풀(Object Pool)을 사용하므로, 생성과 소멸 오버헤드를 줄인다.
val handler = object : Handler(Looper.getMainLooper()) {
    override fun handleMessage(msg: Message) {
        when (msg.what) {
            1 -> {
                // 메시지 처리 로직
            }
        }
    }
}

val msg = Message.obtain().apply {
    what = 1
    obj = "데이터"
}
handler.sendMessage(msg)

2. Runnable

  • Runnable은 자바 표준 인터페이스로, run() 메서드에 실행할 코드를 담는다.
  • Handler.post(Runnable r) 또는 postDelayed(Runnable r, delay)로 메시지 큐에 등록하면, 메시지 큐 내부에서 Runnable.run()이 호출된다.
  • 간단히 실행할 동작(블록)을 직접 전달할 때 사용한다.
val handler = Handler(Looper.getMainLooper())
handler.post {
    // UI 업데이트 또는 실행할 코드
}

3. 사용 시점 비교

구분용도
Message여러 종류의 메시지 구분, 데이터 전달, 재사용 오버헤드 절감
Runnable즉시 실행할 코드 블록 전달, 간단한 UI 작업 스케줄링
  • Message는 내부 필드를 통해 데이터와 타입을 함께 전달할 때 유용하다.
  • Runnable실행할 동작 자체를 전달할 때 간편하게 사용할 수 있다.

이처럼 HandlerMessage 객체 또는 Runnable 객체를 메시지 큐에 등록하여, 등록된 작업이 순차적으로 처리되도록 한다.

실전 활용 사례

  • 백그라운드 연산 후 UI 반영: 네트워크 호출이나 파일 I/O 작업 후 메인 스레드에서 결과를 처리한다.
  • 지연/반복 작업 스케줄링: postDelayed()sendMessageDelayed()를 활용해 특정 간격으로 작업을 반복 실행한다.
  • 커스텀 스레드 풀 연동: HandlerThread를 사용해 별도 스레드에서 메시지 루프를 운영하고 작업을 분산 처리한다.

Handler vs. 기타 대안 비교

방식사용 시점장점단점
Handler(Looper.getMainLooper())정밀 스케줄링, 레거시 코드지연/반복 제어 가능, 직접 메시지 큐 제어코드 복잡, 메모리 누수 위험
view.post특정 View와 연계된 간단 UI 업데이트별도 Handler 생성 불필요, 간편View detach 시 실행되지 않을 수 있음
CoroutineScope(Dispatchers.Main)최신 비동기 처리코드 가독성 우수, structured concurrency 지원코루틴 학습 필요

주의할 점 및 팁

  • Handler 사용 시 내부 클래스로 정의하면 외부 클래스(예: Activity)를 암묵적으로 참조하여 메모리 누수가 발생할 수 있다.
  • 이를 방지하려면 static inner 클래스와 WeakReference를 활용해 Handler를 정의한다.
  • 백그라운드 스레드에서 Looper를 사용할 때는 루프 종료 시점을 명확히 관리하여 스레드가 불필요하게 유지되지 않도록 한다.
  • Android Jetpack의 HandlerThread를 활용하면 Looper 준비와 스레드 관리를 간편하게 처리할 수 있다.

결론

Looper와 Handler는 안드로이드에서 메시지 기반 비동기 처리를 가능하게 하는 핵심 컴포넌트이다.
메인 스레드의 안정적 UI 처리와 백그라운드 스레드 간 통신을 위해 Looper와 Handler의 동작 원리를 정확히 이해하는 것이 중요하다.
실제 프로젝트에서 다양한 활용 사례와 주의사항을 참고하여 안전하고 효율적인 비동기 처리를 구현한다.

참고링크

Processes and Threads Android 개발자 가이드

0개의 댓글