안드로이드 스레드, 핸들러

PEPPERMINT100·2021년 1월 7일
2
post-thumbnail

서론

작년 운영체제 수업에서 프로세스와 스레드에 대해 배웠다. 굉장히 이론적이고 추상적으로 알고있었고 javascript로 코딩을 할 때에는 스레드에 대해 크게 신경 쓸 일이 없어서(분명 있겠지만 잘 알지 못해서) 넘어갔었는데, 안드로이드 os에서는 이 스레드에 대한 처리가 실제로도 아주 중요하다고 해서 알아보는 시간을 가져보려고 한다.

간단하게 프로세스와 스레드에 대한 지식이 필요한데, 프로세스는 어떤 프로그램이 시작되면 그 실행을 프로세스라고 하고 그 안에서 스레드가 실행되어 여러개의 작업을 동시에 할 수 있도록 한다.

일반적으로 이렇게 "하나의 프로세스 안에 스레드가 실처럼 실행된다." 라고 많이들 표현한다. 이외에도 멀티 프로세싱과 멀티 스레딩의 공유 자원에 의한 차이나 처리방식 등 컴퓨터 공학적인 부분은 파고 들면 정말 깊지만 일단 여기서는 하나의 프로그램이 실행되면 프로세스를 생성하고 그 안에 스레드를 여러 개 생성하여 여러 작업을 동시에 처리한다는 사실 정도만 알면 되겠다.

안드로이드의 스레드

안드로이드 os에서 스레드는 2가지로 나뉜다. 하나는 메인 스레드(UI 스레드) 그리고 작업 스레드(Worker 스레드)가 있다. 메인 스레드는 말 그대로 주된 작업을 진행하는 스레드이고 UI 작업을 수행할 수 있는 스레드이다. 그리고 나머지 다른 스레드들을 전부 작업 스레드라고한다.

왜 이렇게 스레드의 종류를 둘로 나누었을까? 아래 코드를 한 번 보자.

val firstButton = findViewById<Button>(R.id.first_button)
val text = findViewById<TextView>(R.id.text)

var i = 0
firstButton.setOnClickListener{
	for(i in 1..100000000L){
        text.text = "${i}번째 text"
   }
}

레이아웃에서 버튼을 가져오고 버튼을 누르면 i를 하나씩 증가시키며 그 값을 버튼에 넣으려고 한다. 그런데 이렇게 코딩된 안드로이드 에뮬레이터를 설정하고 버튼을 누르면 바로 어플리케이션이 가버리는 것을 볼 수 있다.

이유는 무엇일까? 이유는 메인 스레드는 하나의 실처럼 생겨서 요청을 하나 둘 순서대로 처리하는데, 저 긴 for문을 연산하느라 아무런 작업도 진행할 수 없는 상태가 되었기 때문이다.

하지만 안드로이드 개발을 하다보면 저런 작업을 해야할 일이 생길 것이다. 위 코드 정도는 아니겠지만 작업을 돌리며 UI 작업을 동시에 수행해야 할 일이 생길 것이다. 그래서 안드로이드에서는 작업 스레드라는 것을 생성하여 따로 연산을 해주어야 한다.

즉 어떤 오래걸리는 반복 작업은 메인 스레드에서 하는 것이 아닌 작업 스레드를 따로 생성해서 해주어야 한다는 것이다.

class MainActivity : AppCompatActivity() {
    private lateinit var myText: TextView
    private lateinit var startButton: Button

    private lateinit var myThread: MyThread

    private val TAG: String = "로그"
    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        Log.d(TAG, "MainActivity - onCreate: ");

        startButton = findViewById(R.id.start_button)
        myText = findViewById(R.id.my_text)

        myThread = MyThread()

        startButton.setOnClickListener{
            Log.d(TAG, "MainActivity - onCreate: 스타트 버튼 클릭");
            myThread.start()
            myText.text="world hello"
        }
    }

    inner class MyThread: Thread(){
        override fun run() {
            for(i in 1..100000000L){
                Log.d(TAG, "MyThread - run: i : $i ");
                SystemClock.sleep(1000)
            }
        }
    }
}

이런식으로 작업 스레드를 하나 생성하여 for문을 돌리고 또 text를 지정해주면 메인 스레드는 방해 받는 것 없이 text 값을 변화시켜줄 수 있게 되는것이다. 그런데 만약 myText.text="world hello" 부분을 myThreadrun 메소드 안에 넣으면 바로 어플리케이션이 가버린다. 왜냐하면 UI 연산은 메인 스레드에서만 진행하도록 약속이 되어 있기 때문이다.

UI 관련 작업은 메인 스레드(UI 스레드)에서만 가능하다. 라는 개념을 잘 알고 있어야 한다.

그렇다면 왜 UI 관련 작업은 메인 스레드에서만 가능할까? 만약 하나의 버튼이 있고 그 버튼 안의 text 값을 메인 스레드에서도 지정하고 다른 작업 스레드에서도 지정한다고 하자.

먼저 UI 스레드에서 어떤 버튼의 text 값을 버튼1이라고 한다고 하고, 정말 어쩌다가 동시에 다른 작업 스레드에서 같은 버튼의 text 값을 버튼2로 정한다고 하자. 이러한 충돌이 생기면 당연히 어플리케이션에 문제가 생기기 때문에 안드로이드에서 UI 관련 작업은 메인 스레드에서만 할 수 있도록 해놓았다. (이렇게 하면 하나의 스레드에서 순서대로 UI 처리를 할 수 있게 될 것이다.)

번외로 자바스크립트는 싱글 스레드 기반이라 setTimeout과 같은 연산이 생기면 알아서 이 작업을 백그라운드에 넣어놓고 이벤트 루프가 돌며 처리를 해주었지만 안드로이드, kotlin, java에서는 개발자가 직접 스레드를 생성하고 핸들링 해주어야 한다. 이러한 작업을 어떤식으로 하는지 알아보도록 하자.

핸들러

일단 지금까지 배운 것들 중 확실히 해두어야 할 부분이 있다. 하나는 스레드라는 것은 컴퓨터 공학, 개발 분야에서 공통되는 내용이지만 UI 스레드와 작업 스레드를 나누는 것, 그리고 지금 배울 핸들러는 안드로이드에만 있는 개념이다. 핸들러는 작업 스레드에서 UI 작업을 할 수 있도록 도와준다.

class MainActivity : AppCompatActivity() {
    private lateinit var myText: TextView
    private lateinit var startButton: Button
    private lateinit var stopButton: Button

    private lateinit var myThread: MyThread
    private lateinit var myHandler: MyHandler

    private var isRunning: Boolean = false

    private val TAG: String = "로그"
    override fun onCreate(savedInstanceState: Bundle?) {

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        Log.d(TAG, "MainActivity - onCreate: ");

        startButton = findViewById(R.id.start_button)
        stopButton = findViewById(R.id.stop_button)
        myText = findViewById(R.id.my_text)


        isRunning = true

        myThread = MyThread()
        myHandler = MyHandler()

        startButton.setOnClickListener{
            Log.d(TAG, "MainActivity - onCreate: 스타트 버튼 클릭");
            myThread.start()
            isRunning = true
        }

        stopButton.setOnClickListener {
            isRunning = false
        }
    }

    inner class MyThread: Thread(){
        override fun run() {
            while(isRunning){
                SystemClock.sleep(1000)
                myHandler.sendEmptyMessage(0)
            }
        }
    }

    inner class MyHandler: Handler(){
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            Log.d(TAG, "MyHandler - handleMessage: msg : $msg ");
            var now = System.currentTimeMillis()
            myText.text = "now : $now"
        }
    }
}

코드를 조금씩 뜯어보자면

    private var isRunning: Boolean = false
    startButton.setOnClickListener{
    	Log.d(TAG, "MainActivity - onCreate: 스타트 버튼 클릭");
   	 	myThread.start()
    	isRunning = true
    }

    stopButton.setOnClickListener {
		isRunning = false
    }

위 코드를 통해 isRunning으로 스레드를 돌릴지 말지를 정한다. 그리고 그 데이터 변경을 startButtonstopButton의 클릭 리스너에 등록하여 변경해줄 수 있다. startButton을 누르면 스레드를 실행할 것이고, stopButton을 누르면 스레드의 실행을 멈춘다.

inner class MyThread: Thread(){
        override fun run() {
            while(isRunning){
                SystemClock.sleep(1000)
                myHandler.sendEmptyMessage(0)
            }
        }
    }

위 코드는 isRunningtrue이면 즉 startButton을 누르면 현재 시간을 계산하고 1초의 시간을 기다렸다가 myHandler에 메시지를 보낸다. 지금 이 sendEmptyMessage는 스레드에서 핸들러를 깨우는 역할만 하게 된다.

 inner class MyHandler: Handler(){
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            Log.d(TAG, "MyHandler - handleMessage: msg : $msg ");
            var now = System.currentTimeMillis()
            myText.text = "now : $now"
        }
    }

그리고 대망의 핸들러이다. 핸들러는 스레드에서 메시지를 받아 UI 작업을 하게 된다. 빌드를 하고 에뮬레이터에서 버튼을 누르면 이제 1초마다 text가 변하는 것을 확인할 수 있다. 즉 작업 스레드에서 핸들러를 통해 UI 작업을 하도록 한 것이다.

여기서 조금 더 나아가면 핸들러는 now 값을 계산하는 연산이 아닌 UI 작업만 하도록 분리하는 것이 좋다 그런 경우엔

    inner class MyThread: Thread(){
        override fun run() {
            while(isRunning){
                var now = System.currentTimeMillis()
                Log.d(TAG, "MyThread - run: now : $now ");
                SystemClock.sleep(1000)
                var msg = Message()
                msg.what = 0
                msg.obj = now
                myHandler.sendMessage(msg)
            }
        }
    }

    inner class MyHandler: Handler(){
        override fun handleMessage(msg: Message) {
            super.handleMessage(msg)
            myText.text = "now : ${msg.obj}"
        }
    }

위 처럼 스레드와 핸들러 코드를 변경해주면 된다. Message 객체로 msg 변수를 생성하고 안에 .what.obj를 정해준다. obj는 핸들러에 보내고 싶은 데이터, 즉 연산된 now를 보내고 what은 일반적으로 when 문법을 통해 obj 어떻게 처리할지 나눌 때 사용한다고 한다.

그런데 만약 startButton을 한번 더 누르면 앱이 죽는 것을 확인 할 수 있는데, 이는 스레드의 생명주기와 관련이 있다.

다음에 다뤄볼 메시지 큐와 루퍼의 개념도 사진에 있는데 일단 스레드는

스레드 생성 -> 스레드 실행 -> 스레드 종료(소멸)

의 단계를 따르는데, 스타트 버튼을 누르는 순간 myThread 객체가 생성이 되었기 때문에 start 단계를 지나게 된 것이다. 그래서 다시 버튼을 누르면 start 메소드를 실행하는 모순적인 상황이 발생하게 된다. 그러므로

     startButton.setOnClickListener{
            myThread = MyThread()
            Log.d(TAG, "MainActivity - onCreate: 스타트 버튼 클릭");
            myThread.start()
        }

필요에 따라서 리스너 안에 스레드를 계속 생성하게 하면 앱의 종료 없이 여러 개의 스레드를 생성하게 된다.

결론

안드로이드에서 중요한 메인 스레드, 작업 스레드, 핸들러, 루퍼 개념을 익히기 위해 구글링을 열심히 해보았지만 이해하기 어려운 부분이 많아 유튜브에 잘 설명된 영상을 보고 핸들러의 기본부터 배워보았다. 운영체제 수업을 들었을 때 이론적으로만 배워서 와닿지 못한 부분이 많았는데, 이렇게 실무적인 부분이 적용되는 것을 발견하니 전공지식을 배운 시간에 대한 의미를 조금이나마 찾을 수 있었다. 하지만 핸들러를 선언할 때마다 이미 deprecated 되었다는 문구가 자꾸 뜨는데, 역시 안드로이드 분야도 정말 빠른 속도로 바뀌는 분야라는 것을 알게 되었다.

profile
기억하기 위해 혹은 잊어버리기 위해 글을 씁니다.

0개의 댓글