이번에는 Android 에서의 Thread, Handler, Runnable 에 대해서 알아봅니다. 사실 작년에 프로젝트를 할 때도 자주 사용되는 기능이고, 또 클론 코딩 등 공부를 하면서도 자주 사용했지만 이번에도 누군가 저에게 완벽하게 설명해보라 하면... 자신이 없어서 꼭 한 번 정리해보고 싶었습니다.
포스팅을 읽다보면 뒤의 내용이 조금씩 앞에 나올 수도 있지만 끝까지 쭉 읽어보면 이해가 될 겁니다! 그럼 Thread 부터 봅시다.
Thread (스레드)
Thread는 프로세스 내에서 “순차적으로 실행되는 실(실행 흐름)" 의 최소 단위이다.
안드로이드 앱에서는 메인 스레드는 메시지 큐 수신을 기다리는 루프를 실행하고, 사용자 입력, 시스템 이벤트, 화면 그리기 등의 메시지가 수신되면 각 메시지에 매핑된 핸들러의 메서드를 실행한다.
메인 스레드는 여러 개 존재할 수 있는 스레드 중 가장 최초의 스레드이다. 안드로이드의 메인 스레드는 앱의 UI 을 그리는 일을 담당한다.
만약 어떤 기능이 메인 스레드에 구현되었을 때 동작에 영향을 준다면 (예, 실행이 오래 걸리는 Room 작업, 네트워킹 작업 등) 그 기능은 별도의 스레드에서 수행되어야 한다.
🤔 ??? 저는 코딩을 할 때 메인 스레드를 지정해준 적이 없는데요?
걱정하지 마세요.ㅎ 처음으로 실행되는 메인 스레드는 안드로이드 프레임워크 상에서 미리 실행되서 초기에는 따로 작성할 필요가 없다. android.app.ActivityThread 클래스에서 main() 함수가 구현되어 있고 이 함수 내에서 메인 스레드를 실행해 줍니다.
Thread 클래스를 사용하여 새로운 스레드를 생성하고 실행하는 방법 두 가지
- Thread 클래스 상속한 서브 클래스
- 상속한 서브 클래스에서 run() 을 오버라이드
- Thread 클래스의 기능 확장이 필요한 경우
- task 의 세부적인 기능 수정과 추가에 장점이 있다.
- 클래스 상속에 따른 오버헤드가 발생한다.
- Runnable 인터페이스 구현한 클래스
- 그 클래스에서 run() 메서드 작성
- 단순히 run() 메서드만 구현하는 경우
- 논리적으로 구분된 task 설계에 장점이 있다.
- 인터페이스 구현이 간결하다.
대부분의 앱에서는 코드가 선형적으로 실행되고 끝나는 경우는 없다. 사용자의 입력이 필요한 앱에서는 그 입력 이벤트를 처리하기 위한 Loop 가 실행되어야 한다.
이게 무슨 소리냐면
내가 A 버튼을 누르면 aa 동작을 수행하도록 코드 블록('코드 블록 A' 라고 합시다.)를 작성했는데 만약 코드가 선형적으로 실행되고 끝난다면 '코드 블록 A' 를 지난 상황이라면 A 버튼을 눌러도 aa 동작을 수행하지 않을 것이다. 그래서 코드 블록 A 을 시스템이 읽기 위해서 다시 돌아갈 필요가 있다.
하지만 for 문이나 while 문처럼 무작정 루프를 돌리는 것이 아니다. 이는 리소스를 너무 많이 먹는다.
UI 프레임워크는 메시지큐를 사용하여 Loop 코드를 작성하도록 가이드한다. Message 는 문자열을 말하는 것이 아니라 시스템의 모든 이벤트를 전달할 때 사용하는 객체이다. 데이터, 데이터 타입, 기타 추가 정보를 가진다. Message Queue 는 FIFO 큐이다.
메시지가 메시지큐에 수신되면, 메시지가 담고 있는 내용에 따라 적절한 Handler 메서드가 호출된다.
시스템 이벤트를 처리하기 위한 Loop 는 Looper 클래스를 통해 실행된다. 어떤 루프를 실행하고 그 루프 안에서 메시지 큐로 전달되는 메시지가 있는지 검사한다. 새 메시지가 있다면 해당 메시지를 처리할 Handler 메서드를 실행한다.
안드로이드에서 핸들러는 메시지를 보내고, 메시지를 수신했을 때 처리하는 역할을 한다. (통상적인 개념에서는 메시지 수신 시 그 처리를 담당하는 역할만 수행)
아래 그림을 보면 이해하기 쉬울 것이다.
- post (핸들러가 시스템 이벤트를 포착)
- 핸들러가 sendMessage (Looper 에게 메시지 보내기)
- Looper 가 큐에 추가
- Looper 가 MessageQueue 에서 FIFO 로 메시지 꺼내기
- Handler 가 꺼낸 메시지를 처리함.
스레드 통신, Handler (핸들러)
기본적으로 스레드는 다른 스레드가 하는 일에 관심이 없다.
그래서 만약 스레드끼리의 정보가 주고 받아야 하는 상황이라면, 스레드 간 통신을 해야 한다. 이는 여러 방법이 있지만 가장 자주 사용되고 쉽게 사용되는 것이 Handler 이다.
위 그림에서 핸들러가 post(시스템 이벤트 포착)한다. 이 때 시스템 이벤트는 다른 스레드로부터 받는 메시지일 수 있다.
예를 들어 2번 스레드의 Handler 가 메인 스레드의 handler 에게 sendMessage 할 수 있다. 그렇다면 위에서의 1 ~ 5번의 절차에 걸쳐 처리되는 것이다. 이것이 스레드 간의 통신이다.
아래와 같은 방식으로 사용하면 된다.
class MyHandler : Handler() {
companion object {
const val TAG = "MyHandler"
const val MSG_DO_SOMETHING1 = 1
const val MSG_DO_SOMETHING2 = 2
const val MSG_DO_SOMETHING3 = 3
const val MSG_DO_SOMETHING4 = 4
}
override fun handleMessage(msg: Message) {
when (msg.what) {
MSG_DO_SOMETHING1 -> {
Log.d(TAG, "Do something1")
}
MSG_DO_SOMETHING2 -> {
Log.d(TAG, "Do something2")
}
MSG_DO_SOMETHING3 -> {
Log.d(TAG, "Do something3")
}
MSG_DO_SOMETHING4 -> {
Log.d(TAG, "Do something4, arg1: ${msg.arg1}," +
" arg2: ${msg.arg2}, obj: ${msg.obj}")
}
}
}
}
// 1
val handler: Handler = MyHandler()
// 2
val msg: Message = handler.obtainMessage(MyHandler.MSG_DO_SOMETHING1)
handler.handleMessage(msg)
// 3
handler.sendEmptyMessage(MyHandler.MSG_DO_SOMETHING2)
// 4
val msg2 = Message.obtain(handler, MyHandler.MSG_DO_SOMETHING3)
handler.handleMessage(msg2)
MyHandler 클래스를 생성하여 메시지에 따른 기능을 handleMessage 메서드를 오버라이드 하여 작성한다.
그리고 핸들러 객체를 만들어서 메시지에 따른 이벤트 처리를 하도록 해준다.
코드 출처: https://codechacha.com/ko/android-handler-basic/
아래 안드로이드 공식 문서에서 여러 메서드를 확인할 수 있다.
https://developer.android.com/reference/android/os/Handler
핸들러 Myth (오해)
🫢 스레드 통신은 본인 혼자서도 할 수 있다.
예를 들어 스레드 A 는 메시지를 같은 스레드인 스레드 A 로 보낼 수 있다.
외부 스레드에서 전달되는 메시지 처리를 위해 구현한 기능을 재사용하거나, 순차적으로 실행되는 코드 사이에서 시스템 이벤트가 고려되어야 할 때
🫢 스레드 내에서 여러 핸들러가 있을 수 있다.
오히려 스레드 내의 핸들러가 메시지의 종료에 따라 여러 개가 있는 것이 더 나을 때가 많다.
Runnable
핸들러로 메시지를 전달하는 방법보다 Runnable 객체를 사용하는 방법이 더 간단하게 스레드 통신을 할 수 있을 때가 존재한다.
공식 문서에 따르면 핸들러는 메시지만 보낼 수 있는 것이 아니라 Runnable 객체도 보낼 수 있다고 한다.
메시지를 사용하면 번거로운 절차가 필요하다.
- 메시지에 저장된 데이터 타입을 식별해야 함. 그것을 상수로 정의함.
- 그 상수에 따른 조건문을 실행해야 함.
- 메시지를 보낼 때 별도의 메시지 객체를 구성해야 함. 등,,,
하지만 Runnable 은 메시지에 데이터를 담아서 보내는 형식이 아닌 “실행 코드”를 넣어서 바로 보내고 그 코드를 Runnable 을 받은 스레드에서 직접 실행하는 형식이다.
즉, Runnable 은 실행 코드가 담긴 객체이다!
- Runnable 객체를 핸들러로 보내기 위해서는 수신 스레드에서 핸들러 객체를 생성한다.
private val handler = Handler(Looper.getMainLooper()) // MainThread 에 들어이있는 Handler.
위 코드는 수신 스레드가 MainThread 인 것이다.
- Runnable 객체를 송신 스레드에서 만들고 run() 메서드를 오버라이드한다.
- Runnable 인터페이스를 구현하여
- 그렇게 만든 클래스를 Thread 인스턴스에 전달
- 생성된 스레드 t 를 start()로 실행한다.
class RunnableExam : Runnable {
override fun run() {
// 실행하려는 코드
}
}
var t = Thread(RunnableExam())
t.start()
그렇다면 이러한 질문을 가질 수 있다. (내가 그렇게 알고 있었듯이)
🤔 Runnable 인터페이스가 새로운 Thread 를 실행할 때만 사용되는 군요? 그럼 Thread 클래스처럼 스레드 실행 코드가 구현되어 있는 건가요?
정확하게 둘 다 틀린 말이다.
명확히 알아야 하는 것은 새로운 스레드를 실행하는 역할은 Thread 클래스의 역할이다. Runnable 은 단지 새로운 스레드에서 실행될 run() 메서드를 가지는 인터페이스이다.
🤔 그렇다면…. 스레드를 사용하지 않을 때 Runnable 은 언제 사용하지>>?
Runnable 객체는 어디서든 사용될 수 있다.
코드 실행이 필요한 곳이면 어디서든 Runnable 인터페이스를 상속받아서 run() 메서드를 작성한 다음, 해당 객체를 전달해 run() 을 실행할 수 있다.
예를 들어서 메모장 앱에서 글을 작성하는데 몇 개의 단어를 작성할 때마다 작성한 내용들을 자동으로 저장하려고 한다고 하면, 저장하는 기능을 수행할 때는 UI의 EditText의 Text 가 바뀌었을 때 즉, addTextChangedListener 로 구현될 것이다.
이러한 경우 작성된 글을 저장하는 runnable 을 만들고 handler 를 새로 만들어서 그 handler 에서 구현한 runnable 을 실행하는 것이 예시가 될 수 있다.
(위에서 설명했듯이 하나의 스레드에서도 시스템 이벤트의 종류에 따라 여러 handler 를 만들 수 있다.!)
runOnUiThread
Runs the specified action on the UI thread. If the current thread is the UI thread, then the action is executed immediately. If the current thread is not the UI thread, the action is posted to the event queue of the UI thread.
위는 공식문서에서 runOnUiThread 을 설명하는 글이다.
위에서 설명했듯이 스레드를 새로 생성해서 어떤 작업을 수행하게 할 때는 메인 스레드에서 하기 어려운 작업을 수행시킬 떄 주로 사용한다.
runnable 을 통해 쓰레드를 구현하고 작업을 수행한 후, 작업한 내용을 UI 에 작동시키는 작업은 스레드 통신에서 매우 자주 사용되는 것 중 하나이다. 이것을 구현하게 위해서는 Runnable 객체 안에서 심플하게 runOnUiThread 라는 메서드를 사용하면 된다.
public final void runOnUiThread(Runnable action) {
if (Thread.currentThread() != mUiThread) {
mHandler.post(action);
} else {
action.run();
}
}
위 코드는 Activity.java 라는 파일에 이미 정의되어 잇는 메서드이다.
이는
- 메서드가 새로운 스레드에서 실행이 된다면 (현재 스래드가 메인 스레드가 아니면)
- 핸들러의 post() 메서드를 통해 runnable 객체를 전달하고
- 만약 그것이 아니라면 (메인 스레드 자신이 runnable 객체를 자신에게 보낸 것일 수 있으니까)
- runnable 객체를 현재 스레드에서 실행한다.
위에 있는 글을 모두 잘 읽어보았으면 이제는 이미 구현되어 있는 runOnUiThread 가 이해가 될 것이다!!!!!
사용 방법은 아래와 같다.
Thread(Runnable{
/* 다른 스레드에서 수행할 작업 */
// 현재 이 스레드는 메인 스레드가 아니다. 메인 스레드로 전환을 해주어야 함.
runOnUiThread{
/* 메인 스레드에서 수행할 작업. */
}
}
}).start()
이로서 안드로이드에서의 Thread, Handler, Runnable 에 대해 알아 보았다. 만약 이 포스팅을 읽으신다면 이 포스팅을 다 읽고 나서 안드로이드 공식 문서를 읽으시면 더 도움이 될 겁니다!! 혹은 그 반대도 그렇구요!
물론 모든 개념을 마스터한 것은 아니겠지… 왜냐하면 특히 Thread 는 대학교 컴퓨터구조 강의나 운영체제 강의 때 공부했던 것처럼 포스팅 한 개, 책 한권으로 마스터할 수 있는 것은 아니니까..
https://proandroiddev.com/decoding-handler-and-looper-in-android-d4f3f2449513
https://recipes4dev.tistory.com/143#32-안드로이드-메인-ui-스레드
https://recipes4dev.tistory.com/166
https://developer.android.com/reference/android/os/Handler
'Android > Theory' 카테고리의 다른 글
MVC, MVP, MVVM 봐도 봐도 조금씩 헷갈리면 모르는 거임 (1) | 2023.10.02 |
---|---|
Android Room, SQLite 기본 (0) | 2022.09.06 |
Android SharedPreference (0) | 2022.09.05 |
Intent는 택배 상자! (1) | 2022.08.28 |
Activity LifeCycle (액티비티 생명주기) (2) | 2022.08.27 |