안드로이드 코틀린 : 쉬운 말로 설명하는 Thread 와 Handler 개념 이해 및 타바타 타이머 예시
Lucy Archive
Lucy 2023
2021. 5. 21. 01:54

Android Kotlin : 내마음대로 설명하는 Thread, Handler 와 예시

저는 새로운 것을 학습해야 할 때, 처음부터 익숙하지 않은 단어와 도표들을 보면 진입 장벽이 느껴져 어렵지 않은 것들도, 어렵게만 느껴집니다. 대부분의 지식은 이해하면 쉽다고 생각하지만, 쉽게 설명을 들을 수 있는 기회를 찾기 어렵습니다. 그래서 먼저 새로운 것을 학습할 때 나만의 언어로 변경해서 개념을 이해합니다. 이후에, 기술 용어나 이를 설명하는 다이어그램을 보면서 조금더 깊이 있게 학습을 하려 합니다. (이런 방식이 저에게만 맞는지 모르겠지만..)  이 포스트는

안드로이드와 같이 OS에서 실행되는 응용프로그램 개발할 때 필요한 Thread와 Handler의 개념을 제 방식대로 정리하고, 타이머 앱으로 구현한 예시를 소개

합니다.

 

내 맘대로 설명하는 Thread, Handler

안드로이드, 리눅스, 윈도우, IOS와 같은 OS의 응용프로그램이 처음이거나, Thread와 Handler 개념이 없으신분은 이 단원을 한번 참고해보세요. 저는 어느 학문분야이든 이렇게 이해를 해야 감이와서, 제 방식대로 개념을 정리해 보았습니다. 이 포스트에는 심도있는 Thread, Handler, 멀티태스킹, 멀티코어, 멀티쓰레딩 등등의 설명은 없습니다. 가볍게 봐주세요.

 

이야기1 : 햄버거 가게 이야기

박씨는 햄버거 가게를 창업했습니다.

처음엔 혼자 주문도 받고, 햄버거도 만들고, 서빙도 했습니다.

가게 오픈 초창기엔 혼자서도 할만 했지만, 혼자서 하기 버거워졌습니다.

이제, 서빙을 하지 않고 햄버거는 셀프로 받아가도록 시스템을 변경했습니다.

그럭저럭 할만하다가, 손님이 점점 많아져 혼자 일하기 벅찼습니다.

햄버거와 감자튀김을 만들어 주는 주방 담당 김씨를 고용하고, 박씨는 주문만 받았습니다.

김씨는 주문을 받으면 생산한 음식을 셀프대에 놓았습니다.

손님이 적었을 땐 그런대로 잘 운영되었지만, 손님이 많아지고 주문이 밀렸습니다.

셀프바의 크기는 한정되어 있고, 늦게 찾아가는 손님도 있는 등 여러가지 이유로 손님이 많아지면 셀프바는 난장판이 되고 손님의 불만이 커져갔습니다.

그래서 이씨를 고용했습니다. 이씨가 하는 일은 내가 받은 주문을 접수하고, 햄버거가 완료되면, 주문받은 순서대로 셀프바에 음식을 잘 찾아 갈 수 있도록 배치하는 역할을 맡겼습니다.

이렇게 이씨를 고용하고나니, 김씨는 셀프바를 생각하지 않고, 오로지 음식을 만들고, 다 만든 음식을 이씨에게 알려 주기만하게 되었습니다.

키워드 정리
  • 박씨 : 가게 주인
  • 김씨 : 주방 담당
  • 이씨 : 카운터, 서빙 준비(셀프바)

 

이야기2 : 컴퓨터 이야기

CPU는 한 번에 한가지 일만 할 수 있습니다.

아닌데?? PC, 스마트폰에서 음악들으면서 게임도 할 수 있으니 두가지 일을 하는거 아닌가?

맞습니다. 사용자가 보기에 여러가지 프로그램 또는 앱을 동시에 실행해주는 것 처럼 보입니다.

밥도 먹고, 티비도 봐야 하는 경우 우리가 할 수 있는 것은 두가지 입니다.

밥을 다 먹고 티비를 볼 수 있고,

밥한 숟갈 먹고, 티비 보고, 다시 한 숟갈먹고 티비 보고... 이렇게 두 가지 일을 동시에 할 수 있습니다.

편의상 두 가지 일을 동시에 했다고 하지만 여러 가지 일을 전환하며 진행한 것이죠.

하나의 PC에서 여러가지 프로그램을 동시에 실행하는 것도 같은 방식입니다.

프로그램A, 프로그램B를 빠르게 왔다 갔다 실행하면서 동시에 실행하게 하는 것 처럼 느끼게 해줍니다.

이렇게 여러가지 프로그램을 동시에 실행시키는 것 처럼 보일 수 있도록 스케쥴링을 하는 프로그램을 OS라고 합니다.

OS는 윈도우, 리눅스, 안드로이드, IOS 등의 Operating System을 말합니다. OS는 다른 주요한 기능은 많치만 생략합니다.

PC 윈도우에는 많은 프로그램, 스마트폰에는 많은 앱이 설치되어 있습니다.

이 떄, 실행 중인 프로그램 또는 앱을 프로세스라고 합니다.

스마트폰에서 유튜브를 실행했으면, 유튜브는 실행되었기 때문에 프로세스입니다.

유투브 어플을 생각해보면, 유튜브AI로 나에게 맞춤 영상 리스트를 보여주고, 내가 영상을 클릭하면 영상을 재생시켜 줍니다.

이런 일련의 작업을 수행하는 녀석을 쓰레드라고 합니다. 즉, 작업자라고 생각하면 됩니다.

앱의 크기, 종류 성격에 따라 작업자가 1명인 경우도 있고, 다수로 구성된 경우가 있습니다.

즉, 하나의 프로세스에는 메인쓰레드만 있거나 메인쓰레드 + 서브쓰레드로 구성됩니다.

쓰레드는 백그라운드 쓰레드라는 것이 있습니다.

백그라운드라는 말은 PC 화면 또는 스마트폰 화면에 보이지 않는 상태에서 실행되는 것을 말합니다.

전화, 문자 어플은 실행화면이 보이지 않는 상태에서도 수신 대기를 유지하고, 메세지가 수신되면 알려줍니다.

이런 백그라운드 스레드는 UI요소를 접근하지 못하게 되어 있습니다. UI요소를 변경할 일이 있으면 해당 앱이나 프로그램이 화면에 보일 때에 최종적인 화면을 반영하는 것이 효율적이기 때문일거라 추측합니다. 그래서 메인쓰레드와 백그라운드쓰레드 사이의 메신저 역할을 하는 핸들러가 있습니다.

키워드 정리
  • 프로그램
  • 프로세스 : 실행 중인 프로그램
  • 프로세스는 (다수의) 작업 단위 집합
  • 스레드(Thread) : 작업을 실행하는 주체
  • 프로세스는 하나의 스레드만 사용 될 수도 있고, 다수의 스레드가 사용될 수 있음
  • 백그라운드 스레드는 UI요소를 직접 접근 못함
  • 백그라운드 스레드와 메인스레드를 연결해 주는 것은 핸들러

 

연결고리 : 이야기1, 이야기2

너와 나의 연결고리~ 이야기1 이야기2 연결고리를 정리하면 아래와 같습니다.

  • 햄버거가게 : 프로그램
  • 햄버거가게(영업 중) : 프로세스
  • 박씨(창업자) : 메인쓰레드
  • 김씨 : 백그라운드 쓰레드
  • 이씨 : 핸들러
  • 셀프바 : 화면 UI

이 연결 고리를 조금 더 길게 적으면 아래와 같습니다.

우리 동내에는 여러개의 가게가 있다.
내 핸드폰에는 여러개의 앱(프로그램)이 있다.
우리 동내에 낮에 영업 중인 가게는 5개 이다.
내 폰에서 현재 실행 되고 있는 프로그램(프로세스는)은 5개 이다.
갓 창업한 소규모 가게는 직원이 한명이다.
적은 기능만 실행하는 프로세스는 스레드 하나만 사용한다.
가게가 크고 손님이 많은 식당은 직원이 여러명이다.
복잡한 기능을 가지는 프로그램이 여러개의 쓰레드가 사용된다.
주방에서 일하는 직원은 홀이나 주문 카운터에 나올 수 없다.
백그라운드 스레드는 UI요소를 직접 제어 할 수 없다.
음식이 완성되면 주방에서 일하는 직원이 매니저에게 알려준다.
백스라운드 스레드에서 UI요소를 제어해야 하는 경우 핸들러에게 이벤트를 준다.

 

안드로이드 타이머 예제

쓰레드를 사용하여 타바타와 같이 운동 시간, 쉬는 시간을 알려주는 타이머 앱 예시입니다. 타이머의 기능 중에서 시간을 측정하는 작업을 서브쓰레드에 할당한 프로그램입니다. 

개발 환경

이 포스트에 사용된 개발 환경은 아래와 같습니다. 

  • 개발 툴 : Android Studio 4.1.2
  • 사용 언어 : Kotlin 1.4.31
  • 기타 : ViewBinding 사용

ViewBinding 은 레이아웃(xml)의 View Id로 코틀린 코드에서 레이아웃 View에 접근하기 위해 사용합니다. ViewBinding 설정을 하기 위해 build.gradle 파일에 아래의 코드를 추가합니다. 

android { 
	... 
    viewBinding { 
    	enabled = true 
    } 
}

ViewBinding 설정 방법 및 사용법은 아래의 링크를 참조해주세요.

안드로이드 View Binding 사용하기

 

안드로이드 View Binding 사용하기 - kotlin-android-extensions 지원 중단

안드로이드 View Binding 방법 정리 안드로이드 코드에서 레이아웃 View에 접근하기 위해 사용된 kotlin-android-extensions 의 지원이 중단예정으로, 이를 대체하여 사용 할 수 있는 ViewBinding 사용법에 대해

juahnpop.tistory.com

 

activity_main.xml 레이아웃 코드 작성

activity_main.xml 레이아웃 파일에 아래와 같이 코드를 작성합니다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Work"
        app:layout_constraintBottom_toTopOf="@+id/txtWorkTime"
        app:layout_constraintEnd_toEndOf="@+id/txtWorkTime"
        app:layout_constraintStart_toStartOf="@+id/txtWorkTime" />

    <TextView
        android:id="@+id/txtWorkTime"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:text="00"
        android:textSize="50sp"
        android:textColor="#0000FF"
        android:gravity="center"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/txtRestTime"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Rest"
        app:layout_constraintBottom_toTopOf="@+id/txtRestTime"
        app:layout_constraintEnd_toEndOf="@+id/txtRestTime"
        app:layout_constraintStart_toStartOf="@+id/txtRestTime" />

    <TextView
        android:id="@+id/txtRestTime"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:text="00"
        android:textSize="50sp"
        android:textColor="#FF0000"
        android:gravity="center"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/txtWorkTime"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="운동 시간 : "
        android:textSize="16sp"
        app:layout_constraintBottom_toBottomOf="@+id/editWorkTime"
        app:layout_constraintEnd_toStartOf="@+id/editWorkTime"
        app:layout_constraintTop_toTopOf="@+id/editWorkTime" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="휴식 시간 : "
        android:textSize="16sp"
        app:layout_constraintBottom_toBottomOf="@+id/editRestTime"
        app:layout_constraintEnd_toStartOf="@+id/editRestTime"
        app:layout_constraintTop_toTopOf="@+id/editRestTime" />

    <EditText
        android:id="@+id/editWorkTime"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="10dp"
        android:layout_marginBottom="10dp"
        android:hint="시간을 입력하세요.."
        app:layout_constraintBottom_toTopOf="@+id/editRestTime"
        app:layout_constraintEnd_toEndOf="parent" />

    <EditText
        android:id="@+id/editRestTime"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="10dp"
        android:layout_marginBottom="30dp"
        android:hint="시간을 입력하세요.."
        app:layout_constraintBottom_toTopOf="@+id/btnStop"
        app:layout_constraintEnd_toEndOf="parent" />

    <Button
        android:id="@+id/btnStart"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:text="시작"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/btnStop"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/btnStop"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:text="종료"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toEndOf="@+id/btnStart" />

</androidx.constraintlayout.widget.ConstraintLayout>

위와 같이 코드를 작성하면 아래와 같은 UI가 생성됩니다. 우측 하단의 운동 시간과, 휴식 시간을 입력하고 시작버튼을 누르면 운동시간과 쉬는시간을 카운트하면서 보여줍니다.  

MainActivity.kt 코틀린 코드 작성

MainActivity.kt 에 아래와 같이 코드를 작성합니다. 쓰레드와 관련한 주요 코드는 목록은 아래와 같습니다.

  • Global 변수
    • workTime : 운동 시간(실시간)
    • restTime : 쉬는 시간(실시간)
    • workTimeStarted : 운동 시간을 카운트 중인지 판단
    • restTimeStarted : 쉬는 시간을 카운트 중인지 판단
    • stared : 타이머가 동작중인지 판단
  • 24줄 handler : Thread에서 1초간격 알림을 주면 UI 요소의 타이머를 업데이트 
  • 67줄 workTimerStart() : 운동 시간을 카운트 하는 함수, 시간 측정하는 쓰레드를 생성
  • 82줄 restTimerStart() : 쉬는 시간을 카운트 하는 함수, 시간 측정하는 쓰레드를 생성
  • 97줄 endTimer() : 종료 버튼을 누르면 실행되는 함수, 모든 설정을 초기화
package com.blacklog.timer

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.Log
import com.blacklog.timer.databinding.ActivityMainBinding
import kotlin.concurrent.thread

class MainActivity : AppCompatActivity() {

    lateinit var binding : ActivityMainBinding

    var workTime = 0
    var restTime = 0

    var workTimeStarted = false
    var restTimeStarted = false

    var started = false

    private val handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            if(started){
                when{
                    workTimeStarted -> {
                        binding.txtWorkTime.text = workTime.toString()
                        workTime -= 1
                        if(workTime == -1){
                            workTimeStarted = false
                            restTimerStart()
                        }
                    }
                    restTimeStarted -> {
                        binding.txtRestTime.text = restTime.toString()
                        restTime -= 1
                        if(restTime == -1){
                            restTimeStarted = false
                            workTimerStart()
                        }
                    }
                }
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)

        binding.btnStart.setOnClickListener {
            started = true
            binding.btnStart.isEnabled = false
            workTimerStart()
        }

        binding.btnStop.setOnClickListener {
            endTimer()
            binding.btnStart.isEnabled = true
        }
    }

    private fun workTimerStart(){

        workTime = binding.editWorkTime.text.toString().toInt()
        workTimeStarted = true

        thread(start=true){
            while(workTime >= 0){
                Log.d("MyDebug", "workTime : $workTime")
                handler.sendEmptyMessage(0)
                Thread.sleep(1000)
                if(!workTimeStarted) break
            }
        }
    }

    private fun restTimerStart(){

        restTime = binding.editRestTime.text.toString().toInt()
        restTimeStarted = true

        thread(start=true){
            while(restTime >= 0){
                Log.d("MyDebug", "restTime : $restTime")
                handler.sendEmptyMessage(0)
                Thread.sleep(1000)
                if(!restTimeStarted) break
            }
        }
    }

    private fun endTimer(){
        started = false
        workTimeStarted = false
        restTimeStarted = false
        workTime = 0
        restTime = 0
        binding.txtRestTime.text = restTime.toString()
        binding.txtWorkTime.text = workTime.toString()
    }
}

실행 결과

위 코드를 실행 하면, 아래와 같이 텍스트 입력창에 입력 된 시간 만큼 운동시간, 쉬는시간을 표시해 주는 것을 확인 할 수 있습니다. 종료버튼을 누르면 타이머가 종료되고, 시작을 누르면 입력된 시간 만큼 타이머가 동작을 시작합니다. 예제로 간단히 만든 파일이라서, 입력 창의 값이 없는 등의 예외적 사항 처리는 되지 않았습니다.

운동시간 타이머

끝까지 읽어 주셔서 감사합니다.^^

관련포스트

🤞 안드로이드(코틀린) 앱 제작하기 관련글 목록 보기

🤞 안드로이드(코틀린) 멀티태스킹 관련글 목록 보기

🤞 안드로이드(코틀린) 스레드 관련글 목록 보기

🤞 내 마음대로 개념 설명한 포스트 목록 보기