본문 바로가기
Android/Background 처리

Thread, Looper, Handler

by jaesungLeee 2021. 12. 8.

1. 동기와 비동기

안드로이드 Thread를 이해하기 위해 동기와 비동기의 차이를 명확하게 구분지어야 한다.

동기(Synchronous) 방식은 어떤 작업을 수행하고 그 작업이 완료될 때까지 다른 작업을 하지 못하고 기다리는 방식이다. 비동기(Asynchronous) 방식은 어떤 작업을 수행하지만 작업의 완료 여부와 관계없이 계속해서 다른 작업을 진행할 수 있는 방식이다.


2. Main Thread

Android Components들이 시작되고 어플리케이션에 실행 중인 다른 Component들이 없으면 Android System은 하나의 단일 Thread로 Linux Process를 시작한다. 기본적으로 동일한 어플리케이션 내의 Components들은 같은 Process와 Thread에서 실행된다. (필요한 경우 다른 Process에서 실행되게 할 수도 있다.) 이 Thread를 Main Thread라고 부른다. 

Main Thread는 화면의 UI를 그리는 담당을 하기 때문에 UI Thread라고도 불린다. Main Thread는 Android UI Kit의 구성요소들 (android.widget, android.view)와 상호작용하고, UI의 Event를 사용자에게 응답하는 역할을 한다.


3. Android에 비동기가 필요한 이유

기본적으로 안드로이드는 Main Thread만을 갖는 Single Thread System이다. 이제 어플리케이션이 사용자의 상호작용에 응답하여 Resource를 많이 소모하는 작업을 수행하는 경우를 생각해볼 수 있다. Single Thread System인 안드로이드의 Main Thread (UI Thread)에서 이 작업을 수행하는 경우 낮은 성능을 보이며 전체 UI가 차단될 수 있다. 이 경우, 사용자에게는 어플리케이션이 중단된 것처럼 보인다. Main Thread가 약 5초간 차단되는 더욱 심각한 경우, ANR ("어플리케이션이 응답하지 않습니다")라는 다이얼로그가 표시된다. 그럼 사용자는 애플리케이션을 종료하게 되고 어플리케이션을 삭제할 수도 있다. 따라서 이러한 작업들은 다른 Thread에서 처리되어야 하기 때문에 비동기가 필요하다.

반면, 위에서 설명한 Android UI Kit 즉, UI와 관련된 작업은 다른 Thread에서 조작하지 않고 모두 Main Thread (UI Thread)에서 조작해야 한다. 

지금까지 다룬 내용들을 정리하여 공식문서에서는 아래 두 규칙을 따르도록 제시한다.

1. Main Thread (UI Thread)는 차단되면 안된다.

2. Main Thread (UI Thread)가 아닌 다른 Thread에서 Android UI Kit에 액세스 할 수 없다.


4. Background Thread (Worker Thread)

계속 설명한 것처럼 애플리케이션 UI가 정상적으로 동작하기 위해서는 Main Thread (UI Thread)를 차단하지 않는 것이 중요하다. 즉, 수행되어야 하지만 UI처럼 즉각적인 조치가 필요하지 않는 작업의 경우 별도의 Thread에서 수행해야 한다. 이 별도의 Thread를 보통 Background Thread 또는 Worker Thread라고 부른다. 

그럼 Background Thread에서 Main Thread (UI Thread)에 액세스해 UI를 업데이트해야 할 경우가 있을 수 있다. Background Thread에서 Main Thread에 액세스 하는 여러 방법 중 가장 많이(?) 사용되는 Method를 아래에 설명한다.

 

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • View.postDelayed(Runnable, long)

 

binding.button.setOnClickListener {
    Thread {
        val bitmap = processBitMap("image.png")

        imageView.post {
            imageView.setImageBitmap(bitmap)
        }
    }.start()
}

 

위의 예시는 Thread로부터 안전하다. processBitMap( )은 별도의 Thread에서 수행되는 반면 ImageView 자체는 항상 Main Thread (UI Thread)에서 조작되기 때문이다. 


5. Looper & Handler

외부 작업을 위해 생성한 다른 Thread에서 UI 액세스를 위해 Main Thread로 접근하는 것을 위에서 설명했다. 내부적으로는 Looper와 Handler를 사용한다. 아래 그림으로 확인할 수 있다.

Looper와 Handler를 사용하는 목적을 아래와 같이 생각해볼 수 있다. 같은 설명을 반복하고 있지만 Android에서 Main Thread에서만 UI 작업이 가능하도록 제한한다. 만약 Main Thread와 별도의 Thread가 동시에 같은 TextView에 액세스 하여 setText( )를 시도하는 경우, 둘 중 어느 Thread의 setText( )가 적용될지 예측할 수 없고, 사용자는 둘 중 하나의 값만 볼 수 있기 때문에 다른 Thread의 결과는 버려진다.

결과적으로 우리는 Main Thread에서만 UI 액세스가 가능하다는 것을 알고 있지만 두 개 이상의 Thread를 사용할 때 동기화 이슈를 해결하기 위해 Looper와 Handler를 사용한다.

 

위 그림은 Looper와 Handler의 작동 원리를 보이고 있다. Main Thread는 내부적으로 Looper를 가지고 그 안에는 Message Queue가 포함된다. Message Queue는 해당 Thread가 다른 Thread 혹은 자기 자신으로부터 전달받은 Message를 FIFO 형식으로 보관하는 Queue이다. Looper는 Message Queue에서 Message 또는 Runnable 객체를 차례로 꺼내 Handler에서 처리하도록 전달한다. Handler는 Looper로부터 받은 Message를 실행 또는 처리하거나 다른 Thread로부터 Message를 받아 MessageQueue에 넣는 역할을 하는 Thread 간 통신장치이다.

5.1 Handler

Handler는 Thread의 Message Queue와 연계하여 Message나 Runnable 객체를 받거나 처리하여 Thread간 통신을 할 수 있도록 한다. 각 Handler의 인스턴스는 해당 Thread의 Message Queue와 연관되어있다. 즉, Handler는 Looper에 바인딩되어 의존적이라고 볼 수 있다.

아래 많이 사용되는 Handler 클래스의 Method를 설명한다.

 

  1. handleMessage(Message msg) : Looper가 MessageQueue에서 꺼내 준 Message나 Runnable 객체를 처리
  2. sendMessage(Message msg) : MessageQueue에 Message를 전달
  3. sendMessageAtFrontOfQueue(Message msg) : MessageQueue의 맨 앞에 Message를 전달
  4. sendMessageDelayed(Message msg, long delayMillis) : delayMillis만큼 지연 후 MessageQueue에 Message를 전달
  5. post(Runnable r) : MessageQueue에 Runnable 객체 전달
  6. postAtFrontOfQueue(Runnable r) : MessageQueue의 맨 앞에 Runnable 객체 전달
  7. postDelayed(Runnable r, long delayMillis) : delayMillis만큼 지연 후 MessageQueue에 Runnable 객체 전달

 

Handler가 Message나 Runnable 객체를 전달하는 역할을 한다는 것을 알게 되었다. 하지만 이 두 가지는 차이점이 있을 것이다.

 

Thread 간 통신에서 Handler를 사용하여 데이터를 보내기 위해서는, 데이터 종류를 식별할 수 있는 식별자와 실질적인 데이터를 저장한 객체, 추가 정보를 전달할 객체가 필요하다. 즉, Message는 전달할 데이터를 한 곳에 저장하는 역할을 하는 클래스이다. handler.optainMessage( ) 또는 message.obtain( )을 사용하여 데이터를 담게 된다. Java에서는 아래와 같이 사용된다.

 

class NewThread extends Thread {
    Handler handler = mHandler ;

    @Override
    public void run() {
        while (true) {
            // obtain a message.
            Message message = handler.obtainMessage() ;

            // fill the message object.
            message.what = MSG_A ;
            message.arg1 = ... ;
            message.arg2 = ... ;
            message.obj = ... ;

            // send message object.
            handler.sendMessage(message) ;
        }
    }
}

 

Message 객체를 사용하는 경우 메시지에 저장된 데이터의 종류를 식별하기 위한 식별자를 정의해야 하고, 다른 Thread에서 작성된 코드를 실행시키기 위해 전달할 데이터를 Message 객체에 담아야 한다. 핵심은 오직 다른 Thread에서 작성된 코드를 실행시키기 위해 사용한다는 것이다.

 

Runnable 객체는 Message를 통해 데이터를 전달하는 불필요한 과정 말고 실행할 코드 자체를 바로 보내기 위해 사용한다. 즉, Handler에 실행 코드가 담긴 객체를 보내고, 대상 Thread에서는 수신된 객체의 코드를 직접 실행하도록 한다. 실행 코드가 담긴 객체가 Runnable이다. 

 

Handler를 통해 다른 Thread로 무언가 보내야 한다면 데이터 (Message) 보다는 데이터가 포함된 코드 (Runnable) 자체를 보내는 것이 효율적인 것 같다는 생각이 든다.

5.2 Looper

Looper는 하나의 Thread만 담당할 수 있고, 하나의 Thread도 오직 하나의 Looper를 갖는다. Looper는 무한히 루프를 돌며 자신이 속한 Thread의 MessageQueue에 들어온 Message나 Runnable 객체를 차례로 꺼내 이를 처리할 Handler에 전달하는 역할을 한다. Main Thread는 기본적으로 Looper를 가지고 있지만, 새로 생성한 Thread는 기본적으로 Looper를 가지고 있지 않다. 단지 run( )만 실행한 후 종료하기 때문에 Message나 Runnable 객체를 받을 수 없다. 

다른 Thread에서 Looper를 만들기 위해 많이 사용되는 Looper 클래스의 Method를 아래에 설명한다.

 

  1. looper.prepare( ) : 해당 Thread에서 Looper를 생성 및 초기화
  2. looper.loop( ) : 해당 Thread에서 무한히 루프를 돌며 MessageQueue를 실행
  3. looper.quit( ) : Message Queue에 존재하는 Message의 여부와 상관없이 Looper를 즉시 종료
  4. looper.quitSafely( ) : Message QUeue에 존재하는 Messge들을 모두 처리한 후 Looper 종료

Main Thread를 제외한 다른 Thread는 Looper를 생성해야 한다는 것을 확인했다. Android의 Thread는 Java의 Thread를 사용하기 때문에 Android에서 도입한 Looper를 기본적으로 가지지 않는다. 이런 불편함을 개선하기 위해 Thread 생성 시 Looper를 자동으로 보유한 클래스가 HandlerThread이다.


6. 추가 내용 - Handler( ) Deprecated

기존에는 Handler를 선언할 때 생성자를 넣어주지 않았었다. 하지만 위 글처럼 Deprecated 되면서 생성자를 넣어줘야 되는데 이때 Looper를 넣어주게 된다. Looper에는 getMainLooper, myLooper 두 종류가 있다.

6.1 Looper.getMainLooper( )

MainThread에 있는 Looper (Main Looper)를 반환한다. 다른 Thread에서 UI 접근이 불가능하기 때문에 MainThread를 통해 UI에 접근하기 위해 사용한다. 아래 예시를 통해 확인할 수 있다.

 

Handler(Looper.getMainLooper()).postDelayed({
   Toast.makeText(this@MainActivity, "LOOPER", Toast.LENGTH_SHORT).show()
}, 3000)

 

6.2 Looper.myLooper( )

Looper.myLooper( )는 호출한 Thread의 Looper를 반환한다.


References

https://academy.realm.io/kr/posts/android-thread-looper-handler/

https://developer.android.com/reference/android/os/Message

https://developer.android.com/reference/android/os/Handler

https://developer.android.com/reference/android/os/Looper

https://recipes4dev.tistory.com/166

https://stackoverflow.com/questions/61023968/what-do-i-use-now-that-handler-is-deprecated

https://velog.io/@sery270/안드로이드-Thread와-Handler-

'Android > Background 처리' 카테고리의 다른 글

Future를 이용한 Thread Pool 작업 완료 통보  (0) 2021.12.13
Thread Pool  (0) 2021.12.13