본문 바로가기
■Development■/《Java》

[Java] Handler 완벽 가이드

by 은스타 2019. 9. 5.
반응형

Java Handler 완벽 가이드

안녕하세요.
이번 포스팅은 안드로이드 애플리케이션 개발에서 핵심적인 역할을 하는 Java Handler에 대해 상세히 알아보겠습니다. Handler는 스레드 간 통신과 비동기 작업 처리를 위한 필수적인 컴포넌트로, 이를 제대로 이해하고 활용하면 안드로이드 앱의 성능과 사용자 경험을 크게 향상시킬 수 있습니다. 이 글에서는 Handler의 개념부터 실제 활용 방법, 그리고 주의해야 할 사항까지 모든 것을 다루겠습니다.


목차

  1. Handler란 무엇인가?
  2. Handler가 필요한 이유
  3. Handler의 기본 구조와 동작 원리
  4. Handler와 Looper의 관계
  5. Message와 MessageQueue
  6. Handler 기본 사용법
  7. Handler로 지연된 작업 처리하기
  8. Handler를 활용한 반복 작업
  9. Handler 사용 시 메모리 누수 방지하기
  10. Handler vs 다른 비동기 처리 방식
  11. 실무에서의 Handler 활용 패턴
  12. 자주 묻는 질문
  13. 결론

 

#1. Handler란 무엇인가?

Handler는 안드로이드 프레임워크에서 제공하는 클래스로, 스레드 간 메시지 전달 및 작업 스케줄링을 담당합니다. 기본적으로 안드로이드의 UI는 단일 스레드(메인 스레드)에서만 업데이트할 수 있기 때문에, 백그라운드 스레드에서 UI를 변경하려면 Handler를 통해 메인 스레드로 작업을 전달해야 합니다.

public class Handler {
    // Handler 내부에는 메시지 처리, 작업 실행 등을 위한 다양한 메서드가 있습니다.
}

Handler는 다음과 같은 핵심 기능을 제공합니다:

  1. 메시지 전송: 한 스레드에서 다른 스레드로 메시지 전달
  2. 작업 스케줄링: 특정 시간 후에 작업 실행 또는 주기적 작업 예약
  3. UI 업데이트: 백그라운드 스레드에서 UI 스레드로 데이터 전달

 

#2. Handler가 필요한 이유

안드로이드 애플리케이션에서 Handler가 필요한 이유는 다음과 같습니다:

1. 안드로이드 UI 스레드 모델

안드로이드는 싱글 스레드 모델을 기반으로 하며, 모든 UI 조작은 메인 스레드(UI 스레드)에서만 이루어져야 합니다. 다른 스레드에서 UI를 직접 업데이트하려고 하면 CalledFromWrongThreadException이 발생합니다.

// 잘못된 예: 백그라운드 스레드에서 직접 UI 업데이트
new Thread(new Runnable() {
    @Override
    public void run() {
        textView.setText("백그라운드에서 업데이트"); // 오류 발생!
    }
}).start();

2. 비동기 작업 처리

네트워크 요청, 파일 입출력 등의 시간이 오래 걸리는 작업은 메인 스레드에서 직접 수행할 경우 앱이 멈춘 것처럼 보일 수 있습니다. 이러한 작업은 백그라운드 스레드에서 수행하고, 결과를 Handler를 통해 UI 스레드로 전달해야 합니다.

3. 지연된 작업 실행

특정 시간 후에 작업을 실행하거나, 주기적으로 작업을 반복해야 할 때 Handler를 활용할 수 있습니다.

 

#3. Handler의 기본 구조와 동작 원리

Handler는 Message 객체를 받아 처리하거나, Runnable 객체를 실행합니다. 이를 위해 내부적으로 MessageQueueLooper를 사용합니다.

Handler의 생명 주기

  1. 생성: Handler는 Looper가 연결된 스레드에서 생성됩니다.
  2. 메시지 전송: sendMessage() 또는 post() 메서드를 통해 메시지나 Runnable이 MessageQueue에 추가됩니다.
  3. 메시지 처리: Looper가 MessageQueue에서 메시지를 꺼내 Handler의 handleMessage() 메서드를 호출합니다.
// Handler 생성 및 메시지 처리
Handler handler = new Handler(Looper.getMainLooper()) {
    @Override
    public void handleMessage(Message msg) {
        // 메시지 처리 코드
        switch (msg.what) {
            case MSG_UPDATE_UI:
                updateUI();
                break;
            case MSG_PROCESS_DATA:
                processData((String) msg.obj);
                break;
        }
    }
};

 

#4. Handler와 Looper의 관계

Handler는 독립적으로 동작할 수 없으며, Looper와 함께 작동합니다. Looper는 메시지 루프를 관리하며, MessageQueue에서 메시지를 꺼내 적절한 Handler에 전달하는 역할을 합니다.

Looper의 역할

  • MessageQueue에서 메시지를 순차적으로 가져옴
  • 각 메시지를 대상 Handler에 전달
  • 한 스레드에는 하나의 Looper만 존재 가능
// 일반 스레드에 Looper 설정하기
class LooperThread extends Thread {
    public Handler handler;

    @Override
    public void run() {
        Looper.prepare(); // 현재 스레드에 Looper 생성

        handler = new Handler(Looper.myLooper()) {
            @Override
            public void handleMessage(Message msg) {
                // 메시지 처리
            }
        };

        Looper.loop(); // 메시지 루프 시작
    }
}

메인 Looper

안드로이드 애플리케이션의 메인 스레드는 이미 Looper가 설정되어 있습니다. 메인 Looper는 Looper.getMainLooper()로 접근할 수 있습니다.

// 메인 스레드의 Handler 생성
Handler mainHandler = new Handler(Looper.getMainLooper());

 

#5. Message와 MessageQueue

Handler 시스템의 또 다른 중요한 구성 요소는 Message와 MessageQueue입니다.

Message

Message는 Handler가 처리하는 데이터 객체입니다. 주요 필드는 다음과 같습니다:

  • what: 메시지 유형을 식별하는 정수 값
  • arg1, arg2: 추가 정수 파라미터
  • obj: 임의의 객체 참조
  • target: 이 메시지를 처리할 Handler
  • callback: 메시지와 함께 실행될 Runnable
// Message 객체 사용 예
Message message = Message.obtain();
message.what = MSG_UPDATE_PROGRESS;
message.arg1 = progressValue;
message.obj = someData;
handler.sendMessage(message);

MessageQueue

MessageQueue는 Handler가 처리할 Message나 Runnable 객체들을 저장하는 자료구조입니다. Looper는 이 큐에서 메시지를 하나씩 꺼내 처리합니다.

 

#6. Handler 기본 사용법

Handler를 사용하는 방법은 크게 두 가지로 나눌 수 있습니다:

1. 메시지 기반 방식

// 1. Handler 정의
private static final int MSG_UPDATE_UI = 1;
private Handler handler = new Handler(Looper.getMainLooper()) {
    @Override
    public void handleMessage(Message msg) {
        if (msg.what == MSG_UPDATE_UI) {
            // UI 업데이트 로직
            TextView textView = findViewById(R.id.textView);
            textView.setText((String) msg.obj);
        }
    }
};

// 2. 백그라운드 스레드에서 메시지 전송
new Thread(new Runnable() {
    @Override
    public void run() {
        // 시간이 걸리는 작업 수행
        String result = performLongTask();

        // UI 업데이트를 위한 메시지 전송
        Message message = Message.obtain();
        message.what = MSG_UPDATE_UI;
        message.obj = result;
        handler.sendMessage(message);
    }
}).start();

2. Runnable 기반 방식

// Handler 생성
private Handler handler = new Handler(Looper.getMainLooper());

// 백그라운드 스레드에서 Runnable 전송
new Thread(new Runnable() {
    @Override
    public void run() {
        // 시간이 걸리는 작업 수행
        final String result = performLongTask();

        // UI 스레드에서 실행할 Runnable 전송
        handler.post(new Runnable() {
            @Override
            public void run() {
                // UI 업데이트 로직
                TextView textView = findViewById(R.id.textView);
                textView.setText(result);
            }
        });
    }
}).start();

 

#7. Handler로 지연된 작업 처리하기

Handler를 사용하면 특정 시간 후에 작업을 실행하거나, 일정 시간 간격으로 작업을 반복할 수 있습니다.

1. 지연된 작업 실행

// 2초 후에 작업 실행
handler.postDelayed(new Runnable() {
    @Override
    public void run() {
        // 지연 후 실행할 코드
        showToast("2초가 지났습니다!");
    }
}, 2000); // 2000 밀리초 = 2초

2. 메시지를 사용한 지연 실행

Message message = Message.obtain(handler, MSG_TIMEOUT);
handler.sendMessageDelayed(message, 5000); // 5초 후 메시지 전송

#8. Handler를 활용한 반복 작업

Handler를 사용하여 주기적으로 반복되는 작업을 구현할 수 있습니다. 이는 타이머, 애니메이션, 주기적인 데이터 갱신 등에 유용합니다.

private static final int INTERVAL = 1000; // 1초
private Handler handler = new Handler(Looper.getMainLooper());
private int counter = 0;

private Runnable updateTask = new Runnable() {
    @Override
    public void run() {
        // 수행할 작업
        updateCounter(counter++);

        // 다음 실행 예약
        handler.postDelayed(this, INTERVAL);
    }
};

// 반복 작업 시작
public void startRepeatingTask() {
    updateTask.run();
}

// 반복 작업 중지
public void stopRepeatingTask() {
    handler.removeCallbacks(updateTask);
}

 

#9. Handler 사용 시 메모리 누수 방지하기

Handler 사용 시 발생할 수 있는 가장 흔한 문제 중 하나는 메모리 누수입니다. 특히 익명 내부 클래스로 Handler를 구현할 경우, 외부 Activity/Fragment에 대한 참조를 유지하게 되어 GC(Garbage Collection)가 제대로 이루어지지 않을 수 있습니다.

메모리 누수 예방법

1. 정적 내부 클래스와 WeakReference 사용

public class MyActivity extends AppCompatActivity {
    private TextView resultView;
    private MyHandler handler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        resultView = findViewById(R.id.result_view);
        handler = new MyHandler(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // 모든 콜백 및 메시지 제거
        handler.removeCallbacksAndMessages(null);
    }

    // 정적 내부 클래스로 Handler 구현
    private static class MyHandler extends Handler {
        // Activity에 대한 약한 참조
        private final WeakReference<MyActivity> activityRef;

        MyHandler(MyActivity activity) {
            super(Looper.getMainLooper());
            this.activityRef = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            MyActivity activity = activityRef.get();
            if (activity == null) return; // Activity가 파괴된 경우

            // 메시지 처리
            if (msg.what == MSG_UPDATE_UI) {
                activity.resultView.setText((String) msg.obj);
            }
        }
    }
}

2. 생명주기에 맞춰 Handler 정리하기

@Override
protected void onDestroy() {
    super.onDestroy();
    // 모든 지연된 메시지와 콜백 제거
    handler.removeCallbacksAndMessages(null);
}

 

#10. Handler vs 다른 비동기 처리 방식

안드로이드에서는 Handler 외에도 다양한 비동기 처리 방식이 있습니다. 각각의 장단점을 비교해보겠습니다.

No 비동기 처리 방식 장점 단점 적합한 사용 사례
1 Handler 세밀한 제어 가능, 스레드 간 통신 용이 직접 관리 필요, 메모리 누수 가능성 UI 업데이트, 지연된 작업, 반복 작업
2 AsyncTask 간단한 사용법, 진행 상태 업데이트 쉬움 API 29에서 deprecated, 구성 변경 시 문제 짧은 백그라운드 작업 (더 이상 권장되지 않음)
3 Executor 간결한 API, 스레드 풀 관리 UI 업데이트에 추가 단계 필요 다수의 백그라운드 작업
4 Coroutines (Kotlin) 간결한 코드, 취소 관리 용이 Java에서 사용 제한적 복잡한 비동기 작업, 순차적 작업
5 RxJava 강력한 스트림 처리, 조합 가능 학습 곡선 높음, 복잡성 복잡한 이벤트 처리, 데이터 스트림

언제 Handler를 사용해야 할까?

  • UI 스레드와 직접 통신이 필요할 때
  • 세밀한 타이밍 제어가 필요한 작업
  • 간단한 지연 작업이나 반복 작업
  • 낮은 수준의 스레드 통신이 필요할 때

 

#11. 실무에서의 Handler 활용 패턴

1. 스플래시 화면 구현

public class SplashActivity extends AppCompatActivity {
    private static final long SPLASH_DELAY = 2000; // 2초
    private Handler handler = new Handler(Looper.getMainLooper());

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_splash);

        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                startActivity(new Intent(SplashActivity.this, MainActivity.class));
                finish();
            }
        }, SPLASH_DELAY);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        handler.removeCallbacksAndMessages(null);
    }
}

2. 실시간 데이터 업데이트

public class RealTimeUpdateActivity extends AppCompatActivity {
    private static final int UPDATE_INTERVAL = 1000; // 1초
    private Handler handler = new Handler(Looper.getMainLooper());
    private TextView dataView;
    private boolean isUpdating = false;

    private Runnable updateRunnable = new Runnable() {
        @Override
        public void run() {
            updateData();
            if (isUpdating) {
                handler.postDelayed(this, UPDATE_INTERVAL);
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_realtime);

        dataView = findViewById(R.id.data_view);
    }

    @Override
    protected void onResume() {
        super.onResume();
        isUpdating = true;
        handler.post(updateRunnable);
    }

    @Override
    protected void onPause() {
        super.onPause();
        isUpdating = false;
        handler.removeCallbacks(updateRunnable);
    }

    private void updateData() {
        // 데이터 업데이트 로직
        String newData = fetchLatestData();
        dataView.setText(newData);
    }

    private String fetchLatestData() {
        // 실제 데이터를 가져오는 코드
        return "Data: " + System.currentTimeMillis();
    }
}

3. 네트워크 타임아웃 처리

public void performNetworkRequest() {
    final Handler timeoutHandler = new Handler(Looper.getMainLooper());
    final Runnable timeoutRunnable = new Runnable() {
        @Override
        public void run() {
            // 타임아웃 처리
            showTimeoutError();
            cancelRequest();
        }
    };

    // 10초 후 타임아웃 설정
    timeoutHandler.postDelayed(timeoutRunnable, 10000);

    // 네트워크 요청 시작
    startNetworkRequest(new Callback() {
        @Override
        public void onResponse(Response response) {
            // 타임아웃 취소
            timeoutHandler.removeCallbacks(timeoutRunnable);

            // 응답 처리
            processResponse(response);
        }

        @Override
        public void onFailure(Exception e) {
            // 타임아웃 취소
            timeoutHandler.removeCallbacks(timeoutRunnable);

            // 오류 처리
            handleError(e);
        }
    });
}

 

#12. 자주 묻는 질문

Q1: Handler는 항상 UI 스레드에서만 사용해야 하나요?

A: 아닙니다. Handler는 어떤 스레드에서도 사용할 수 있지만, 해당 스레드에 Looper가 설정되어 있어야 합니다. 메인 스레드(UI 스레드)는 기본적으로 Looper가 설정되어 있지만, 다른 스레드에서는 Looper.prepare()Looper.loop()를 호출하여 Looper를 설정해야 합니다.

Q2: HandlerThread는 무엇인가요?

A: HandlerThread는 자체적으로 Looper를 갖는 Thread의 확장 클래스입니다. 기본 스레드와 달리 자동으로 Looper를 설정하고 시작하므로, Handler를 쉽게 연결할 수 있습니다.

HandlerThread handlerThread = new HandlerThread("BackgroundThread");
handlerThread.start();

Handler backgroundHandler = new Handler(handlerThread.getLooper());
backgroundHandler.post(new Runnable() {
    @Override
    public void run() {
        // 백그라운드에서 실행될 코드
    }
});

Q3: postDelayed와 sendMessageDelayed의 차이점은 무엇인가요?

A: 둘 다 지연된 작업을 예약하는 메서드이지만, postDelayed는 Runnable을 사용하고 sendMessageDelayed는 Message 객체를 사용합니다. Runnable 방식이 더 간단하지만, Message 방식은 더 많은 데이터를 전달할 수 있고 what 필드를 통해 메시지 유형을 구분할 수 있습니다.

Q4: Handler를 사용할 때 메모리 누수를 방지하는 방법은 무엇인가요?

A: 메모리 누수를 방지하기 위한 주요 방법은 다음과 같습니다:

  1. 정적 내부 클래스와 WeakReference 사용
  2. Activity/Fragment 생명주기에 맞춰 Handler 콜백 제거
  3. 적절한 시점에 removeCallbacksAndMessages(null) 호출

Q5: 안드로이드 API 30 이상에서 Handler 사용 시 주의할 점이 있나요?

A: API 30 이상에서는 Handler 생성자 사용 시 Looper를 명시적으로 지정하는 것이 권장됩니다. 또한 Handler.Callback 인터페이스를 구현하는 방식도 권장됩니다.

// 권장되는 방식
Handler handler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
    @Override
    public boolean handleMessage(@NonNull Message msg) {
        // 메시지 처리
        return true; // 메시지가 처리되었음을 표시
    }
});

 

#13. 결론

Java Handler는 안드로이드 애플리케이션 개발에서 스레드 간 통신과 비동기 작업 처리를 위한 핵심적인 컴포넌트입니다. 메인 스레드와 백그라운드 스레드 간의 원활한 통신, 지연된 작업 실행, 반복 작업 처리 등 다양한 상황에서 유용하게 활용될 수 있습니다.

Handler를 효과적으로 사용하기 위해서는 Looper, Message, MessageQueue의 개념과 동작 원리를 이해하는 것이 중요합니다. 또한 메모리 누수와 같은 잠재적인 문제를 방지하기 위해 적절한 패턴을 적용해야 합니다.

최신 안드로이드 개발에서는 Kotlin Coroutines나 RxJava와 같은 대안이 있지만, Handler는 여전히 많은 상황에서 유용하며 안드로이드 프레임워크의 기본 구성 요소로 동작합니다. 따라서 모든 안드로이드 개발자는 Handler의 기본 개념과 사용법을 숙지하는 것이 중요합니다.

여러분의 앱 개발에서 Handler를 어떻게 활용하고 계신가요? 흥미로운 사용 사례나 팁이 있다면 댓글로 공유해 주세요!


마무리

긴 글 읽어주셔서 감사합니다.

끝.

 

 

참고 자료

반응형