[Android] 10(1). 스레드와 루퍼

5 minute read


스레드와 루퍼

안드로이드의 스레드는 크게 1개만 존재하는 메인 스레드와 여러 개가 존재할 수 있는 백그라운드 스레드로 나눌 수 있습니다.


메인 스레드(UI 스레드)


안드로이드 시스템은 새로운 앱을 시작하면 새로운 리눅스 프로세스를 시작합니다. 기본적으로 메인 액티비티를 비롯한 모든 컴포넌트는 단일 프로세스 및 메인 스레드에서 실행됩니다.

안드로이드의 메인 스레드는 다음과 같은 특징과 제약 사항이 있습니다.

  • 화면의 UI를 그리는 처리를 담당합니다.
  • 안드로이드 UI 툴킷의 구성 요소(android.widget.android.view…)와 상호작용하고, UI 이벤트를 사용자에게 응답하는 스레드입니다.
  • UI 이벤트 및 작업에 대해 수 초 내에 응답하지 않으면 안드로이드 시스템은 ANR(Application Not Responding, 응용 프로그램이 응답하지 않음) 팝업창을 표시합니다. 따라서 시간이 오래 걸리는 코드는 새로운 스레드를 생성해서 처리해야 합니다.

이러한 제약 사항의 목적은 지속적이고 유연한 사용자 환경의 제공입니다.



백그라운드 스레드


안드로이드는 네트워크 작업, 파일 업로드와 다운로드, 이미지 처리, 데이터 로딩 등 메모리 이외의 다른 곳에서 데이터를 가져오는 작업을 백그라운드 스레드에서 처리할 것을 권장합니다.

백그라운드 스레드를 생성하는 방법은 다음과 같습니다.


Thread 객체

Thread 클래스를 상속받아 스레드를 생성할 수 있습니다.

1. Thread를 상속받는 클래스 정의

Thread 클래스를 상속받는 WorkerThread 클래스를 정의합니다.

class WorkerThread: Thread(){

}


2. run( ) 메서드 오버라이드

스레드가 처리할 로직을 정의하는 run( ) 메서드를 오버라이드합니다. 여기서는 간단히 변수 i의 값이 10이 될 때까지 반복하여 로그캣 창에 출력하는 코드를 작성합니다.

class WorkerThread: Thread(){
    override fun run(){
        var i = 0
        while (i < 10){
            i += 1
            Log.i("WorkerThread", "$i")
        }
    }
}


3. 커스텀 스레드 인스턴스를 생성하고 start( ) 메서드 호출

WorkerThread 객체를 생성해 별도의 스레드를 start( ) 메서드를 호출하면 run( ) 메서드에 정의된 로직을 생성된 스레드가 처리합니다.

override fun onCreate(savedInstanceState: Bundle?){
    super.onCreate(savedInstanceState)
    setContentView(binding.root)
  
    var thread = WorkerThread()
    thread.start()
}


Runnable 인터페이스

Runnable 인터페이스를 구현해 스레드를 생성할 수 있습니다. Runnable 인터페이스는 다중 상속을 허용하지 않는 코틀린 언어의 특성상 상속 관계에 있는 클래스도 구현할 수 있도록 지원하는 모델입니다.

1. Runnable 인터페이스를 구현하는 클래스 정의

Runnable 인터페이스를 구현하는 WorkerRunnable 클래스를 정의합니다.

class WorkerRunnable: Runnable{
  
}


2. run( ) 메서드 오버라이드

Thread 클래스와 마찬가지로 Runnable 인터페이스도 스레드가 처리할 로직을 정의하는 run( ) 메서드를 오버라이드합니다

class WorkerRunnable: Runnable{
    override fun run(){
        var i = 0
        while (i < 10){
            i += 1
            Log.i("WorkerThread", "$i")
        }
    }
}


3. Runnable 객체를 Thread 클래스의 생성자로 전달하고 start( ) 메서드 호출

Thread를 상속받은 객체와 달리 Runnable 인터페이스를 구현한 객체는 Thread 클래스의 생성자로 전달하고 Thread 클래스의 start( ) 메서드를 호출해야 스레드가 생성됩니다.

override fun onCreate(savedInstanceState: Bundle?){
    super.onCreate(savedInstanceState)
    setContentView(binding.root)
  
    var thread = Thread(WorkerRunnable())
    thread.start()
}


람다식으로 Runnable 익명객체 구현

인터페이스 내부에 메서드가 하나만 있는 경우는 람다식으로 변환이 가능합니다. Runnable 인터페이스를 이용한 스레드는 람다식으로 변환이 가능합니다.

Thread{
    var i = 0
    while (i < 10){
        i += 1
        Log.i("LambdaThread", "$i")
    }
}.start()


코틀린에서 제공하는 thread() 구현

코틀린에서는 다음과 같이 백그라운드를 사용할 수 있습니다. thread() 안에 파라미터로 start=true를 전달하면 thread() 안의 코드 블록이 실행됩니다.

thread(start=true){
    var i = 0
    while (i < 10){
        i += 1
        Log.i("LambdaThread", "$i")
    }
}



메인 스레드와 백그라운드 스레드


백그라운드 스레드로 메인 스레드에 집중될 수 있는 코드를 분산함으뢰써 더 효율적인 앱을 만들 수 있습니다.

하지만 주의할 점이 하나 있는데, 안드로이드에는 ‘백그라운드 스레드는 UI 구성 요소에 접근하면 안 된다’라는 중요한 규칙이 있습니다.


예를 들어 아래와 같은 코드는 FATAL EXCEPTION 예외를 발생시키고 앱이 종료됩니다.

override fun onCreate(savedInstanceState: Bundle?){
    super.onCreate(savedInstanceState)
    setContentView(binding.root)
  
    Thread{
        var i = 0
        while (i < 10){
            binding.textView.text = "$i" // 백그라운드 스레드는 UI 구성 요소에 접근하면 안 됨
            i += 1
            Thread.sleep(1000)
        }
    }.start()
}
Only the original thread that created a view hierarchy can touch its views.


메인 스레드 이외의 스레드는 UI를 업데이트할 수 없습니다. 이 제약 사항은 윈도우 프로그램이나 iOS 앱과 같은 다른 애플리케이션에도 공통으로 적용되는 사항입니다.



핸들러와 루퍼


안드로이드는 메인 스레드와 백그라운드 스레드 및 스레드 간의 통신을 위해 ‘핸들러’와 ‘루퍼’를 제공합니다.

핸들러와 루퍼의 작동 원리는 다음과 같습니다.

  1. 메인 스레드는 내부적으로 루퍼를 가지며 루퍼는 Message Queue 를 포함합니다.
  2. Message Queue는 다른 스레드 혹은 스레드 자기 자신으로부터 전달받은 메시지를 보관하는 Queue입니다.
  3. 루퍼는 Message Queue에서 메시지, Runnable 객체를 차례로 꺼내서 핸들러가 처리하도록 전달합니다.
  4. 핸들러는 루퍼로부터 받은 메시지, Runnable 객체를 처리하거나 메시지를 받아서 Message Queue에 넣는 스레드 간의 통신 장치입니다.

image-20210822154145546


루퍼

루퍼^Looper^는 MainActivity가 실행됨과 동시에 for문 하나가 무한루프를 돌고 있는 서브 스레드라고 생각하면 됩니다. 이 무한루프는 대기하고 있다가 자신의 큐에 쌓인 메시지를 핸들러에 전달합니다.

여러 개의 백그라운드에서 큐에 메시지를 입력하면, 입력된 순서대로 하나씩 꺼내서 핸들러에 전달합니다.


핸들러

핸들러^Handler^는 루퍼가 있는 메인 스레드(MainActivity)에서 주로 사용되며 새로 생성된 스레드들과 메인 스레드와의 통신을 담당합니다. 핸들러는 루퍼를 통해 전달되는 메시지를 받아서 처리하는 일종의 명령어 처리기로 사용됩니다.

루퍼는 앱이 실행되면 자동으로 하나 생성되어 무한루프를 돌지만, 핸들러는 개발자가 직접 생성해서 사용해야 합니다.


메시지

메시지^Message^는 루퍼의 큐에 값을 전달하기 위해서 사용되는 클래스입니다. 메시지 객체에 미리 정의해둔 코드를 입력하고 큐에 담아두면 루퍼가 꺼내서 핸들러에 전달합니다.



타이머 앱 만들기


앞에서의 내용을 바탕으로 Timer 프로젝트를 작성해보겠습니다.


activity_main.xml

메인 UI는 다음과 같이 구성합니다.

image-20210822160611367

  • 텍스트뷰: id=textTimer, text=”00:00”, textSize=56sp, gravity=center
  • 버튼 1: id=btnStart, text=”START”
  • 버튼 2: id=btnStop, text=”STOP”


MainActivity.kt

MainActivity.kt 파일에서 핸들러를 다루는 코드를 작성합니다.

binding을 연결하고 시작합니다.

1. 변수 선언

전체 시간을 저장하는 total과 시작 여부를 나타내는 started를 선언합니다. 그리고 total에는 처음 시작값으로 ‘0’초를, started는 시작되지 않았으므로 ‘false’를 입력합니다.

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        // 변수 선언
        var total = 0        // 전체 시간
        var started = false  // 시작 여부
      
    }


2. Handler 구현

이제 total과 started를 이용해서 화면에 시간값을 출력하는 Handler를 구현하고 handler 변수에 저장합니다.

이제 핸들러로 메시지가 전달되면 total에 입력되어 있는 시간(초)을 60으로 나눈 값은 분 단위로, 60으로 나눈 나머지 값은 초 단위로 사용해서 textTimer에 입력합니다.

        // 화면에 시간 값을 출력하는 Handler 정의
        val handler = object: Handler(Looper.getMainLooper()){
            // 스레드로부터 메시지를 수신하면 실행
            override fun handleMessage(msg: Message){
                val minute = String.format("%02d", total/60)
                val second = String.format("%02d", total%60)
                binding.textTimer.text = "$minute:$second"
            }
        }


3. 시작 코드 구현

이제 btnStart 버튼에 클릭 리스너를 달고 시작 코드를 구현합니다.

버튼이 클릭되면 먼저 started를 true로 변경하고 새로운 스레드를 실행합니다. 스레드는 started가 true인 동안 while문을 반복하며 1초에 한 번씩 total의 값을 1 증가시키고 핸들러에 메시지를 출력합니다.

핸들러를 호출하는 곳이 하나밖에 없으므로 메시지에 0을 담아서 호출합니다.

        // 시작 코드 구현
        binding.btnStart.setOnClickListener {
            started = true
            thread(start=true){
                while(started){
                    Thread.sleep(1000)
                    if(started){
                        total += 1
                        handler?.sendEmptyMessage(0) // 핸들러에 메시지 전송
                    }
                }
            }
        }


4. 종료 코드 구현

마지막으로 btnStop 버튼에 클릭 리스너를 달고 종료 코드를 구현합니다.

        // 종료 코드 구현
        binding.btnStop.setOnClickListener {
            if(started){
                started = false
                total = 0
                binding.textTimer.text = "00:00"
            }
        }



[결과 화면]

image-20210822162142280



정리


  • 메인 스레드는 메인 액티비티를 비롯한 모든 컴포넌트를 실행하는 스레드입니다.
  • 백그라운드 스레드는 네트워크 작업, 파일 업로드와 다운로드, 이미지 처리, 데이터 로딩 등 메모리 이외의 다른 곳에서 데이터를 가져오는 작업을 수행할 때 사용하는 스레드입니다.
  • 백그라운드 스레드를 사용하는 방법에는 4가지가 있습니다.
    • Thread 클래스 상속
    • Runnable 인터페이스 구현
    • Runnable을 람다식으로 구현 (Thread.{…}.start( ))
    • 코틀린의 thread(start=true) 구현
  • 백그라운드 스레드는 UI 컴포넌트에 접근할 수 없습니다.
  • 루퍼는 메인 액티비티가 실행되는 시점에 무한루프를 돌기 시작하여 대기하고 있다가 자신의 큐에 쌓인 메시지를 핸들러에 전달합니다.
  • 핸들러는 새로 생성된 스레드들과 메인 스레드와의 통신을 담당합니다. 핸들러는 루퍼를 통해 전달되는 메시지를 받아서 처리하는 일종의 명령어 처리기로 사용됩니다.
  • 메시지는 루퍼의 큐에 값을 전달하기 위해서 사용되는 클래스입니다.
  • 서브 스레드가 메시지를 전송하면 이 메시지는 루퍼의 큐에 저장되고, 루퍼가 큐에 저장된 메시지를 핸들러에게 전달하면 미리 정의되어있는 핸들러 로직이 수행됩니다. 메시지를 수신했을 때의 로직은 handleMessage 메서드를 오버라이드해서 구현합니다.

Categories:

Updated:

Leave a comment