프로그래밍/Android

[Android] 진짜 쉬운 Main Thread와 Handler

Lou Park 2022. 7. 18. 00:59

Main Thread (UI Thread)

안드로이드는 UI를 업데이트하는데는 메인 스레드만 사용하는, 싱글 스레드 모델이 적용된다. 따라서 I/O나 복잡한 연산이 있는 경우 다른 스레드에서 작업하는 것이 권장된다. 멀티 스레드로 UI를 업데이트 할 경우 일반적인 멀티 스레드 문제에도 직면하게 되는데 Deadlock이나 Race condition등이 대표적인 예시이고, 이는 모든상황에도 그렇지만 특히나 UI에서 발생하면 안되는 문제이다. TextView의 글자를 업데이트 하는데, 여러 스레드에서 동시에 텍스트뷰에 접근해 값을 바꾸는 경우, 어느 한 값은 결국 버려질 수 밖에없다.

 

따라서 다른 스레드에서 UI를 업데이트 하려고 할 경우 Handler를 이용해 다음 작업때 “이렇게 업데이트 해주세요!”라고 메인 스레드에 메세지를 전달해주면 된다.

 

먼저 메인 스레드의 구성요소와 동작 과정을 살펴보자. 메인 스레드는 메인 Looper를 가지고 있고, 애플리케이션이 실행되는 동안 Looper는 닉값을 하며 무한 루프를 돌게된다. 또한 이 Looper는 Message와 Runnable들을 다루는 Message Queue를 가지고 있다.

Thread - Looper - MesageQueue

현재까지의 표현을 도식화 해보면 이렇게 된다. Looper.loop(); 은 매 루프마다 MessageQueue에 접근하여 메세지를 가져와 처리한다. 이렇게 하나하나씩 MessageQueue에 쌓인 요청들만 처리함으로서 race condition 문제를 해결한다.

 

 

Handler

그럼 Handler는 도대체 뭘까? 한마디로 얘기하면, Handler는 MessageQueue를 조작할 수 있게 해준다. MessageQueue에 메세지를 추가하거나, 가져와서 처리하기도 한다. Handler를 통해 스레드간 통신이 가능한 것이다. 아래는 이해를 돕기위해 Handler를 사용할때 자주쓰는 메소드들을 가져왔다.

fun handleMessage(msg: Message)
fun post(r: Runnable)
fun sendMessage(msg: Message)
fun postDelayed(r: Runnable, delayMillis: Long)
fun sendMessageDelayed(msg: Message, delayMillis: Long)

Handler를 생성할때 Looper를 넣어주는데, (안 넣어줄 경우 호출한 스레드의 기본 Looper)이 Looper의 MessageQueue에 접근하게 된다.

 

만약 Looper가 만들어져 있지 않은 다른 스레드에서 Handler를 그냥 초기화 하려고 하면 Looper가 없어서 RuntimeException이 발생할 것이다.

class SomeThread : Thread() {
	override fun run() {
		val handler = Handler()
	}
}

정말로 이 스레드에서 Looper를 사용하려면 아래처럼 Looper를 추가하고 (대신에 Looper가 작동하므로 이 스레드는 종료되지 않는다) Handler를 사용하면 된다.

class SomeThread : Thread() {
	override fun run() {
		Looper.prepare()
		val handler = Handler()
		Looper.loop()
	}
}

와! 너무 불편한걸? Looper를 생성하는걸 깜빡하면 곧 바로 RunimeException이라니, 라고 생각하는 당신을 위해서 제공되는 클래스가 있는데, 바로 HandlerThread이다. HandlerThread는 Looper를 자동으로 생성해준다.

 

Handler가 무엇인지 알았으니 도식에 Handler도 추가해보자.

Thread - Looper - MessageQueue - Handler

Message와 Runnable

용어의 연속이다. 그래도 쫄지말자! Message와 Runnable은 둘다 MessageQueue에 들어가 처리되는 작업단위인데, Message 생긴 꼬라지를 보면 왜 Runnable이 필요한지 곧바로 수긍하게 된다.

public final class Message implements Parcelable {
	public int what;
	public int arg1;
	public int arg2;
	...
}

Ok, 내가 UI에 이미지와 텍스트를 업데이트 할꺼고, 그러면서 애니메이션 효과도 싹 넣고 싶은데 이걸 Message로…보내려면 이러한 역할을 하는 상수 FADE_IN_IMAGE_TEXT = 100을 하나 만들어서 어쩌고 저쩌고…대신에 그냥 실행 코드 자체를 담아 넘겨주는 건 어떨까? 그게 바로 Runnable이다.

 

Message를 사용할때는 직접 Message 생성자를 통해 생성하는 방법과 Message.obtain()을 통해 미리 생성된 메세지 풀에서 메세지를 가져오는 방법이 있는데, 당연하게도 자원낭비를 막기위해 Message.obtain()을 가지고 메세지를 가져오는 것이 좋다.

private static Message sPool;

public static Message obtain() {
    synchronized (sPoolSync) {
        if (sPool != null) {
            Message m = sPool;
            sPool = m.next;
            m.next = null;
            m.flags = 0; // clear in-use flag
            sPoolSize--;
            return m;
        }
    }
    return new Message();
}

내부는 대략 이렇게 생겼는데, 직접 메세지 큐같은 걸 구현할때 이러한 방식으로 구현해보는 것도 좋을 것 같다. 안드로이드도 발전하면서 점점 저수준에있는 것들을 개발하면서 보기 어렵게되었는데, 최근에 다량의 이벤트를 순차적으로 처리하는 부분에 대해서 고민하다 다시 찾아보게 되었다. 요런걸 시스템이라 하는건가...정말 멋지고 깔끔하다.