Android - Handler vs Executor

JS · 06 Dec 2020

Handler는 Android에서 어떤 작업을 처리하기 위해 Message를 보내거나 Runnable을 실행하도록 설계된 클래스입니다.

Executor는 Java에서 제공하는 인터페이스로, Runnable을 실행하도록 설계되었습니다.

이 둘은 Runnable과 같은 Task를 특정 Thread에서 수행시키는 공통점이 있습니다. 반면에 여러가지 차이점들도 있습니다.

이 글에서는 Handler와 Executor의 차이점에 대해서 알아보았습니다.

Android 예제 코드는 코틀린으로 작성되었으며, Framework 또는 Java 라이브러리는 Java로 참조하였습니다.

Android Library, Java Library

Handler는 Android에서 제공하는 라이브러리입니다. 반면에 Executor는 Java에서 제공하는 라이브러리입니다. Executor를 사용하면 Android 개발 경험이 없는 Java 개발자가 더 이해하기 쉬울 수 있습니다.

다음은 Android Handler 클래스입니다. 일반적으로 sendMessage()post()로 Task를 실행시킵니다.

public class Handler {

  public final boolean sendMessage(@NonNull Message msg) {
      return sendMessageDelayed(msg, 0);
  }

  public final boolean post(@NonNull Runnable r) {
     return  sendMessageDelayed(getPostMessage(r), 0);
  }
  ....
}

다음은 Java의 Executor 인터페이스입니다. execute()로 Task를 실행시킵니다.

public interface Executor {
    void execute(Runnable command);
}

Message 전달, Runnable 수행

Handler는 Message 전달 및 Runnable을 수행할 수 있습니다. 반면에 Executor는 Message 전달 기능이 없고, Runnable만 실행시킬 수 있습니다.

Handler는 다음과 같이 handleMessage()를 구현할 수 있습니다.

class MyHandler : Handler() {
    companion object {
        const val TAG = "MyHandler"
        const val MSG_DO_SOMETHING1 = 1
        const val MSG_DO_SOMETHING2 = 2
    }
    override fun handleMessage(msg: Message) {
        when (msg.what) {
            MSG_DO_SOMETHING1 -> {
                Log.d(TAG, "Do something1")
            }
            MSG_DO_SOMETHING2 -> {
                Log.d(TAG, "Do something2")
            }
        }
    }
}

다음과 같이 Message를 보내 Handler의 Thread에서 바로 수행되거나 일정 시간 뒤에 수행되도록 할 수 있습니다.

handler.sendEmptyMessage(MyHandler.MSG_DO_SOMETHING2)

handler.sendEmptyMessageDelayed(MyHandler.MSG_DO_SOMETHING3, 2000)

또한, Handler는 다음과 같이 Runnable을 실행할 수 있습니다.

handler.post(object: Runnable {
    override fun run() {
        // do something   
    }
})

반면에 Executor는 다음과 같이 Runnable을 실행시키는 것만 할 수 있습니다.

val executor = this.getMainExecutor()
executor.execute(object: Runnable {
    override fun run() {
        // do something
    }
})

참고로, 익명 클래스를 전달하는 대신에 Lambda 또는 Kotlin에서 제공하는 문법으로 다음과 같이 간단히 구현할 수 있습니다.

executor.execute(Runnable {
    // do something            
});

handler.post {
    // do something
}

P OS 부터 Context.getMainExecutor()로 Main Thread에서 동작하는 Executor를 가져올 수 있습니다.

Single Thread, Multi Thread

Handler는 Single Thread에서 Task가 처리됩니다.

하지만 Executor는 클래스에 따라서 Single Thread에서 처리될 수도 있고, Multi Thread에서 처리될 수도 있습니다.

예를 들면, Android에서 다음과 같이 ThreadPoolExecutor 객체를 생성할 수 있습니다. 이 객체는 원하는 크기의 ThreadPool을 생성하고, execute()로 전달되는 Runnable을 ThreadPool에서 실행시킵니다. 즉, Multi Thread에서 Runnable이 실행됩니다.

val workQueue = LinkedBlockingQueue<Runnable>()
val executor = ThreadPoolExecutor(5 /* corePoolSize */,
        5, 50, TimeUnit.MILLISECONDS, workQueue)

executor.execute { Log.d("Test", "Adding task1") }
executor.execute { Log.d("Test", "Adding task2") }
executor.execute { Log.d("Test", "Adding task3") }
executor.execute { Log.d("Test", "Adding task4") }
executor.execute { Log.d("Test", "Adding task5") }

다음은 위 코드의 실행 결과입니다. TID를 보면 모두 다른 Thread에서 수행된 것을 볼 수 있습니다. Single thread가 아니기 때문에 실행 순서 또한 보장되지 않습니다.

12-06 21:41:16.739  5358  6371 D Test    : Adding task1
12-06 21:41:16.739  5358  6373 D Test    : Adding task3
12-06 21:41:16.739  5358  6375 D Test    : Adding task5
12-06 21:41:16.740  5358  6372 D Test    : Adding task2
12-06 21:41:16.742  5358  6374 D Test    : Adding task4

등록된 Task 개수

Handler의 경우 등록된 Task가 몇개인지 알 수 없습니다.

반면에, 일부 Executor 클래스는 Task 개수에 대한 API를 제공하기도 합니다.

예를 들면, ThreadPoolExecutorgetTaskCount()라는 API로 스케줄링된 Task 개수를 리턴합니다.

val workQueue = LinkedBlockingQueue<Runnable>()
val executor = ThreadPoolExecutor(5, 5,
        50, TimeUnit.MILLISECONDS, workQueue)

executor.execute { Log.d("Test", "Adding task1") }
Log.d("Test", "Task count: ${executor.taskCount}")
executor.execute { Log.d("Test", "Adding task2") }
Log.d("Test", "Task count: ${executor.taskCount}")
executor.execute { Log.d("Test", "Adding task3") }
Log.d("Test", "Task count: ${executor.taskCount}")
executor.execute { Log.d("Test", "Adding task4") }
Log.d("Test", "Task count: ${executor.taskCount}")
executor.execute { Log.d("Test", "Adding task5") }
Log.d("Test", "Task count: ${executor.taskCount}")

Output:

12-06 22:07:10.994  6063  6063 D Test    : Task count: 1
12-06 22:07:10.994  6063  6063 D Test    : Task count: 2
12-06 22:07:10.995  6063  6063 D Test    : Task count: 3
12-06 22:07:10.995  6063  6063 D Test    : Task count: 4
12-06 22:07:10.996  6063  6063 D Test    : Task count: 5
12-06 22:07:11.040  6063  8556 D Test    : Adding task2
12-06 22:07:11.040  6063  8555 D Test    : Adding task1
12-06 22:07:11.041  6063  8558 D Test    : Adding task4
12-06 22:07:11.041  6063  8557 D Test    : Adding task3
12-06 22:07:11.041  6063  8559 D Test    : Adding task5

정리

만약 자신이 구현하려는 것이 Executor만으로 충분하다면, Java 라이브러리인 Executor를 사용하는 것이 좋을 것 같습니다. 또한, Executor를 사용하면 현재는 Single Thread에서 Task가 수행되더라도, 나중에 ThreadPool로 쉽게 확장할 수 있습니다.

반면에 Handler를 사용해야할 때가 있습니다. handleMessage()와 같은 패턴을 사용해야하거나, Context.registerReceiver()와 같이 인자로 Handler를 요구하는 Android API를 사용할 때 입니다.

댓글을 보거나 쓰려면 이 버튼을 눌러주세요.
codechachaCopyright ©2019 codechacha