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

프로세스(process)란?

 

  🚀  프로세스(process)란 단순히 실행 중인 프로그램(program)으로 사용자가 작성한 프로그램이 운영체제에 의해 메모리 공간을 할당받아 실행 중인 것을 말함
  🚀  이러한 프로세스는 프로그램에 사용되는 데이터와 메모리 등의 자원 그리고 스레드로 구성

 

스레드(thread)란?


  🚀  스레드(thread)란 프로세스(process) 내에서 실제로 작업을 수행하는 주체를 의미
         ➡️  모든 프로세스에는 한 개 이상의 스레드가 존재하여 작업을 수행
  🚀  두 개 이상의 스레드를 가지는 프로세스를 멀티스레드 프로세스(multi-threaded process)라고 함

 

  📍  앱이 처음 시작될 때 시스템이 스레드 하나를 생성하는데 이를 메인 스레드라고 함
     

    ⚡️  메인 스레드의 역할

  • 액티비티의 모든 생명 주기 관련 콜백 실행을 담당
  • 버튼, 에디트텍스트와 같은 UI위젯을 사용한 사용자 이벤트와 UI드로잉 이벤트를 담당. UI 스레드라고도 불림.

작업량이 큰 연산이나, 네트워크 통신, 데이터베이스 쿼리 등은 처리에 긴 시간이 걸림. 이 모든 작업을 메인 스레드의 큐에 넣고 작업하면 한 작업의 처리가 완료될 때까지 다른 작업을 처리하지 못하기 때문에 사용자 입장에서는 마치 앱이 먹통이 된 것처럼 보이게 됨. 또, 몇 초 이상 메인 스레드가 멈추면 '앱이 응답하지 않습니다.'라는 메시지를 받게 됨

  📍  백그라운드 스레드를 활용하면 이러한 먹통 현상을 피할 수 있음. * 백그라운드 스레드 = 워커 스레드
      ✓  메인 스레드에서 너무 많은 일을 처리하지 않도록 백그라운드 스레드를 만들어 일을 덜어주는 것
      ✓  백그라운드 스레드에서 복잡한 연산이나, 네트워크 작업, 데이터베이스 작업 등을 수행
      ✓  주의할 점은 '백그라운드 스레드에서는 절대로 UI 관련 작업을 하면 안 된다는 점
            ➡️  각 백그라운드 스레드가 언제 처리를 끝내고 UI에 접근할지 순서를 알 수 없기 때문에 UI는 메인 스레드에서만 수정할 수있게 한 것. 따라서 백그라운드 스레드에서 UI 자원을 사용하려면 메인 스레드에 UI 자원 사용 메시지를 전달하는 방법을 이용해야 함

    📍  UI 스레드에서 UI 작업을 하는데 Handler 클래스, AsyncTask 클래스, runOnUiThread() 메서드 등을 활용할 수 있음

 


1)  runOnUiThread() 메서드

  🚀  runOnUiThread()는 UI 스레드(메인 스레드)에서 코드를 실행시킬 때 쓰는 액티비티 클래스의 메서드

Activity.java에 정의된 메서드
public final void runOnUiThread(Runnable action) {
    if (Thread.currentThread() != mUiThread) {
        mHandler.post(action);
    } else {
        action.run();
    }
}

 

  ✓  if문을 살펴보면, 만약 현재 스레드가 UI 스레드가 아니면 핸들러를 이용해 UI 스레드의 이벤트큐에 action을 전달 post
  ✓  만약 UI 스레드이면 action.run()을 수행. 즉 어떤 스레드에 있던지 runOnUiThread() 메서드는 UI스레드에서 Runable 객체를 실행

 

  📍  다음과 같은 UI 관련 코드를 runOnUiThread()로 감싸주어 사용

runOnUiThread(object : Runnable {
    override fun run() {
        // 여기에 원하는 로직을 구현
    }
})

 

   📍  코틀린의 SAM Single Abstract Method를 사용하면 더 간단하게 표현

runOnUiThread {
    // 여기에 원하는 로직을 구현
}

 


2.  스톱워치 만들기

 

1)  기본 레이아웃 설정

colors.xml과 strings.xml 설정
  • 스톱워치에서 4가지 색상과 3가지 문자열을 사용
  • [app] - [res] - [values] colors.xml파랑, 빨강, 노랑을 추가
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="purple_200">#FFBB86FC</color>
    <color name="purple_500">#FF6200EE</color>
    <color name="purple_700">#FF3700B3</color>
    <color name="teal_200">#FF03DAC5</color>
    <color name="teal_700">#FF018786</color>
    <color name="black">#FF000000</color>
    <color name="white">#FFFFFFFF</color>

    <!-- 직접 추가한 색 -->
    <color name="blue">#603CFF</color>
    <color name="red">#FF6767</color>
    <color name="yellow">#E1BF5A</color>
</resources>

 

  • [app] - [res] - [values] strings.xml에서 '시작', '일시정지', '초기화' 문자열을 추가
<resources>
    <string name="app_name">thread_0529</string>

    <!-- 추가한 문자열 -->
    <string name="start">시작</string>
    <string name="pause">일시정지</string>
    <string name="refresh">초기화</string>
</resources>

 

 

버튼 추가
  • [초기화]와 [시작] 버튼 생성
     <Button
        android:id="@+id/btnStart"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="80dp"
        android:backgroundTint="@color/blue"
        android:padding="20dp"
        android:text="@string/start"
        android:textColor="@color/white"
        android:textSize="16sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/btnRefresh"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="50dp"
        android:backgroundTint="@color/yellow"
        android:padding="20dp"
        android:text="@string/refresh"
        android:textColor="@color/white"
        android:textSize="16sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toTopOf="@+id/btnStart"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

 

 

텍스트뷰 추가
  • 흐르는 시간을 표현해줄 텍스트뷰를 생성
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/tvMinute"
android:text="00"
android:textSize="45sp" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/tvSecond"
android:text=":00"
android:textSize="45sp" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/tvMillisecond"
android:text=".00"
android:textSize="30sp" />

 

  • 레이아웃 미리보기 창에서 텍스트뷰들을 드래그하여 원하는 위치로 놓아줌

 

  📍 수직 방향 제약 추가하기

  • 세 텍스트 뷰의 수직 방향에 제약을 추가 ▶️  초를 나타내는 텍스트뷰를 중심으로 왼쪽에는 분을 나타내는 텍스트뷰, 오른쪽에
    는 밀리초를 나타내는 텍스트 뷰를 위치
  • 먼저 초 텍스트뷰의 위쪽을 레이아웃의 위쪽에 초 텍스트 아래쪽을 [초기화] 버튼의 상단에 드래그 ▶️ 그럼 텍스트뷰가 상하 제약의 중간 지점에 놓임
  • 분 텍스트뷰와 밀리초 텍스트뷰를 일직선 위에 놓이도록 제약을 추가.

         모든 텍스트뷰를 일직선 위에 놓으려면 베이스라인을 사용

              분 텍스트뷰 위에서 마우스 우클릭 -> [Show Baseline]을 선택. 그럼 텍스트의 아래쪽에 베이스라인 막대가 보임.분

              텍스트뷰 막대를 초 텍스트뷰 막대 모양에 드래그. 밀리초 텍스트뷰 역시 똑같은 방법으로 베이스라인을 정렬.

 

 

  📍 수평 방향 제약 추가하기

 

      ⚡️  컨스트레인트 레이아웃에는 뷰 여러 개의 수직 또는 수평 여백을 손쉽게 관리하는 체인 Chain을 제공

  • Ctrl 키를 누른 상태에서 세 텍스트뷰를 모두 클릭하여 선택
  • 선택한 텍스트뷰 위에서 마우스 우클릭 후, [Chains] - [Create Horizontal Chain]을 선택
  • 그럼 다음과 같이 세 텍스트뷰가 수평 방향으로 균등한 여백을 두고 위치

 

  • 딱 붙어있어야 되는 경우 세 텍스트뷰 위에서 마우스 우클릭 후, [Chains] - [Horizontal Chain Style] - [packed] 선택


 

2)  버튼에 이벤트 연결하기

MainActivity.kt
class MainActivity : AppCompatActivity(), View.OnClickListener {
    var isRunning = false // 실행 여부 확인용 변수

    private lateinit var btnStart: Button
    private lateinit var btnRefresh: Button
    private lateinit var tvMinute: TextView
    private lateinit var tvSecond: TextView
    private lateinit var tvMillisecond: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 뷰 가져오기
        btnStart = findViewById(R.id.btnStart)
        btnRefresh = findViewById(R.id.btnRefresh)
        tvMinute = findViewById(R.id.tvMinute)
        tvSecond = findViewById(R.id.tvSecond)
        tvMillisecond = findViewById(R.id.tvMillisecond)

        // 버튼별 OnClickListener 등록
        btnStart.setOnClickListener(this)
        btnRefresh.setOnClickListener(this)
    }

    override fun onClick(p0: View?) {
        when(p0?.id) {
            R.id.btnStart -> {
                if(isRunning) {
                    pause()
                } else {
                    start()
                }
            }
            R.id.btnRefresh -> {
                refresh()
            }
        }
    }

    private fun start() {
        // 스톱워치 측정을 시작하는 로직
    }

    private fun pause() {
        // 스톱워치 측정을 일시정지하는 로직
    }

    private fun refresh() {
        // 초기화하는 로직
    }
}

 

  ✓  클릭 이벤트를 처리하는 View.OnClickListener 인터페이스를 구현

  ✓  스톱워치가 현재 실행되고 있는지를 확인하는 데 사용하는 isRunning 변수를 false로 초기화해 생성

  ✓  findViewById() 메서드로 xml 레이아웃 파일에서 정의한 뷰들을 액티비티에서 사용할 수 있게 가져옴

  ✓  btnStart와 btnRefresh에 구현한 리스너를 등록. setOnClickListener() 메서드를 이용해서 onClickListener를 추가해주어야 클릭이 가능

  ✓  클릭 이벤트가 발생했을 때 어떤 기능을 수행할 지 구현. 따라서 View.OnClickListener 인터페이스는 반드시 onClick() 메서드를 오버라이딩해야함.

 


3)  스톱워치 시작 기능 구현하기

  [시작] 버튼을 누르면 스톱워치가 시작되고 [시작] 버튼 텍스트가 '일시정지'로 바뀜

 

MainActivity에 타이머 관련 변수 timer와 time을 추가하고 초기화
class MainActivity : AppCompatActivity(), View.OnClickListener {
    var isRunning = false // 실행 여부 확인용 변수
    var timer: Timer? = null
    var time = 0

 

start() 메서드를 구현
private fun start() {
    btnStart.text = "일시정지" // 버튼 텍스트 변경
    btnStart.setBackgroundColor(getColor(R.color.red)) // 배경색을 빨강으로 변경
    isRunning = true // 실행상태 변경

    // 스톱워치 측정을 시작하는 로직
    timer = timer(period = 10) {
        time ++ // 0.01초마다 time에 1을 더함

        // 시간 계산
        val milliSecond = time % 100
        val second = (time % 6000) / 100
        val minute = time / 6000

        runOnUiThread { // 텍스트뷰가 UI 스레드에서 실행되도록 메서드 사용
        
            // 시간이 한 자리일 때 전체 텍스트 길이를 두 자리로 유지
            if (isRunning) {
                tvMillisecond.text = if (milliSecond < 10) ".0${milliSecond}" else 
                  ".${milliSecond}" // 밀리초
                tvSecond.text = if (second < 10) ":0${second}" else ":${second}" //초
                tvMinute.text = if (minute < 10)"0${minute}" else "${minute}"//분
             }
        }
    }
}
💡  코틀린에서 제공하는 timer(period = [주기]) {} 메서드는 일정한 주기로 반복하는 동작을 수행할 때 사용
      {} 안에 쓰인 코드들은 모두 백그라운드 스레드에서 실행
      주기를 나타내는 period 변수를 10으로 지정했으므로 10밀리초마다 실행

 


4)  스톱워치 멈춤 기능 구현

private fun pause() {
    // 텍스트 속성 변경
    btnStart.text = "시작"
    btnStart.setBackgroundColor(getColor(R.color.blue))

    // 스톱워치 측정을 일시정지하는 로직
    isRunning = false // 멈춤 상태로 전환
    timer?.cancel() // 타이머 멈추기
}

 


5)  스톱워치 초기화 기능 구현

private fun refresh() {
    timer?.cancel() // 백그라운드 타이머 멈추기

    btnStart.text = "시작"
    btnStart.setBackgroundColor(getColor(R.color.blue))
    isRunning = false // 멈춤 상태로 전환

    // 초기화하는 로직
    time = 0
    tvMillisecond.text = ".00"
    tvSecond.text = ":00"
    tvMinute.text = "00"
}

 

 

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]

+ Recent posts