본문 바로가기

안드로이드/[Kotlin]

[Kotlin] Worker Thread(작업 스레드) & Handler를 활용한 UI 처리

 Thread(스레드) 란 프로세스 내에서 "순차적으로 실행되는 실행 흐름" 의 최소 단위를 말한다. 
그 중에서 하나의 프로그램에서 main() 함수로부터 최초로 시작되는 실행 흐름을 Main Thread(메인 스레드) 라고 부른다.

 초기에는 이 Main Thread(메인 스레드) 로 부터 다른 스레드들이 필요에 따라 만들어 지고 실행된다. 그리고 이렇게 만들어 진 스레드들 또한 다른 스레드들을 만들고 실행할 수 있다. 추가적으로 생성된 스레드 들을 Worker Thread(작업 스레드) 라고 부른다.

 

 


Main Thread

UI 작업이 이루어지는 스레드이다. 앱은 일반적으로 프로세스 하나 위에 여러 멀티 스레드가 돌고 있는 형태이지만, UI 작업을 멀티 스레드로 해 버리면 동기화 문제가 발생한다.  따라서 안드로이드에서 UI 작업은 오직 메인스레드에서만 가능하다.

메인스레드에서는 사용자 입력 이벤트를 처리하기 위한 루퍼(Looper) 에서 큐(Queue) 방식으로 이벤트를 발생 순서대로 전달받아 처리한다. 자세한 처리 방식은 아래에서 또 설명 하도록 하겠다.

 

 


Worker Thread

위에서 말했다시피 안드로이드의 UI 작업은 기본적으로 메인스레드를 주축으로 하는 싱글 스레드 모델로 동작한다. 따라서 메인 스레드에서는 시간이 오래 걸리는 작업을 피해야 한다.

이를 위해 워커 스레드에서는 시간이 오래 걸리는 작업을 실행하고 Handler, runOnUiThread 등을 통해 UI에 접근해서 데이터를 전달하고 UI를 변경한다.

 

 


 

 

 

 

MessageQueue

Message 객체나 runnable 객체와 같이 전달하고자 하는 데이터를 저장하는 Message객체를 큐(Queue) 형태로 관리하는 자료구조이다.

앱의 메인스레드에서 기본적으로 사용되고 있지만, 개발자가 Message Queue 객체를 직접 참조해서 메시지를 전달하거나, 가져와서 처리하는 등의 과정은 하지 않고, 메시지 전달은 Handler 를 통해서, 그리고 큐로부터 메시지를 꺼내고 처리하는 역할은 Looper 가 수행한다.

 

 

 


Handler

스레드의 Looper 와 연결된 메시지 큐로 메시지를 보내고 처리할 수 있게 해 준다.

사용은 아래와 같은 방식으로 할 수 있다.

  • Message 객체 전달은 Handler(Looper.getMainLooper()).sendMessage(msg)
  • Runnable 객체 전달은 Handler(Looper.getMainLooper()).post(run : runnable)
inner class WorkerThread : Thread() { 
    override fun run() { 		
        Handler(Looper.getMainLooper()).post{
            ... 		
        } 	
    } 
}

뭐 위와같은 식으로 Handler를 활용한다. 

 

 


Looper

무한하게 돌면서 Message Queue 에 들어있는 Message 객체Runnable 객체를 차례대로 가져와서 그와 연결된 Handler 를 호출하는 역할을 담당한다.

개발자들은 메인스레드로 전달할 Messsage 객체나 Runnable 객체를 구성하고, 스레드의 Message Queue에 연결된 Handler 를 통해 해당 객체를 보내면 된다.

 

 

 


아래 예시 코드를 보고 설명을 이어가겠다.

//SongActivity.kt

class SongActivity : AppCompatActivity() {
    private lateinit var player : Player
    
    override fun onCreate(savedInstanceState: Bundle?) {
        player = Player() //Player 클래스의 인스턴스 생성. 
                          //한마디로 쓰레드를 실행하기 위한 인스턴스 생성.
        player.start()  //start 메소드로 쓰레드 시작!
    }
    
    inner class Player : Thread() { 
    // 쓰레드를 상속받음으로써 Player 클래스를 워커스레드로 활용 할 계획.
        override fun run() {
        //위에서 Player 클래스의 인스턴스(player)를 만들고,
        //player.start() 를 실행하면 쓰레드가 실행되면서 run() 함수가 실행된다.
        //그리고 run 안의 코드가 끝나야 스레드가 종료된다.
            try {
                while (true) {
                    if (song.currentTime >= song.totalTime) { //노래가 끝나면 run 함수가 종료되면서 쓰레드도 종료!
                        break
                    }
                    if (song.isPlaying) { //노래가 실행 중일때만 쓰레드 실행. (seekbar 진행 + 시간초 증가)
                        Log.d("player", "player 쓰레드 잘 실행 중이다!")
                        sleep(1000)
                        song.currentTime++
                        //<1번방법 : Handler>
                        Handler(Looper.getMainLooper()).post {
                            binding.playerSongSeekbar.progress = song.currentTime*1000/song.totalTime
                            binding.playerTimeStartTv.text
                            = String.format("%02d:%02d", song.currentTime/60, song.currentTime%60)
                        }

//                        <2번방법>
//                        runOnUiThread {
//                            binding.playerSongSeekbar.progress = song.currentTime*1000/song.totalTime
//                            binding.playerTimeStartTv.text 
//                            = String.format("%02d:%02d", song.currentTime/60, song.currentTime%60)
//                        }
                    }
                }
            } catch (e : InterruptedException) { 
            // try 구문 안에서  catch(...) 의 ... 라는 오류가 나면 앱이 오류가 나는 대신에 catch 구문을 실행한다.
                Log.d("interrupt", "Player 쓰레드 정상 종료!")
            }
        }
    }
    override fun onDestroy() { // 화면(SongActivity) 이 꺼질때 onDestroy 함수가 호출이 된다.
        player.interrupt() //오류를 내서 쓰레드를 종료 시켜 버리는 함수.
        super.onDestroy()
        Log.d("destroy", "쓰레드 종료 + 화면꺼짐")
    }
}

 

 

 

 

우선 Thread() 를 상속받는 Player 라는 클래스 만들어서 Worker Thread 로 사용할 것이다.

 

그리고 Player 클래스의 인스턴스(player) 생성 후에 start() 함수를 사용해서 스레드를 실행한다.

 

스레드를 만들고 실행하는거 보다 더 중요한것이 스레드를 종료시키는 것이다.

 

start() 함수를 실행하게 되면 스레드가 실행되면서 Thread() 에 내장된 run() 함수가 실행되는데, 이 run() 내부의 코드가 끝나야 스레드가 종료된다. 

 

try, catch 구문을 통해 오류를 컨트롤 함과 동시에, Activity가 finish 될때 호출되는 onDestroy() 함수 내부에서 interrupt() 함수를 통해 스레드에 오류를 내서 스레드를 종료시킬 수 있다.  종료는 필수!!!

 

 

 

 

 

 

++ 추가적으로 위 그림에서 노래 플레이 시간을 나타내주는 바를 SeekBar 라고 한다.

나는 위에서 만들어 준 Player 라는 워커 스레드를 통해서 'SongActivity 화면의 시간초와 SeekBar 상태변화' 라는 UI 작업을 Handler 를 통해 UI 작업을 한 거다.

 

다음 글에서 SeekBar에 대한 간단한 설명을 하겠다.

 

 

 

 

 

 

아래 블로그에서 공부를 많이 했다. 엄청 정리 잘 되어있다..!!!

Thread 관련 정리 : https://recipes4dev.tistory.com/143