Thread+Handler+Looper

장범준·2023년 5월 18일
0

※지극히 저의 언어로 해석해서 이해한 내용입니다.※

그개 뭔가요?

이 셋은 서로가 서로를 봐완하기 위해 존재 합니다. 그리고 그 연관 관계는 수요의 흐름을 보다보면 쉽게 이해 할수있을 것입니다.

Thread

Thread는 '손'입니다. 손은 주로 일처리를 하죠, 그렇기에 Thread는 하나의 작업단위라고 볼 수 있습니다(하나의 Thread가 하나의 작업을 처리한다고 생각하면 편함). 처음에 사용되는 Thread를 메인 스레드라고 합니다. 하지만 하나의 Thread로 동시에 여러 작업을 하기에는 무리가 있습니다. 그래서 동시에 여러 작업을 하기위해 멀티 스레드라는 개념이 존재합니다(말 그대로 스레드를 여러개 만든 것). 같은 프로세스 안에 들어 있으면서 메모리 리소스를 공유하기 때문에 효율적인 처리 가능하다는 장점도 있습니다.

Handler

Handler는 UI한정 Thread에 '대리자' 입니다. 만약 Thread가 여러개있는 상황에서, 둘 이상의 Thread가 하나의 UI객체에 접근하게 되면 데드락이 발생됩니다(교착상태 같은 것. 둘 이상의 명령이 왔을 때, 무엇을 먼저 처리해야 될 지 모르는 상태). 그래서 이를 방지하기위해 UI에 접근 권한은 MainThread에게 만 주고, 다른 Thread는 Handler객체에게 메세지를 보내 UI수정을 요청합니다. 그럼 Handler객체가 판단하여 데드락이 생기지 않도록 잘 처리합니다(Queue{MessageQueue}를 사용, 스케쥴링 가능).

Looper

Looper는 Handler의 일을 덜어주는 일을 합니다. 만약 음식점에서 한 사람이 주문도 받으면서, 서빙도 해야 된다면, 그건 극한 직업이겠죠? 그래서 Handler에게 주문과 서빙을 다 시키지 않고, Looper에게 서빙 역할을 줍니다(엄연히 얘기하면 MessageQueue를 관리 함).

작동 방식

위에 글로만 이해하기에는 부족함이 있다 생각되어 작동 방식을 설명하자면...

  1. multi Thread로 앱을 돌리던중 UI에 수정이 필요한 작업 발생합니다.
  2. Thread가 직접 수행하다가는 데드락이 발생할 수 있기 때문에 Handler에게 요청합니다.
  3. Handler는 이 요청을 MessageQueue에 기록합니다.
  4. MessageQueue는 Looper가 관리하며 처리할 메시지를 Handler에 전달합니다.
  5. Handler는 handleMessage를 통해 메시지를 처리합니다.

이러한 과정을 거치면서 요청들을 처리합니다. 또 이과정을 보면 handler는 할줄아는게 없습니다. MessageQueue와 Looper에 의존적이죠. 한마디로 Looper, Handler, Thread는 서로에 존재가 꼭 필요합니다.

예제

Thread

class BackgroundThread extends Thread {
        // 스레드를 시작하면 run() 메서드가 실행됨
        public void run() {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(1000);
                } catch (Exception e) {}

                value += 1;
                Log.d("Thread", "value : "+value);
            }
        }
    }

이렇듯 Thread를 상속받고, run()메소드를 구현합니다. 그후 객체를 생성하고, start 시키면 쓰레드가 생성되어 돌아가게 됩니다.
위에 예제는 1초마다 value가 1씩 커지는 로직이라는 것을 알 수 있습니다.
더불어 log형태로(logcat에서 볼수있음) value에 값이 출력됩니다. 하지만 우리는 이것이 화면에 나왔으면 좋겠죠? 그래서 화면에 TextView를 추가하고, setText(value) 를 하면 오류가 납니다.

Only the original thread that created a view hierarchy can touch its views.

해석을 해보면 Thread 클래스를 상속받은 BackgroundThread 객체에서 UI 객체를 직접 접근했다는 의미입니다. 더 확대해보면 메인 스레드에서 관리하는 UI 객체는, 개발자가 직접 만든 스레드 객체에서는 접근할 수 없다는 의미입니다.
그렇다면 mainThread에 요청하여 바꾸면 되겠죠? 하지만 이것 또한 데드락에 위협이 있습니다. 그래서 우리는 Handler를 사용합니다. 간단합니다.

Handler(메세지 전송)

public class 클래스 extends AppCompatActivity(지워도 됨) {

    TextView textView;
    MainHandler handler;

    ...

        handler = new MainHandler(); //객체를 생성해주면 끝
    }
    class BackgroundThread extends Thread {
        ...

                Message message = handler.obtainMessage(); 
                Bundle bundle = new Bundle();  
                bundle.putInt("value", value);
                message.setData(bundle);
                handler.sendMessage(message); 
            }
        }
    }
    
    class MainHandler extends Handler {
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            Bundle bundle = msg.getData();
            int value = bundle.getInt("value");
            textView.setText("value 값 : "+value);
        }
    }
  1. Activity 파일에 Handler를 선언만 해주면 됩니다.
  2. Thread에서 obtainMessage()를 호출해, 객체를 반환 받음
  3. 그 후 객체에 메세지를 넣은 후, sendMessage()를 통해 메세지큐에 삽입
  4. 메세지큐가 순차적으로 처리함
  5. 그러다 handleMessage()가 실행되는데, MainThread에서 실행됨

메세지 객체안에는 bundle이 있는데, put()로 데이터를 넣고 get()으로 데이터를 가져올 수 있음. 그래서 MainHandler에 bundle.getInt("키")로 값을 가져 와 setText를 하는걸 볼 수 있다.

Handler(Runnable 객체)

 handler.post(new Runnable() {
                    @Override
                    public void run() {
                        textView.setText("value 값 : "+value);
                    }
                });

Runnable 객체를 핸들러의 post() 메서드로 전달해주면, 이 객체에 정의된 run() 메서드 안의 코드들은 메인 스레드에서 실행되는 구조입니다. 이거는 따로 Handler를 구현해주지 않아도 됩니다. 더불어 간편합니다.

Looper

Thread t = new Thread(new Runnable(){
    @Override
    public void run() {
        Looper.prepare(); //생성
        handler = new Handler();
        Looper.loop(); //메세지 전송
    }
});
t.start(); 

Looper.prepare() 를 통해 해당 쓰레드에 종속되는 Looper 와 MessageQueue 가 생성되어 준비를 합니다. 그 후 Handler 를 생성해주면, Handler 와 Looper 가 연결됩니다. Looper.loop() 를 호출해줌으로써 Message 전달을 하고, 기다립니다.
더불어 작업이 종료되었을 때(onDestory() 일때), Handler.getLooper.quit()를 이용해 꼭 종료해야 한다.

하지만 이것은 수행과정 중 작업이 손실되거나 충돌하는 등의 버그가 발생할 수 있어서 deprecated(잘 사용하지 않음) 되었습니다.

그래서 우리는 안드로이드에서 지원해주는 HandlerThread 클래스를 활용할 수 있습니다.

HandlerThread ht = new HandlerThread("my HandlerThread");
ht.start();
handler = new Handler(ht.getLooper());

이는 기본적으로 Looper를 가지고 있고 해당 Thread를 start시키면 자동으로 Loop도 돌기 때문에 편리하게 쓸 수 있습니다.

정리

멀티스레드 구현을 위해 Thread와 Handler,Looper를 공부해 보았습니다. 제가 원하는 자료를 찾아 여기저기 들어가다보니 코드끼리 통일이되지 않고, 호환이 안될 수 도 있을것 같습니다. 더불어 공부에 목적에 맞게 생략한 내용도 많으니 꼬 다른 velog도 찾아보시기를 바랍니다!! 출처는 정리되면 올리겠습니다.

profile
자신의 언어로 해석해, 자신의 것으로 만둘자.

0개의 댓글