액티비티 생명주기, 액티비티 ANR 문제와 코루틴

이윤설·2024년 9월 2일
0

액티비티 생명주기

액티비티의 상태

액티비티는 사용자에게 UI가 있는 화면을 제공하는 앱 컴포넌트이다.
액티비티를 다룰 때 중요한 점은 생명주기로 이해하고 각 상황에 적절하게 대처하는 것이다.

액티비티의 상태는 크게 3가지로 구분할 수 있다.

  • 활성: 액티비티 화면이 출력되고 있고 사용자가 이벤트를 발생시킬 수 있는 상태
  • 일시정지: 액티비티 화면이 출력되고 있고 있지만, 사용자가 이벤트를 발생시킬 수 없는 상태
  • 비활성: 액티비티 화면이 출력되고 있지 않는 상태
    ex) 앱을 사용하다가 다른 앱을 사용하기 위해 홈 버튼을 눌렀을 때..

실전에서는 onStart(), onStop() 등 다양한 생명주기 함수를 사용해야 한다.
중요한 개념이므로 알아둘 필요가 있다.

액티비티의 상태 저장

액티비티가 종료되면 일반적으로 객체가 소멸되어 해당 액티비티의 데이터도 모두 사라진다. 따라서 다시 액티비티를 실행하면 초기 상태로 돌아오게 된다. 그러나 특정 상황에서는 종료 시 데이터를 저장해, 다시 액티비티가 실행될 때 복원할 수 있다.

데이터 저장이 필요한 경우

예를 들어, 화면을 회전하면 액티비티가 종료되고 다시 생성된다. 이때, onCreate()부터 onResume()까지의 생명주기가 다시 실행되며, 새로운 액티비티 객체가 생성되어 화면에 표시된다.

만약 화면 회전 시 유지해야 할 데이터가 있다면, 이를 Bundle 객체에 저장해야 한다. Android 시스템은 일부 UI 상태를 자동으로 Bundle에 저장하고 복원하지만, 중요한 데이터는 개발자가 onSaveInstanceState() 메서드를 통해 직접 관리해야 한다.

참고로, 화면을 회전해도 EditText에 입력된 텍스트는 사라지지 않는다. 과거에는 EditText의 데이터가 소멸했지만, 이제는 Android가 내부적으로 이를 자동 저장하도록 개선되어 추가적인 작업이 필요 없다.

요약하자면, 액티비티가 종료될 때 복원해야 할 데이터가 있다면, 이를 Bundle에 저장하는 것이 중요하다. 기본적으로 Android 시스템이 자동으로 처리하는 데이터 외에, 중요한 정보는 onSaveInstanceState() 메서드를 통해 직접 저장하고 복원해야 한다.


액티비티 ANR 문제와 코루틴

ANR(Application Not Responding)은 Android에서 앱이 응답하지 않는 상태를 나타내는 경고로, 특정 작업이 너무 오래 걸려 사용자가 앱을 사용하지 못하는 상황에서 발생한다. ANR이 발생하면 사용자는 앱을 종료하거나 기다리는 선택지를 제공받게 된다.

ANR이 발생하는 주요 원인

  1. 메인 스레드에서 긴 작업 수행: Android 앱은 메인(UI) 스레드에서 사용자 인터페이스를 처리한다. 이 스레드는 짧고 빠르게 작업을 처리해야 하지만, 만약 메인 스레드에서 긴 작업(예: 파일 I/O, 네트워크 요청, 복잡한 연산)을 수행하면 ANR이 발생할 수 있다.

  2. 브로드캐스트 리시버에서의 시간 초과: 브로드캐스트 리시버는 일반적으로 짧은 시간 내에 실행을 마쳐야 한다. 하지만 이 리시버에서 긴 작업을 수행하거나 작업이 완료되지 않으면 ANR이 발생할 수 있다.

  3. 서비스에서의 시간 초과: 서비스는 백그라운드에서 작업을 수행하지만, startForegroundService()로 시작된 서비스는 일정 시간 내에 startForeground()를 호출하지 않으면 ANR이 발생한다.

브로드캐스트 리시버

브로드캐스트 리시버는 Android에서 특정 이벤트나 시스템 상태 변화를 감지하고, 그에 따라 앱이 적절한 작업을 수행할 수 있도록 도와주는 컴포넌트다. 이 컴포넌트는 시스템 또는 다른 앱이 발생시키는 브로드캐스트 메시지를 수신하고 처리하는 역할을 한다.
ex) 인터넷 연결 상태 감지, 배터리 상태 감지, 이어폰 연결여부 감지

ANR의 시간 한계

  • Activity 및 BroadcastReceiver: 5초 이내에 작업이 완료되지 않으면 ANR이 발생한다.
  • ContentProvider: 10초 이내에 작업이 완료되지 않으면 ANR이 발생한다.
  • Service: 20초 이내에 작업이 완료되지 않으면 ANR이 발생한다.

ANR을 방지하는 방법

  1. 백그라운드 스레드 사용: 긴 작업은 메인 스레드가 아닌 별도의 스레드(예: AsyncTask, HandlerThread, ExecutorService, Coroutine)에서 처리해야 한다.

  2. 비동기 처리 사용: 네트워크 요청이나 파일 읽기/쓰기는 비동기 방식으로 처리하여 메인 스레드를 블록하지 않도록 한다.

  3. 서비스 최적화: 서비스에서 수행하는 작업이 오래 걸릴 경우, 백그라운드 스레드에서 작업을 수행하고, 필요시 JobIntentServiceWorkManager 같은 적절한 작업 관리 클래스를 사용한다.

  4. UI 작업 최소화: UI 스레드에서는 가능한 한 가벼운 작업만 수행하며, 무거운 작업은 피해야 한다.

ANR 예시

시스템에서 액티비티를 실행하는 수행 흐름을 메인 스레드 또는 UI 스레드라고 한다. 즉, 메인 스레드 == UI 스레드다.

메인스레드가 오래 걸리는 작업을 실행한다고 해서 그 자체로 오류가 발생하지는 않는다. 아무리 오래 걸려도 사용자가 액티비티 화면을 터치하지 않는 등 이벤트가 없다면 오류가 발생한다.

그러나 사용자가 언제 화면을 터치할지 모른다. 따라서 액티비티를 작성할 때는 항상 ANR 오류를 고려해야 한다.

액티비티에서 시간이 오래 걸리는 대표적인 작업은 서버와 통신하는 네트워크다.
정상적인 상황에서는 서버와 연결하여 데이터를 주고받는데 1,2초면 끝나지만, 이때에도 ANR 오류가 발생할 수 있다고 생각해야 한다. 왜냐하면 모바일에서는 네트워크가 불안정할 때가 많은데, 그런 상황에서 앱이 실행되면 네트워크에 접속을 실행하느라 시간이 오래 걸릴 수 있기 때문이다. (물론 네트워크 통신을 사용하는 전문 라이브러리(Volley, Retrofit2)를 사용하면 개발자가 ANR 문제를 고려하지 않아도 됨)

해결책

ANR 문제를 해결하는 방법은 액티비티를 실행한 메인 스레드 이외에 실행 흐름(개발자 스레드)를 따로 만들어서 시간이 오래 걸리는 작업을 담당하게 하면 된다.
그러면 개발자가 만든 스레드가 시간이 오래 걸리는 작업을 수행하더라도 메인 스레드는 언제든지 이벤트를 처리할 수 있어서 ANR이 발생하지 않는다.

하지만 이 방법은 화면을 변경할 수 없다는 또다른 문제가 생긴다.
왜냐하면 화면 변경은 오직 메인 스레드에서만 할 수 있기 때문이다.

코루틴으로 ANR 해결

코루틴이란?

코루틴이란, 비동기 경량 스레드라고 할 수 있다.
프로그램이 단일 흐름으로 실행되면 작업이 차례대로 이루어질 뿐 함께 처리된다.
결국 비동기 처리 방식과 동일하다.

요즘은 스레드를 사용하는 대신, RX 프로그래밍이나 코루틴으로 비동기 처리를 구현한다. 코루틴은 스레드보다 가볍고 더 많은 기능을 제공한다.

예제

싱글 스레드

var sum = 0L
var time = measureTimeMillis {
  for (i in 1..2_000_000_000) {
    sum += i
  }
}

Log.d("ryan", "time : $time")
binding.resultView.text = "sum : $sum"

1부터 20억까지 더하는 작업이다.
결과가 나오기까지 보통 7초정도 걸리며, 글을 입력하려고 터치하면 ANR 오류가 발생할 수 있다.

스레드-핸들러 구조로 수정

// 핸들러 생성 (메인 스레드에서 실행)
val handler = Handler(Looper.getMainLooper())

// 별도의 스레드에서 작업 실행
Thread {
    var sum = 0L
    val time = measureTimeMillis {
        for (i in 1..2_000_000_000) {
            sum += i
        }
    }

    // 로그 출력 (스레드 내에서)
    Log.d("ryan", "time : $time")

    // 결과를 메인 스레드에서 UI 업데이트를 위해 핸들러를 통해 전달
    handler.post {
        binding.resultView.text = "sum : $sum"
    }
}.start() // 스레드 시작
  1. 핸들러 생성:
    Handler는 Looper.getMainLooper()를 이용해 메인 스레드와 연결되어 있다. 이 핸들러는 메인 스레드에서 실행할 작업을 큐에 넣어준다.

  2. 스레드 생성:
    새로운 Thread를 생성해 sum을 계산하는 긴 작업을 수행한다. 이 작업은 메인 스레드와는 별도로 실행되어, UI 스레드가 블록되는 것을 방지한다.

  3. 핸들러로 결과 전달:
    긴 작업이 완료되면, handler.post { ... }를 사용해 메인 스레드로 돌아와 UI를 업데이트한다. 이 부분에서 resultView의 텍스트가 갱신된다.

시간이 오래 걸리는 작업을 개발자 스레드로 작성했기 때문에 ANR 문제는 발생하지 않는다.

참고로 이 구조는 더이상 현업에서 사용되지 않는다. 그냥 참고만 하자.

코루틴 사용

import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

// CoroutineScope를 Dispatchers.Default로 설정하여 백그라운드 작업을 수행
CoroutineScope(Dispatchers.Default).launch {
    var sum = 0L

    // 시간 측정
    val time = measureTimeMillis {
        for (i in 1..2_000_000_000) {
            sum += i
        }
    }

    // GlobalScope를 사용하여 메인 스레드에서 UI 업데이트
    GlobalScope.launch(Dispatchers.Main) {
        Log.d("ryan", "time : $time")
        binding.resultView.text = "sum : $sum"
    }
}

코드 설명

  1. CoroutineScope(Dispatchers.Default):

    • 스코프는 성격이 같은 코루틴을 묶는 개념이다. 한 스코프에 여러 코루틴을 구동할 수 있다.
    • 이 코드는 CoroutineScopeDispatchers.Default로 설정하여 코루틴을 시작한다. 이는 백그라운드에서 CPU 집약적인 작업을 수행하는데 적합한 스레드 풀에서 실행된다.
    • 이 부분에서 연산 작업이 비동기적으로 수행된다.
  2. GlobalScope.launch(Dispatchers.Main):

    • 연산 작업이 완료된 후, GlobalScope.launch(Dispatchers.Main)를 사용하여 메인 스레드에서 결과를 처리한다.
    • GlobalScope는 앱의 전체 수명 동안 살아있는 전역 코루틴 스코프다. 여기서 Dispatchers.Main를 사용하여 UI 업데이트 작업을 메인 스레드에서 실행한다.

Dispatchers

  • Dispatchers.Main: 메인 스레드에서 동작하는 코루틴
  • Dispatchers.IO: 파일에 읽거나 쓰기 또는 네트워크 작업 등에 최적화
  • Dispatchers.Default: CPU를 많이 사용하는 작업을 백그라운드에서 실행

요약

  1. 액티비티의 상태

    • 활성(Active): 액티비티가 화면에 표시되며 사용자와 상호작용이 가능한 상태.
    • 일시정지(Paused): 액티비티가 화면에 있지만 사용자 상호작용이 불가능한 상태.
    • 비활성(Stopped): 액티비티가 화면에 표시되지 않는 상태.
  2. 액티비티의 상태 저장

    • 데이터 저장 필요성: 화면 회전 등으로 액티비티가 종료되고 재생성될 때, 데이터를 Bundle 객체를 사용하여 저장하고 복원할 수 있다.
    • onSaveInstanceState(): 액티비티 종료 시 저장할 데이터는 이 메서드를 통해 Bundle에 저장해야 한다.
  3. ANR(Application Not Responding) 문제

    • 주요 원인:
      • 메인 스레드에서 긴 작업 수행.
      • 브로드캐스트 리시버에서의 시간 초과.
      • 서비스에서의 시간 초과.
    • ANR 시간 한계:
      • Activity 및 BroadcastReceiver: 5초.
      • ContentProvider: 10초.
      • Service: 20초.
  4. ANR 방지 방법

    • 백그라운드 스레드 사용: 긴 작업은 메인 스레드가 아닌 별도의 스레드에서 수행.
    • 비동기 처리: 네트워크 요청 및 파일 작업을 비동기적으로 처리.
    • 서비스 최적화: 서비스에서 긴 작업은 별도의 스레드에서 수행하며 적절한 작업 관리 클래스를 사용.
    • UI 작업 최소화: 메인 스레드에서는 가벼운 작업만 수행.
  5. 코루틴을 이용한 ANR 해결

    • 코루틴: 비동기 경량 스레드로, 스레드보다 가볍고 더 많은 기능 제공.

    • Dispatchers 종류:

      • Dispatchers.Main: 메인 스레드에서 동작하는 코루틴.
      • Dispatchers.IO: 파일 읽기/쓰기, 네트워크 작업 등 비동기 I/O 작업에 최적화.
      • Dispatchers.Default: CPU 집약적인 백그라운드 작업에 최적화.
    • 코루틴 예제:

      • 백그라운드 작업: CoroutineScope(Dispatchers.Default).launch { ... }를 사용하여 CPU 집약적인 작업을 비동기적으로 수행.
      • UI 업데이트: GlobalScope.launch(Dispatchers.Main) { ... }를 사용하여 메인 스레드에서 UI를 업데이트.
profile
화려한 외면이 아닌 단단한 내면

0개의 댓글