[그로쓰] 생명주기와 쓰레드의 중요성

Effy_ee·2023년 5월 12일
0

우리 조원들이 공통적으로 관심있는 것이 무엇일까?🤨

생각해보다 다들 사람의 신체를 파악해 자세를 추정하는 기술을 어플리케이션에 적용해보고 싶어한다는 것을 알게되었다. 따라서, 스타트 때는 모션인식을 통한 K-pop 포인트 안무 연습 & 녹화 서비스를 진행하기로 하였었다. 하지만 춤 동작은 가지수가 너무 다양해서 모델을 학습시키기에 어려움을 겪었었다.
따라서 신체추정이라는 키워드는 유지한채로, 동작의 가짓 수가 조금 더 적고 정형화되어있으며, 모델을 학습시키기위한 데이터베이스가 조금 더 많은 쪽으로 주제를 변경하기로 하였다.
따라서 변경된 주제는, 시각 장애인을 위한 요가 어플리케이션👟이 되었다.
안드로이드 스튜디오에서 코틀린 언어를 사용해서 어플리케이션을 개발하기로 하였다.

안드로이드 스튜디오를 공부하다 보면, 생명주기(LifeCycle)가 굉장히 중요하다는 말을 듣게 되는데 처음에는 그런가보다~하고 넘어갔던 것들이 이번 개발을 통해서 절실히 느껴졌다. 이전 포스트에서도 생명주기를 간단히 설명하고 넘어갔지만, 이것이 어떤 방식으로 어플리케이션 동작 방식을 만드는지는 깊게 이해하지 못하고 개발을 시작했고, 이 때문에 요가 클래스를 구성하는 기능을 만들 때 많이 애를 먹었다.

따라서, 생명주기에 대한 개념을 확실히 하고, 다음 추가된 기능들에 대한 설명을 하려고 한다.

  • 생명주기란?
  • 쓰레드란?
  • 생명주기 ,쓰레드를 사용해서 요가 클래스 구성하기

❤️시작해보자❤️




출처 https://developer.android.com/guide/components/activities/activity-lifecycle?hl=ko

🙋‍♀️ LifeCycle 이란?

앱이 생성되고 종료될 때까지의 단계적인 과정을 말한다. 앱의 상태 변화와 사용자와의 상호작용에 따라 다양한 이벤트와 메서드를 호출하게되는데, 이도 포함한다.

앱의 생명주기를 실제 메모 어플리케이션을 가정해서 생각해보자.

👉 앱을 처음 실행하면,

onCreate() 메서드가 호출되고, 앱의 초기 설정을 수행하고 사용자 인터페이스를 설정하게 된다.
onStart() 메서드가 호출되고, 앱이 사용자에게 보여지기 직전에 필요한 초기화 작업을 수행한다.
onResume() 메서드가 호출된다. 앱이 전면에 나타나고 사용자가 사용할 수 있는 상태가 된다.

👉 사용자가 메모를 작성하고 저장한 후 홈 버튼을 눌러 앱을 나가게되면,

onPause() 메서드가 호출되고, 현재 액티비티가 일시 중지되고 앱이 화면에서 가려지게되는데, 작성한 메모를 저장하거나 앱 상태를 유지를 해야한다.
onStop() 메서드가 호출되고, 앱이 더 이상 사용자에게 보이지 않는 상태가 된다.

👉 사용자가 앱을 다시 실행하여 메모를 수정하려고 하면,

onRestart() 메서드가 호출되고, 앱이 중단된 상태에서 다시 시작되는 경우에만 호출된다. 이 때, 이전에 작성한 메모를 불러올 수 있다.
onStart() 메서드가 호출되고, 앱이 사용자에게 보여지기 직전에 필요한 초기화 작업을 수행한다.
onResume() 메서드가 호출되고, 앱이 전면에 나타나고 사용자와 상호작용할 준비가 된 상태가 된다.

👉 사용자가 앱을 완전히 종료하면,

onPause() 메서드가 호출되고,
onStop() 메서드가 호출되고,
onDestroy() 메서드가 호출된다. 앱이 완전히 종료되기 전에 호출되며, 여기에서 앱이 사용하는 모든 리소스를 해제하고 정리하는 작업을 수행해야 한다.

😊아래는 예시 코드이다.
onCreate부터 onDestroy까지 Log를 찍어서 Logcat을 통해 확인해보기

class MainActivity : AppCompatActivity() {

    companion object {
        private const val TAG = "MainActivity"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        Log.d(TAG, "onCreate() called")
    }

    override fun onStart() {
        super.onStart()
        Log.d(TAG, "onStart() called")
    }

    override fun onResume() {
        super.onResume()
        Log.d(TAG, "onResume() called")
    }

    override fun onPause() {
        super.onPause()
        Log.d(TAG, "onPause() called")
    }

    override fun onStop() {
        super.onStop()
        Log.d(TAG, "onStop() called")
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d(TAG, "onDestroy() called")
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        Log.d(TAG, "onSaveInstanceState() called")
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        super.onRestoreInstanceState(savedInstanceState)
        Log.d(TAG, "onRestoreInstanceState() called")
    }
}


위의 로그는 앱을 중단하고, 다시 들어갔을 때 찍힌 로그이다.


👉🏻 생명주기에 기능 넣어보기

간단한 메모장 어플의 기능을 할 수 있는데,
여기서는 두 개의 액티비티를 사용해서 한 액티비티에서 텍스트를 전송하였을 때, 두 번째 액티비티에서 전송한 텍스트를 보여주는 예시를 만들어보았다.

각 생명주기 별로 구별해서 설명해보면,

onCreate

//액티비티가 생성될 때 호출
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
   
   //binding.root는 액티비티의 레이아웃 파일의 최상위 뷰
    //해당 레이아웃을 화면에 표시
    
    setContentView(binding.root)

    binding.button.setOnClickListener {
        // 다음 화면으로 이동

        // 1. Intent 객체 생성하여 다음 화면으로 이동할 정보 담기
        val intent = Intent(this, SecondActivity::class.java)
        
        // 2. "text"라는 키로 text 변수의 값을 전달
        intent.putExtra("text", text)
        
        // 3. startActivity() 메서드를 호출하여 다음 화면으로 이동
        startActivity(intent)
    }
}



onResume

초기에 어플이 실행될 때, 앱을 잠시 중단했다가 다시 켤 때 호출되는 메서드이다. 따라서 이전에 사용자가 작성했던 메모가 있다면 메모를 바인딩해줘야 한다.

 override fun onResume() {
        super.onResume()
        
        if (!text.isNullOrEmpty()) {
            binding.editText.setText(text)
        }
    }



onPause

잠시 앱을 중단할 때 불리는 메서드이다. 작성했던 메모를 text 에 담아주게 된다.

  override fun onPause() {
        super.onPause()
        text = binding.editText.text.toString()
    }



onRestart

앱을 중단했다가 다시 실행했을 때, 불리는 메서드이다. 사용자가 다시 작성할까요? 하는 다이얼로그를 통해 이전에 작성했던 메모를 지우거나, 이전에 작성했던 메모를 유지한 상태로 작업을 이어나갈 수 있게 된다.

override fun onRestart() {
        super.onRestart()
        AlertDialog.Builder(this)
            .setTitle("다시 작성할까요?")
            .setMessage("작성한 내용이 모두 삭제됩니다.")
            .setPositiveButton("다시 작성") { _, _ ->
                binding.editText.setText("")
                text = null
            }
            .setNegativeButton("취소") { _, _ ->
                // 아무것도 안 함
            }
            .show()
    }

👉🏻 전체코드

class MainActivity : AppCompatActivity() {

    private lateinit var binding:ActivityMainBinding
    private var text: String? = null

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

        binding.button.setOnClickListener {
            // 다음 화면으로 이동
            val intent = Intent(this, SecondActivity::class.java)
            intent.putExtra("text", text)
            startActivity(intent)
        }
    }

    override fun onResume() {
        super.onResume()
        if (!text.isNullOrEmpty()) {
            binding.editText.setText(text)
        }
    }

    override fun onPause() {
        super.onPause()
        text = binding.editText.text.toString()
    }

    override fun onRestart() {
        super.onRestart()
        AlertDialog.Builder(this)
            .setTitle("다시 작성할까요?")
            .setMessage("작성한 내용이 모두 삭제됩니다.")
            .setPositiveButton("다시 작성") { _, _ ->
                binding.editText.setText("")
                text = null
            }
            .setNegativeButton("취소") { _, _ ->
                // 아무것도 안 함
            }
            .show()
    }
}


이제 생명주기에 대해 조금은 이해가 됐길 바란다
다음으로 넘어가보자😎




🙋‍♀️ Thread(쓰레드)란?

하나의 프로세스 내에서 실행되는 작은 작업의 단위이다. 이러한 작업들은 동시에 실행될 수 있다. 즉, 프로그램이 여러 작업을 처리하면서 다른 작업을 동시에 실행할 수 있다.

👉 Single-Thread란?

싱글 스레드(single-thread)는 하나의 실행 스레드(thread)만 가지는 것을 말한다. 즉, 하나의 프로세스(process)에서 단일 스레드만 실행된다.

싱글 스레드 애플리케이션은 한 번에 하나의 작업만 처리할 수 있다. 한 작업이 완료되기 전에 다른 작업을 수행할 수 없으며, 이러한 경우 애플리케이션이 느려지거나 멈출 수 있다.

👉 Multi-Thread란?

멀티스레드(multi-thread)는 하나의 프로세스(process) 내에서 여러 개의 스레드(thread)가 동시에 실행되는 것을 말한다. 즉, 하나의 애플리케이션이 동시에 여러 작업을 수행할 수 있다.

멀티스레드 애플리케이션은 여러 개의 스레드가 동시에 실행되므로 시스템의 성능을 향상시킬 수 있다. 예를 들어, 웹 서버 애플리케이션에서 요청을 처리하는 스레드와 데이터베이스에서 데이터를 가져오는 스레드를 병렬로 실행할 수 있다.

즉, 한 화면에서 여러 기능을 동작시키고 싶을 때, 다른 쓰레드를 정의해서 메인 액티비티에 붙여주면, 각 동작을 병렬적으로 수행하기에 효과적으로 어플리케이션을 동작시킬 수 있다는 말이다😊

생명주기와 쓰레드의 개념을 이해하지 못하고 어플리케이션을 개발하려고 하다보니 당연히 한 액티비티에서 한가지의 생명주기만을 갖는 제한적인 어플리케이션이 만들어졌었다.

🙋‍♀️ Handler란?

스레드 간 통신을 위한 메커니즘 중 하나이다. Handler는 메시지 큐를 사용하여 스레드 간 메시지를 전달하고 처리할 수 있다.

쓰레드를 사용하는 이유는?

  1. 메인(UI) 스레드에서 다른 스레드에서 실행된 작업을 처리할 수 있다.
  2. 작업을 지연시키거나 주기적으로 실행할 수 있다.
  3. 스레드 간 통신을 위한 안전한 방법을 제공한다.

🙋‍♀️ Looper란?

스레드의 루프를 관리하는 클래스이다. Looper는 해당 스레드의 메시지 큐를 루프에 묶어서 메시지 큐에서 메시지를 처리하고, 새로운 메시지가 도착할 때까지 계속해서 대기한다.

쓰레드와 핸들러를 타이머를 통해서 익혀보자.



☝🏻 스레드, 핸들러 추가

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

    // 핸들러 객체 생성
    handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            if (msg.what == MESSAGE_UPDATE_TIMER) {
                val time = msg.obj as String
                // UI에 타이머 시간 업데이트
                binding.text.text = time
            }
        }
    }

    // 타이머 스레드 생성 및 시작
    timerThread = TimerThread(handler)
    timerThread.start()

    var isTimerRunning = true

    // 시작 버튼 클릭 시 타이머 재개
    binding.sbutton.setOnClickListener {
        timerThread.resumeTimer()
    }

    // 일시 정지 버튼 클릭 시 타이머 일시 정지 또는 재개
    binding.pbutton.setOnClickListener {
        if (isTimerRunning) {
            timerThread.pauseTimer() // 타이머 일시 정지
        }
        isTimerRunning = !isTimerRunning // 타이머 상태 토글
    }
}
//앱이 끝날 때, 타이머 스레드도 같이 꺼주기
 override fun onDestroy() {
        super.onDestroy()
        timerThread.stopTimer()

    }



✌ 메세지 정의해주기


companion object {
private const val MESSAGE_UPDATE_TIMER = 1 // 타이머 업데이트 메시지 상수
private const val MESSAGE_TIMER_END = 2 // 타이머 종료 메시지 상수
}

👌🏻 타이머 스레드 정의해주기

private class TimerThread(private val handler: Handler) : Thread() {
private var isRunning = true // 타이머가 실행 중인지 여부를 나타내는 변수
private var isPaused = false // 타이머가 일시 정지되었는지 여부를 나타내는 변수
private var seconds = 30 // 타이머의 초 단위 시간
fun startTimer() { // 타이머를 시작하는 함수
    isRunning = true
}

fun pauseTimer() { // 타이머를 일시 정지하는 함수
    isPaused = true
}

fun resumeTimer() { // 일시 정지된 타이머를 다시 시작하는 함수
    isPaused = false
}

fun stopTimer() { // 타이머를 중지하는 함수
    isRunning = false
}

override fun run() {
    while (isRunning && seconds > 0) { // 타이머가 실행 중이고 남은 시간이 0보다 클 때 반복
        if (!isPaused) { // 타이머가 일시 정지되지 않았을 때
            try {
                sleep(1000) // 1초 대기
                seconds-- // 남은 시간 감소
                val time = formatTime(seconds) // 시간을 포맷팅하여 문자열로 변환
                val message = handler.obtainMessage(MESSAGE_UPDATE_TIMER, time) // 타이머 업데이트 메시지 생성
                handler.sendMessage(message) // 핸들러에 메시지 전송
            } catch (e: InterruptedException) {
                e.printStackTrace()
            }
        }
    }
    if (seconds == 0) { // 타이머가 종료되었을 때
        val message = handler.obtainMessage(MESSAGE_TIMER_END) // 타이머 종료 메시지 생성
        handler.sendMessage(message) // 핸들러에 메시지 전송
    }
}

private fun formatTime(seconds: Int): String { // 시간을 포맷팅하는 함수
    val minutes = seconds / 60 // 분 단위 계산
    val remainingSeconds = seconds % 60 // 초 단위 계산
    return String.format("%02d:%02d", minutes, remainingSeconds) // 포맷팅된 문자열 반환
}




이제 기본적인 어플리케이션 동작 방식을 이해했으니, 본격적으로 구성해보자😎

🙋‍♀️ 적용해보기

요가 자세가 맞으면 카운트 다운이 내려가는 기능을 구현하기 해보려고 했다. 처음엔 쓰레드와 생명주기를 이해하지 못해서 따로 카운트 다운 함수를 아래처럼 만들었다. 카운트 다운이 끝나면 다시 onCreate를 수행했기에 화면 깜빡임이 있을 수 밖에..!

😂 처음 코드

생명주기를 이해하지 않고 따로 함수를 만드니까 뒤에 .start()까지 붙이고, 시간 낭비를 많이 했다.

 fun countDown(time: String,asanaClass: AsanaClass) {
        var conversionTime: Long = 0


        // 1000 단위가 1초
        // 60000 단위가 1분
        // 60000 * 3600 = 1시간
        var getHour = time.substring(0, 2)
        var getMin = time.substring(2, 4)
        var getSecond = time.substring(4, 6)

        // "00"이 아니고, 첫번째 자리가 0 이면 제거
        if (getHour.substring(0, 1) === "0") {
            getHour = getHour.substring(1, 2)
        }
        if (getMin.substring(0, 1) === "0") {
            getMin = getMin.substring(1, 2)
        }
        if (getSecond.substring(0, 1) === "0") {
            getSecond = getSecond.substring(1, 2)
        }

        // 변환시간
        conversionTime =
            java.lang.Long.valueOf(getHour) * 1000 * 3600 + java.lang.Long.valueOf(getMin) * 60 * 1000 + java.lang.Long.valueOf(
                getSecond
            ) * 1000

        // 첫번쨰 인자 : 원하는 시간 (예를들어 30초면 30 x 1000(주기))
        // 두번쨰 인자 : 주기( 1000 = 1초)
        object : CountDownTimer(conversionTime, 1000) {
            // 특정 시간마다 뷰 변경
            override fun onTick(millisUntilFinished: Long) {

                // 시간단위
                var hour = (millisUntilFinished / (60 * 60 * 1000)).toString()

                // 분단위
                val getMin = millisUntilFinished - millisUntilFinished / (60 * 60 * 1000)
                var min = (getMin / (60 * 1000)).toString() // 몫

                // 초단위
                var second = (getMin % (60 * 1000) / 1000).toString() // 나머지

                // 밀리세컨드 단위
                val millis = (getMin % (60 * 1000) % 1000).toString() // 몫

                // 시간이 한자리면 0을 붙인다
                if (hour.length == 1) {
                    hour = "0$hour"
                }

                // 분이 한자리면 0을 붙인다
                if (min.length == 1) {
                    min = "0$min"
                }

                // 초가 한자리면 0을 붙인다
                if (second.length == 1) {
                    second = "0$second"
                }
                binding.textView.setText("$hour:$min:$second")

            }


            // 제한시간 종료시
            override fun onFinish() {


                // 변경 후
                binding.textView.setText("Done")

                // TODO : 타이머가 모두 종료될때 어떤 이벤트를 진행할지
                ttsSpeechManager.speakNextAsana(asanaClass)

                onResume()



            }

        }.start()
    }

수정된 코드는 위해 적용된 쓰레드를 따로 만들어 생명주기에 붙여준 코드를 사용했다😊

잘되는것을 확인😊

0개의 댓글