Android 액티비티 ANR 문제와 코루틴

timothy jeong·2021년 11월 13일
0

Android with Kotlin

목록 보기
40/69

ANR 문제

ANR 은 activity not response 의 약자로, 액티비티가 응답하지 않으는 오류 상황을 의미한다. 액티비티를 작성할 때 ANR 을 고려하지 않으면 앱이 수시로 종료될 수 있다.

액티비티로 구성한 앱 화면은 사용자 이벤트에 빠르게 반응해야 한다. 그런데 액티비티가 사용자 이벤트에 5초 이내에 반응하지 않으면 ANR 오류가 발생한다. 이처럼 익태비티에서 사용자 이벤트를 처리하지 못하는 이유는 액티비티를 실행한 시스팀에서 발생한 수행 흐름에서 이벤트를 처리하기 때문이다. 즉, 시스템의 수행 흐름에서 시간이 오래 걸리는 작업이 끝나지 않으면 사용자 이벤트에 반응하지 못하는 것이다.

시스템에서 액티비티를 실행하는 수행 흐름을 '메인 스레드' 또는 화면을 출려하는 수행 흐름이라는 의미의 'UI 스레드' 라고 한다. 이 메인 스레드가 시간이 올래걸리는 작업을 실행한다고 해서 그 자체로 오류가 발생하지는 않는다. 아무리 오래 걸려도 사용자가 액티비티 화면을 터치하지 않는 등 이벤트가 없다면 오류가 발생하지 않는다. 그러나 사용자가 언제 화면을 터치할지 모른다. 따라서 액티비티를 작성할 때는 항상 ANR 오류를 고려해야 한다.

액티비티에서 시간이 올래 걸리는 대표적인 작업은 서버와 통신하는 네트워크이다. 정상적인 상황에서는 서버와 연결하여 데이터를 주고 받는데 1~2초면 끝나지만, 네트워크 상황에 따라서 ANR 문제가 발생할 수 있다.

물론 대부분의 네트워크 통신을 지원하는 전문 라이브러리(Volley , Retrofit2)를 사용해서 만들고, 이 라이브러리는 내부적으로 ANR 문제를 고려해 작성된다. 이런 라이브러리를 이용할 때는 개발자가 ANR 문제를 고려하지 않아도 된다.

ANR 문제를 해결하는 방법은 액티비티를 실행하는 메인 스레드 이외에 실행 흐름을 만들어서 시간이 오래 걸리는 작업을 담당하게 하면 된다. 그러면 개발자가 만든 스레드가 시간이 올래 걸리는 작업을 수행중이더라도 메인 스레드는 이벤트 처리를 할 수 있다. 하지만 화면 변경은 액티비티를 실행하는 메인 스레드만 할 수 있으므로, 이 경우 화면을 변경할 수 없게 된다.

API 30 이전에는 스레드 핸들러 혹은 AsyncTask 를 이용했지만 API 30 부터는 deprecated 되었다.

코루틴으로 ANR 오류 해결

코틀린 언어가 제공하는 코루틴이라는 기능을 이용하면 ANR 도 해결하고 화면도 변경할 수 있다. 코루틴을 제대로 이해하려면 공부할게 정말 많다. 기초적인 내용만 살펴보자

코루틴이란

코루틴은 비동기 경량 스레드(non-blocking lightweight thread) 라고 할 수 있다. 코루틴은 안드로이드 시스템이 아니라 프로그래밍 언어에서 제공하는 기능이다. 코틀린에서는 1.3 부터 공식으로 지원한다.

프로그램이 단일 흐름으로 실행되면 작업이 차례대로 이뤄질 뿐 함께 처리되지 않는다. 이와 달리 코루틴은 수행 흐름을 여러 갈래로 만들어 여러 작업을 함께 처리한다. 비동기 처리 방식과 간다.

일반적으로 비동기 처리라면 스레드를 생각하기 쉽지만, 스레드는 성능 이슈가 있고 자유롭게 제어할 수 없거나 구현하기 복잡하다. 따라서 요즘은 스레드를 이용하지 않고 RX 프로그래밍이나 코루틴으로 비동기 처리를 구현한다. 스레드보다 가볍고 더 많은 기능을 제공한다.

코루틴은 비동기 처리 대비 아래의 장점을 갖는다

  • 가볍다
  • 메모리 누수가 적다
  • 취소 등 다양한 기능을 지원한다
  • 많은 제트팩 라이브러리에 적용되어 있다

안드로이드에서 코루틴 이용

안드로이드 앱에서 코루틴을 사용하려면 그레이들 파일에 아래의 의존성을 추가해야한다.

implemetation 'org,jetbrains.kotlinx:kotlinx-coroutines-anrooid:1.3.9'

시가닝 오래 걸리는 작업을 한다고 치고 코루틴을 사용해보자.

var sum = 0L
var time = measureTimeMillis {
    for (i in 1...2000000000) {
       sum += i
   }
}
binding.resultView.text = "sum : $sum"

1 부터 2억까지 더한 값을 화면에 표시하는 코드이다. 5초 이상 처리 시간이 필요해서 ANR이 발생했다고 치자, 코루틴을 이용해서 ANR을 해결하는 방법은 아래와 같다.

val channel = Channel<Int>()
val backgroundScope = CoroutineScope(Dispatchers.Default + Job())
backgroudScope.lauch {
    var sum = 0L
    var time = measureTimeMillis {
        for (i in 1..2000000000) {
            sum += i
        }
    }
    channel.send(sum.toInt())
}

val mainScope = GlobalScope.launch(Dispatchers.Main) {
    channel.consumeEach {
        binding.resultView.text = "sum : $it"
    }
}

코루틴을 구동하려면 먼저 스코프(scope) 를 준비해야한다. 그리고 스코프에서 코루틴을 구동한다. 스코프는 성격이 같은 코루틴을 묶는 개념으로 이해하면 된다. 한 스코프에 여러 코루틴을 구동할 수 있으며 한 어플리케이션에 여러 스코프를 만들 수도 있다.

코루틴 스코프는 이 처럼 클래스를 직접 구현할 수도 있고 GlobalScope, ActorScope, ProducerScope 등 코틀린 언어가 제공하는 스코프를 이용할 수도 있다. 위 코드에서는 스코프 클래스를 직접 구현하는 방법으로 val backgroundScope = CoroutineScope(Dispatchers.Default + Job()) 를 통해 스코프를 만들었다.
이때 클래스 생성자에 전달된 Dispatchers.Default 는 백그라운드에서 동작하여 시간이 올래걸리는 작업에 대한 스코프를 만듦을 표시하는 것이다.

이러한 디스패처들은 스코프에서 구동한 코루틴이 어디에서 동작해야 하는지를 나타낸다.

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

백그라운드 스코프에서는 Dispatchers.Default 를, 메인 스코프에서는 Dispatchers.Main 을 지정했다. 메인 디스페처에서는 메인 스레드에서 동작하는 코루틴을 만들기 때문에 UI를 변경할 수 있지만, 시간이 많이 걸리지 않는 작업을 해야한다.

위 예시에서는 디폴트 디스패처 스코프에서 시간이 올래 걸리는 작업을 처리하고, 그 결과는 메인 디스패처 스코프의 코루틴에서 화면에 출력하도록 하였다.

그러면 서로 다른 스코프의 코루틴간에는 데이터를 어떻게 주고 받았을까. 백그라운드 스코프를 만든 다음에 채널(channel) 을 만들었다. 이 클래스는 코루틴의 값을 전달받을 수 있는 방법을 제공한다. 채널은 큐 알고리즘과 비슷하며 채널의 send() 함수로 데이터를 전달하면 그 데이터를 받는 코루틴에서는 receive() 나 consumeEach() 등의 함수로 데이터를 받는다.

profile
개발자

0개의 댓글