Android

안드로이드 [Kotlin] - 핸들러와 루퍼(Handler & Looper)

🤖 Play with Android 🤖 2022. 12. 25. 21:34
728x90


안드로이드에서 Thread

안드로이드에서의 메인 스레드(Main Thread, UI Thread)란?

  • 앱이 실행됐을 때 시스템에서는 메인(Main)이라고 불리는 스레드를 생성한다.
  • 메인스레드는 안드로이드 이벤트 생성 및 처리를 담당할 뿐 아니라 안드로이드에서 발생되는 여러 이벤트를 그와 관련된 위젯으로 연동시키는 중요한 역할을 수행한다.
  • 또한 안드로이드에서 제공하는 다양한 뷰와 위젯을 표현하는 역할과 사용자로 하여금 그것을 사용할 수 있게 해 주어 UI 스레드라고도 불린다.

 

앱에 메인 스레드만 존재한다면?

  • 앱이 단일 스레드 즉 메인 스레드만 존재하는 모델에서는 형편없는 성능을 낳게 된다.
  • 메인 스레드에서 모든 작업을 처리한다면 네트워크 처리 및 데이터베이스 쿼리와 같이 오래 걸리는 작업을 하는 동안 UI와 관련된 작업을 처리하지 못하게 된다.
  • 이것이 문제가 되는 이유는 오랜 시간 UI 관련된 작업을 처리하지 못하면 ANR(Application Not Responding) 에러가 발생하고 그 즉시 앱이 정지되기 때문이다.

 

하지만 메인 스레드는 싱글 스레드 모델로 동작한다.

  • 만약 UI가 멀티스레드에 의해 변경되는 환경이라면 여러 스레드에서 UI를 변경하려고 시도하는 상황이 발생할 수 있다.
  • 이는 어떤 결과가 나타날 지 모르게 만든다.
  • 즉 동작의 무결성을 보장하지 못한다.
  • 따라서 메인 스레드는 싱글 스레드 모델로 동작하여 오직 메인 스레드에서만 UI 관련 동작을 할 수 있게끔 한다.

 

스레드 간 통신의 필요성

여러 스레드의 결과는 보통 UI를 업데이트하는 데 사용된다.

 

이럴 때는 스레드 간 통신이 필요하다. 안드로이드에서는 스레드 간의 통신을 위해 Looper, Handler라는 도구를 제공한다.

 

핸들러(Handler)를 통한 스레드간 통신

동작 순서

  1. “워커 스레드”가 UI처리를 위해 ”메인 스레드의 Handler”sendMessage() 메서드로 메시지를 전달한다.
  2. 해당 메세지는 “메인스레드의 Message Queue”에 저장된다.
  3. 메인 스레드의 Looper”가 차례대로 메시지를 꺼내 “메인 스레드의 Handler”의 handleMessage() 로 전달된다.
  4. “메인스레드의 Handler”가 UI 작업을 수행한다.

 

 

핸들러(Handler)

안드로이드에서 사용할 수 있는 스레드 간 통신 중 대표적인 방법 중 하나가 핸들러를 통해 메시지(Message)를 전달하는 방법이다.

핸들러를 생상하면 호출한 스레드의 메세지 큐(Message Queue)와 루퍼(Looper)에 자동으로 연결된다.

메서드 명 반환 타입 내용 설명
obtainMessage() Message 핸들러 자신으로 지정된 메세지 객체를 리턴
sendMessage(msg: Message) boolean 메세지 큐에 Message 객체 전달
post(runnable: Runnable) void 메세지 큐에 Runnable 객체 전달
sendEmptyMessage(what: Int) boolean 간단하게 what 값을 통해서 메시지를 보낼 때 사용
removeMessages(what: Int) void 전달한 what 코드 메세지를 메세지 큐에서 삭제
handleMessage(msg: Message) void 루퍼를 통해 메제지 큐에서 꺼낸 Message 처리
handler = object : Handler() {
    override fun handleMessage(msg: Message) {
        super.handleMessage(msg)

        if (msg.what === START_CODE) {
            thread.start()
        } else if (msg.what === PROGRESS_CODE) {
            textView.text = "Count : ${msg.arg1}"
        }
    }
}

 

 

메세지(Message)

스레드 통신에서 핸들러에 데이터를 보내기 위한 클래스로 데이터를 Message에 담아서 핸들러로 보내면 해당 객체는 핸들러를 통해 MessageQueue에 쌓이게 된다.

변수 내용 설명
what 메세지 종류를 식별하기 위한 메세지 코드
arg1 메세지를 통해 전달되는 첫 번째 정수 값
arg2 메세지를 통해 전달되는 두 번째 정수 값
obj 메세지를 통해 전달되는 객체
val message = Message()
message.what = PROGRESS_CODE
message.arg1 = count

handler.sendMessage(message)

 

 

메시지 큐(Message Queue)

단방향 연결 리스트로 Message 객체를 Queue 형태로 관리하는 자료구조이다.

 

호출 순서

sendMessage() -> handleMessage()

 

 

루퍼(Looper)

루퍼는 스레드당 하나씩만 가질 수 있다.

루퍼는 메세지 큐가 비어 있는 동안 아무 행동도 하지 않고, 메서드가 들어오면 해당 메시지를 꺼내 작업을 수행한다.

메인 스레드가 아닌 스레드는 기본적으로 생성될 때 루퍼를 가지지 않고 Looper.prepare() 메서드를 호출해야지 루퍼가 생성된다.

루퍼는 무한히 실행되는 메시지 루프를 통해 큐에 메시지가 들어오는지 감시하며 들어온 메세제를 처리할 핸들러를 찾아서 handleMessage()를 호출한다.

 

메서드 명 반환 타입 내용 설명
prepare() void 루퍼를 생성한다.
loop() void 무한히 루프를 돌며 메세지 큐에 쌓인 Message 객체나 Runnable 객체를 핸들러에게 전달한다.
quit() void 루프를 종료한다.

 

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

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

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

 

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

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

  • Runnable 객체가 담겨있다면
    → 해당 Runnable의 run() 메소드를 호출하여 작업을 실행할 수 있다.
  • Message 객체가 담겨있다면
    → 해당 메시지 내부의 Handler가 갖고 있는 handleMessage() 메소드를 호출함으로써 해당 Handler가 메시지를 전달받을 수 있다.

 

 

이때 Message를 보내는 것과 Runnable을 보내는 것은 어떤 차이가 있는 것일까?

 

메시지(Message)와 핸들러(Handler)를 사용하여 스레드 통신을 수행하는 주 목적은, "핸들러를 통해 데이터를 전달하여, 전달된 데이터 처리를 위해 작성된 대상 스레드의 코드가 실행되도록 만드는 것"이다. 이를 위해, 메세지 객체(object, arg1, arg2)에 값을 채워 수신 스레드의 핸들러에 보내고, 수신 측 스레드는 handleMessage()메서드를 오버라이드하여 수신된 메세지 객체를 처리하기 위해 작성된 코드를 실행하는 것이다.

 

그런데 메세지(Message)객체를 사용하는 방법에는 조금 번거로운 절차가 필요하다. 메시지에 저장된 데이터 종류를 식별하기 위해 값을 상수로 지정해야 하고, handleMessage()에서는 상수값에 따른 처리 코드를 조건문으로 작성해야 한다. 그리고 메세지를 보내는 측도 전달할 데이터의 종류에 따라 별도의 메시지 객체를 구성하고 값을 채워 보내야 한다. 이는 오직 대상 스레드에 작성된 코드를 실행하기 위해서다.

 

이때 만약 이러한 번거로운 과정을 거치지 않고 "실행코드"를 바로 보내면 어떨까? 즉 핸들러에 실행 코드가 담긴 객체를 보내고, 대상 스레드에서는 수신된 객체 코드를 바로 실행하게 만드는 것이다.

 

이때 이 "실행코드"가 담긴 객체가 Runnable 객체이다.

 

 

코드 예시

상황 1) 메인 스레드에서 코드 실행

루퍼만 인자로 받는 핸들러

// 루퍼가 있는 핸들러
val mainHandler = Handler(Looper.getMainLooper())

 

루퍼와 핸들러 콜백을 인자로 받는 핸들러

// 루퍼와 콜백이 있는 핸들러
val mainHandler = Handler(Looper.getMainLooper(), Handler.Callback {
    // 코드 로직을 이곳에 구현
    true
})

 

상황 2) 백그라운드 스레드(워커 스레드)에서 코드 실행

루퍼만 인자로 받는 핸들러

// 루퍼가 있는 백그라운드 스레드를 만든다.
val handlerThread = HandlerThread("HandlerThread")
handlerThread.start()


// 백그라운드 스레드에서 작업을 수행할 핸들러를 만든다.
val backgroundHandler = Handler(handlerThread.looper)

 

루퍼와 핸들러 콜백을 인자로 받는 핸들러

// 루퍼가 있는 백그라운드 스레드를 만든다.
val handlerThread = HandlerThread("HandlerThread")
handlerThread.start()


// 백그라운드 스레드에서 작업을 수행할 핸들러를 만든다.
val backgroundHandler = Handler(handlerThread.looper, Handler.Callback {
    // 코드 로직을 여기에 작성한다.
    true
})

 

이 때, "사용 후 스레드를 해제"하는 것을 꼭 기억하자!

// 사용한 스레드를 해제
handlerThread.quit()

 

상황 3) 백그라운드 스레드에서 코드를 실행하고 메인 스레드에서 UI를 업데이트

// 메인 스레드에서 동작할 핸들러 생성
val mainHandler = Handler(Looper.getMainLooper())

// 루퍼가 있는 백그라운드 스레드 생성
val handlerThread = HandlerThread("HandlerThread")
handlerThread.start()

// 백그라운드 스레드에서 동작할 핸들러 생성
val backgroundHandler = Handler(handlerThread.looper, Handler.Callback {
    // 이곳에 코드 로직 구현

    // 메인 스레드에서 UI를 업데이트 한다.
    mainHandler.post {
        
    }
    true
})