[Android, Kotlin] 멀티 스레드 간 통신을 위한 Handler, Looper
Android

[Android, Kotlin] 멀티 스레드 간 통신을 위한 Handler, Looper

안드로이드는 기본적으로 Main Thread (or UI Thread) 하나만으로 구성되는 Single Thread Model로써 동작한다.

Single Thread Model의 2가지 원칙은 다음과 같다.

 

1. Main Thread를 Block 하지 말 것

2. Android UI ToolKit (TextView, ImageView, etc..)은 Main Thread에서만 접근할 수 있도록 할 것

 

이러한 Single Thread Model을 추구하는 이유를 간단하게 보고 넘어가자.

구글링을 통해 Handler, Looper, Thread를 검색하면 나오는 아주 좋은 예시가 있다.

 

 

다음과 같은 멀티 스레드 환경에서 Main Thread와 Worker Thread가 동시에 textView의 text를 변경한다면 실제로 textView에 들어가야 하는 text는 무엇이 될 지 모르게 된다. 이러한 혼란 및 충돌을 방지하고자 UI 관련 작업은 오직 Main Thread에서만 수행하게 되어있다.

 

그러나 결국 Worker Thread를 생성하여 작업을 하는 궁극적인 이유는 장시간이 소요되는 작업을 Worker Thread에서 수행 후 결과를 기반으로 UI에 변화를 주기 위함이다.

허나, Main Thread만 UI 변경의 권한을 독차지한다면 굳이 멀티 스레딩을 할 필요가 있을까?

 

똑똑한 Android는 호락호락하게 이런 비효율적인 방식을 두지 않는다.

Thread에 Handler와 Looper를 두어 스레드 간 Runnable, Message 객체를 주고받을 수 있게 하며, 이를 통해 Worker Thread에서도 Main Thread의 Handler를 통해 Main Thread에서 UI 작업을 수행 할 수 있게 한다.

 

Handler와 Looper, Runnable과 Message에 대한 설명을 하기 전이라 뜬금없는 소리일 수도 있다. 

본 단어들에 대한 설명을 하고 마지막에 위 문장을 다시 쓰겠다. 지금은 패쓰

 

 

(0) Thread에서 Handler와 Looper의 상호작용

Thread 간 주고 받을 수 있는 객체는 MessageRunnable이 있다.

 

Message는 다음과 같은 구조를 가진다.

public final class Message implements Parcelable {
    @NonNull
    public static final Creator<Message> CREATOR = null;
    public int arg1;
    public int arg2;
    public Object obj;
    public Messenger replyTo;
    public int sendingUid = -1;
    public int what;

말 그대로 간단한 정수인 arg , 객체인 Object를 다른 Thread에 보낼 때 사용하는 형태이다.

 

Runnable은 이름 그대로 '실행 가능한' 코드 블럭을 다른 Thread에 보낼 때 사용하는 형태이다.

해당 Runnable 객체를 받은 Thread는 해당 코드 블럭을 실행해준다.

 

 

 

이제 Thread 간 무엇을 주고받을 수 있는지에 대해서는 알게 되었으니, Thread 내에서 Handler와 Looper가 어떤 역할을 하는지 그림을 통해 살펴본다.

 

 

1. A Thread와 B Thread가 있다고 가정하자.

2. A Thread가 B Thread에게 Message 또는 Runnable 객체를 보내고 싶다.

3. A Thread는 B Thread가 가진 Handler의 sendMessage 또는 post 메소드를 통해 Message, Runnable을 보낸다.

4. B Thread의 Handler는 전달받은 메세지를 B Thread의 Looper가 가진 Message Queue에 넣는다.

5. B Thread의 Looper는 Message Queue에 담긴 작업을 순차적으로 Handler에 보내 작업 수행을 요구한다.

6. B Thread의 Handler는 Looper에게 받은 작업을 handleMessage 메소드 등을 통해 수행한다.

 

위와 같은 과정을 통해 Thread 내에서 Handler와 Looper는 Thread간 통신을 가능하게 한다.

 

(1) Handler

Handler는 말 그대로 손을 쓰는 사람이다. Thread의 손이 되어주는 역할을 한다.

 

 

1. Thread 자신의 Handler는 받아온 작업을 수행할 수 있게 해주며,

class MyHandler : Handler() {
    override fun handleMessage(msg: Message) {
        super.handleMessage(msg)
        // 다른 Thread에서 받아온 message 처리 구문 작성
    }
}

 

2.  메세지를 보내고자 하는 다른 Thread의 Handler를 통해 메세지를 보낼 수 있게 해준다.

// handler 생성
val handler = Handler()

handler.post {
    // 해당 handler와 연결된 thread에 전달할 runnable 객체 내부 code block 작성
}

// 해당 handler와 연결된 thread에 message 객체 전달
handler.sendMessage(message)

 

 

Handler와 연결되는 Thread는 생성한 위치에 따라 결정된다.

Main Thread에서 생성한 Handler는 Main Thread의 Handler이다.

위 handler의 경우 별도의 Worker Thread 내부가 아닌 Main Thread에서 handler를 생성하였으므로 Main Thread의 handler가 된다.

 

 

해당 메세지에 대한 작업 처리는 핸들러와 연결된 Thread에서 처리된다.

즉, 위 handler는 Main Thread와 연결되어 있으므로, 전달된 객체는 Main Thread에서 처리한다.

이러한 원리를 통해, Worker Thread에서도 Main Thread의 Handler에 Runnable 객체로 UI 변경 작업을 보내 처리 할 수 있는 것이다.

 

 

(2) Looper

 

Looper는 이름 그대로 무한히 빙글빙글 도는 친구이다.

1. 그냥 돌지는 않고, 계속 동작하면서 Handler가 전달 받은 메세지를 넘겨받아 Message Queue에 순차적으로 넣어준다.

2. 동시에, Message Queue에서 순차적으로 수행할 작업을 꺼내 Handler에게 넘겨주어 Handler가 해당 작업을 처리하도록 한다.

 

Thread() 생성자를 통해 Worker Thread를 생성하면 기본적으로 Looper가 생성되지 않는다.

 

Looper를 생성하지 않는다면 메세지를 Message Queue에 넣지도 못하고 Message Queue에 있는 작업을 꺼내 Handler에게 넘겨주지도 못한다. 즉, Looper를 생성하지 않은 Thread는 다른 Thread에게 메세지를 전달 할 수만 있고, 다른 Thread로부터 메세지를 받지는 못한다.

 

따라서, 다른 Thread로부터 메세지를 전달받을 필요 없이 Worker Thread를 통한 백그라운드 작업만 수행하고 값을 전달할 용도라면 Looper를 생성할 필요는 없다. 그러나 다른 Thread로부터 메세지를 전달받고자 하는 Thread는 Looper를 생성해야 한다.

 

Looper는 다음과 같은 순서로 생성, 삭제한다.

 

class MainActivity : AppCompatActivity() {

    private lateinit var handler: Handler

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val myThread = Thread {
            Looper.prepare() // Looper 준비
            handler = Handler() // myThread의 handler 생성
            Looper.loop() // Looper 시작(동작)
        }
        myThread.start() // myThread 시작
    }

    override fun onDestroy() {
        super.onDestroy()
        handler.looper.quit() // looper 종료
        
        // handler.looper.quitSafely() - message queue의 모든 작업 수행 후 looper 종료
    }

}

 

위와 같이 수동으로 Looper를 생성하는 번거로움을 피하기 위해, Android에서는 Thread를 상속한 HandlerThread 클래스를 제공한다. HandlerThread는 내부에서 Looper.prepare(), Looper.loop()를 수행한다.

 

class MainActivity : AppCompatActivity() {

    private lateinit var handlerThread: HandlerThread

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        handlerThread = HandlerThread("thread_name")
        handlerThread.start() // HandlerThread 시작 및 looper 시작
    }

    override fun onDestroy() {
        super.onDestroy()
        handlerThread.quit() // HandlerThread looper 종료
    }
}

 

Q. 위의 (1)Handler 쪽 코드에서는 Main Thread가 따로 Looper를 생성하지 않았는데도 Main Thread의 Handler에 메세지를 보내는데?

A. Main Thread는 기본적으로 Looper를 가진다. 따로 Looper를 생성할 필요 없이 메세지를 전달 받을 수 있다.

 

 

 

위 Handler, Looper의 동작원리를 이용하여 Worker Thread에서 UI 변경 작업을 어떻게 처리하는지 코드로 보고 마무리한다.

 

class MainActivity : AppCompatActivity() {

    private lateinit var title: TextView
    private lateinit var handler: Handler

    @SuppressLint("SetTextI18n")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        title = findViewById(R.id.tv)

        // Main Thread에서 생성했으므로 Main Thread의 Handler
        handler = Handler()

        val timerThread = TimerThread()
        timerThread.start()

        findViewById<Button>(R.id.stop_btn).setOnClickListener {
            timerThread.stopRunning()
        }
    }

    inner class TimerThread : Thread() {

        private var time = 0
        private var isRunning = true

        override fun run() {
            super.run()
            while (isRunning) {

                //1초 Delay
                sleep(1000)

                // Main Thread Handler에 post를 통해 UI 작업 처리
                // Main Thread의 Handler에 Runnable을 전달하므로
                // 해당 UI 변경 코드는 Main Thread에서 동작한다. 정상 동작
                handler.post {
                    title.text = "Worker Thread 시작 후 ${time}초"
                    time++
                }
            }

            // isRunning = false 이후 UI 작업 처리 (Thread 종료 전)
            handler.post {
                title.text = "Worker Thread가 종료되었습니다."
            }
        }

        fun stopRunning() {
            isRunning = false
        }
    }
}

 

 

 

 

이제는 글 초반에 했던 말을 이해 할 수 있다.

 

Thread에 Handler와 Looper를 두어 스레드 간 Runnable, Message 객체를 주고받을 수 있게 하며, 이를 통해 Worker Thread에서도 Main Thread의 Handler를 통해 Main Thread에서 UI 작업을 수행 할 수 있게 한다.