ANDROID 프로그래밍/THREAD

안드로이드 스레드 통신. 핸들러와 Runnable. (Android Thread Communication. Handler and Runnable)

뽀따 2019. 6. 24. 22:26


1. 안드로이드 핸들러. (Android Handler)

지난 글 [안드로이드 스레드. 핸들러와 메시지. (Android Thread. Handler and Message)]에서, 안드로이드의 가장 기본적인 스레드 통신 방법인 핸들러(Handler)와 메시지(Message)에 대해 살펴보았습니다. 루퍼(Looper)와 메시지큐(MessageQueue), 그리고 핸들러(Handler)와 메시지(Message)에 대해 알아보고, 메시지 전달 및 처리 흐름 내에서 각 요소들이 수행하는 역할들에 대해서 설명했습니다.


핸들러를 통해 메시지를 전달하는 방식은 간단하고 명료합니다. 수신 측 스레드에 핸들러 객체를 생성한 다음 handleMessage() 메서드를 오버라이드하고, 송신 측 스레드에서는 핸들러 객체에서 메시지 객체를 할당하여 값을 채운 다음 sendMessage() 메서드로 보내면 됩니다. 구체적인 절차와 코드 작성 방법은 [안드로이드 스레드. 핸들러와 메시지. (Android Thread. Handler and Message) - 4. 안드로이드 핸들러(Handler) 사용 방법.]에서 확인할 수 있습니다.


그런데 핸들러(Handler)를 통해 스레드 통신을 수행할 때, 반드시 Message 객체만 사용해야 하는 것은 아닙니다. 경우에 따라 Message를 사용하는 방법보다 간단하게 스레드 통신을 수행할 수 있는 방법이 있는데요, 바로 Runnable 객체를 사용하는 방법입니다.

1.1 핸들러(Handler) 사용 목적과 Runnable

[안드로이드 스레드. 핸들러와 메시지. (Android Thread. Handler and Message)]에서, 핸들러(Handler)가 스레드의 메시지 큐(Message Queue)에 메시지(Message)를 보내거나, 수신된 메시지(Message)를 처리할 때 사용하는 클래스라고 설명했었는데요. 사실, 보다 정확한 표현을 위해서는, 여기에 한 가지가 더 추가되어야 합니다. 바로 Runnable 객체도 보낼 수 있다는 것이죠.


안드로이드 개발 참조문서의 Handler 항목(https://developer.android.com/reference/android/os/Handler)을 보면, Handler에 대한 설명이 아래의 문장으로 시작되는 것을 확인할 수 있습니다.


A Handler allows you to send and process Message and Runnable objects associated with a thread's MessageQueue.


번역해보면, "핸들러(Handler)는 스레드의 메시지 큐(MessageQueue)와 관련된 MessageRunnable 객체를 보내고 처리할 수 있게 만들어준다"는 내용입니다. 그런데 여기서, Runnable 객체를 보낼 수 있다는 문장은, 구체적으로 무엇을 의미하는 걸까요? Message를 보내는 것과 Runnable을 보내는 것은 어떤 차이가 있는 걸까요?


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


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


여기서, 한 가지 질문을 던져 보겠습니다. 핸들러 사용의 주 목적이 대상 스레드의 코드를 실행하는 것이라면, 메시지를 통해 데이터를 전달하는 번거로운 과정을 거치지 말고, 그냥 "실행 코드"를 바로 보내면 되지 않을까요? 즉, 핸들러에 실행 코드가 담긴 객체를 보내고, 대상 스레드에서는 수신된 객체의 코드를 직접 실행하도록 만드는 것입니다.


네. 여기서 말하는 "실행 코드가 담긴 객체". 그것이 바로 Runnable 객체입니다.


자, 이제 핸들러를 통해 Runnable 객체를 보낼 수 있다는 문장의 의미가 조금 이해 되시나요? 좀 더 나아가서, [안드로이드 스레드(Android Thread)]를 살펴보고 그 내용을 정확히 이해했다면, 그리고 [안드로이드 개발 참조문서. Runnable]를 확인한 적이 있다면, 대상 스레드에서 실행될 코드를 Runnablerun() 메서드에 작성한다는 것도 눈치채셨을 것 같네요.

2. 핸들러(Handler)로 Runnable 객체 보내기.

Runnable 객체를 보내는 방법도, 수신 스레드 측의 Handler 참조를 사용한다는 점에서는 Message 객체를 보내는 방법과 동일합니다. 하지만 Runnable 객체로 보내고 전달된 코드가 실행되는 부분은 조금 다른데요, 먼저 다음 그림을 통해 Runnable 객체를 보내는 절차를 확인해 보시죠.


2.1 Runnable 수신 스레드 : Handler 객체 생성.

핸들러를 사용해 Runnable 객체를 보내기 위해서는, 먼저 수신 스레드에서 핸들러 객체를 생성해야 합니다. [안드로이드 스레드. 핸들러와 메시지. (Android Thread. Handler and Message.) - 4. 안드로이드 핸들러(Handler) 사용 방법]에서 설명했듯이, Handler 객체는 생성과 동시에 해당 스레드에서 동작 중인 루퍼(Looper)와 메시지 큐(MessageQueue)에 자동으로 연결됩니다.


    Handler mHandler = new Handler() ;


자, 수신 스레드에서 할 일은 이게 전부입니다. [안드로이드 스레드. 핸들러와 메시지. (Android Thread. Handler and Message.) - 4. 안드로이드 핸들러(Handler) 사용 방법] 에서는 핸들러를 생성하고, 수신 메시지를 처리하기 위해 handleMessage() 메서드를 오버라이드했었지만, Runnable 객체를 보낼 때는 이 과정이 필요없습니다. 실행될 코드는 이미 Runnable 객체의 run() 메서드 안에 담겨져 있으니까요.

2.2 Runnable 송신 스레드 : Runnable 객체 생성 및 run() 메서드 오버라이드.

이제 송신 측 스레드에서 Runnable 객체를 만들고 run() 메서드를 오버라이드해야 합니다.


    class NewThread extends Thread {
        Handler handler = mHandler ;

        @Override
        public void run() {
            while (true) {
                // create Runnable instance.
                Runnable runnable = new Runnable() {
                    @Override
                    public void run() {
                        // TODO : 실행 코드 작성.
                    }
                }

                // send runnable object.
                handler.post(runnable) ;
            }
        }
        
    }

2.3 Runnable 객체 보내기

Runnable 객체를 만들고 run() 메서드를 오버라이드하고 나면, 마지막으로 할 일은 Handler.post() 메서드를 사용하여, 앞서 생성한 Runnable 객체를 수신 측 스레드로 보내는 것입니다.


    handler.post(runnable) ;


[안드로이드 스레드. 핸들러와 메시지. (Android Thread. Handler and Message.) - 4. 안드로이드 핸들러(Handler) 사용 방법]에서 Message 객체를 보내기 위해 sendMessage() 메서드를 사용한다는 것 확인했을텐데요, Runnable 객체를 보낼 때는 아래 표에 정리된 "post"로 시작하는 이름의 메서드를 사용합니다.


메서드 프로토타입 설명
boolean post(Runnable r) Runnable 객체를 전달. (핸들러에 연결된 메시지큐에 추가.)
boolean postAtFrontOfQueue((Runnable r) Runnable 객체를 메시지큐의 가장 앞에 추가.
boolean postAtTime(Runnable r, long uptimeMillis) uptimeMillis로 지정된 시각에, Runnable 객체 전달.
boolean postAtTime(Runnable r, Object token, long uptimeMillis) uptimeMillis로 지정된 시각에, Runnable 객체 전달. r을 취소하는데 사용될 수 있는 token 인스턴스 사용 가능.
boolean postDelayed(Runnable r, long delayMillis) 현재 시각에서 delayMillis 만큼의 시간 후에, Runnable 객체 실행.
boolean postDelayed(Runnable r, Object token, long delayMillis) 현재 시각에서 delayMillis 만큼의 시간 후에, Runnable 객체 실행. token 인스턴스를 통해 r의 실행 취소 가능.


"post" 다음에 AtFrontOfQueue, AtTime, Delayed 등의 접미사가 붙은 메서드는 메시지큐 내에서의 Runnable 객체의 위치와 Runnable 객체가 처리되는 시점을 직접 지정하기 때문에, Runnable 객체의 처리 우선순위 또는 스레드 실행 대기 시간 등에 영향을 미칠 수 있습니다. 그러므로 각 메서드의 정확한 동작 방식과 그에 따른 주의사항에 대해 정확히 인지하고 사용하시길 바랍니다.

3. Runnable 인터페이스.

보통 자바 개발자들이 Runnable 인터페이스를 처음 접하게 되는 순간은, 새로운 스레드를 실행할 때 입니다. [안드로이드 스레드(Android Thread)]에서 살펴보았듯이, Runnable 인터페이스를 implements 한 다음, Thread 인스턴스에 전달하고, 생성된 Thread 인스턴스의 start() 메서드를 실행하는 과정을 통해 새로운 스레드를 만들고 실행할 수 있습니다.


    class NewRunnable implements Runnable {
        NewRunnable() {

        }

        public void run() {
            // TODO : thread running codes.
        }
    }

    NewRunnable nr = new NewRunnable() ;
    Thread t = new Thread(nr) ;
    t.start() ;


그런데 스레드를 생성할 때 작성한 Runnable 인터페이스의 코드를 보고, 단순히 Runnable 인터페이스가 새로운 스레드를 실행할 때만 사용된다거나, 심지어 Thread 클래스처럼 스레드 실행 코드가 구현된 클래스로 착각하는 경우가 있습니다.


하지만 명확히 알아두어야 할 것은, 새로운 스레드를 실행하는 역할은 Thread 클래스의 역할입니다. Runnable은 단지 새로운 스레드에서 실행될 run() 메서드를 가지는 인터페이스일 뿐인 것이죠.


Runnable 인터페이스는 단 하나의 메서드, run() 메서드만 가집니다. Runnable 인터페이스를 상속한 클래스가 오버라이드해야 할 단 하나의 메서드이자, Runnable 객체를 참조하는 곳에서 호출할 메서드인 run() 메서드.


Thread 생성 시에 Runnable 인터페이스를 implements 하는 것은 굳이 Thread 클래스를 상속하지 않아도 스레드 실행 코드를 작성할 수 있기 때문이지, Runnable의 용도가 새로운 스레드를 만드는 것에만 국한된 것은 아니라는 것입니다.


Runnable 객체는 어디서든 사용될 수 있습니다. 코드 실행이 필요한 곳이라면 어디서든 Runnable 인터페이스를 상속받아 run() 메서드를 작성한 다음, 해당 객체를 전달해 run() 메서드를 실행할 수 있는 것입니다.


다소 설명이 길어졌는데요, 요약하자면, Runnable 인터페이스가 사용된 코드를 보고, 단순히 새로운 스레드를 실행하는 코드로만 해석하는 실수를 하지 말라는 것입니다.

4. 핸들러(Handler)와 Runnable을 사용한 스레드 통신 예제.

이제 예제를 통해 Runnable 객체를 전달하여 스레드 간 통신을 수행하는 방법을 알아보겠습니다. 예제는, [안드로이드 스레드 예제. 스레드로 고민해보기. (Android Thread UseCase)]에서 핸들러와 메시지를 사용하여 만들어본 디지털 시계를, Message 대신 Runnable을 사용하여 만들어보겠습니다.


아래 예제 코드와 [안드로이드 스레드 예제. 스레드로 고민해보기. (Android Thread UseCase) - 5. 스레드 적용. 스레드 간 통신 적용하기.]에서 작성된 코드를 비교하면, MessageRunnable 사용 방법의 차이를 확인할 수 있습니다.

4.1 Runnable을 전달을 통해 스레드 간 통신을 수행하는 앱 구조 설계.

Runnable을 핸들러로 전달하여 현재 시각을 표시하는 방법의 실행 절차는 아래와 같은 구조를 가집니다.


[STEP-1] 앱 구조 설계. (핸들러를 통해 Runnable 전달)


4.2 메인액티비티 레이아웃 작성.

메인액티비티 화면에는 아래 그림과 같이 화면 가운데 현재 시각을 표시할 수 있는 텍스트뷰를 배치합니다.

[STEP-2] 메인액티비티 레이아웃 작성.
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context=".MainActivity"
    tools:showIn="@layout/activity_main">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="48sp"
        android:text="00:00:00"
        android:id="@+id/clock"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

4.3 Runnable 객체 전달 기능 구현.

자, 이제 Runnable 객체의 run() 메서드에 현재 시각을 표시하는 코드를 작성하고 핸들러를 통해 전달하여, 메인스레드에서 실행되도록 만들겠습니다.


[STEP-3] Runnable 객체에 현재 시각 갱신 코드 작성 후, 메인 스레드로 전달.
public class MainActivity extends AppCompatActivity {

    TextView clockTextView ;
    private static Handler mHandler ;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ... 코드 계속.

        mHandler = new Handler() ;

        // 핸들러로 전달할 runnable 객체. 수신 스레드 실행.
        final Runnable runnable = new Runnable() {
            @Override
            public void run() {
                Calendar cal = Calendar.getInstance();

                SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
                String strTime = sdf.format(cal.getTime());

                clockTextView = findViewById(R.id.clock);
                clockTextView.setText(strTime);
            }
        } ;

        // 새로운 스레드 실행 코드. 1초 단위로 현재 시각 표시 요청.
        class NewRunnable implements Runnable {
            @Override
            public void run() {
                while (true) {

                    try {
                        Thread.sleep(1000);
                    } catch (Exception e) {
                        e.printStackTrace() ;
                    }

                    mHandler.post(runnable) ;
                }
            }
        }

        NewRunnable nr = new NewRunnable() ;
        Thread t = new Thread(nr) ;
        t.start() ;
    }
}

4.4 실행 결과.

예제 작성을 완료하고 실행하면, 현재 시각을 1초 마다 화면에 표시하는 것을 확인할 수 있습니다.


5. 참고.

.END.