본문 바로가기
깡샘 코틀린

13-5 액티비티 ANR 문제와 코루틴

by 농농씨 2023. 7. 1.

ANR 문제란?

액티비티 컴포넌트에서 마지막으로 다룰 주제는 ANR이다. ANR(activiy not response)은 액티비티가 응답하지 않는 오류 상황을 의미한다. 액티비티를 작성할 때 ANR을 고려하지 않으면 앱이 수시로 종료될 수 있다.

 

시스템에서 액티비티를 실행하는 수행 흐름메인 스레드 또는 화면을 출력하는 수행 흐름이라는 의미에서 UI 스레드라고 한다.

 

메인 스레드가 오래 걸리는 작업을 실행한다고  그 자체로 오류가 생기는 건 아니지만, 사용자가 액티비티 화면을 터치하는 등 이벤트가 발생하면 오류가 생길 수 있으므로 액티비티를 작성할 때 항상 ANR 오류를 고려해야 한다.

 

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

물론 앱은 네트워크 전문 라이브러리를 사용해 만들어서 이런 문제들이 이미 고려되어 작성되어있다.

 

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

그런데 이 방법으로 대처하면 ANR 오류는 해결되어도 화면을 변경할 수 없다는 문제가 생긴다. 왜냐하면 화면 변경은 개발자가 만든 스레드에서는 할 수 없고 액티비티를 출력한 메인 스레드만 할 수 있기 때문!

 

 

코루틴으로 ANR 오류 해결

코틀린 언어가 제공하는 코루틴(coroutine) 기능을 이용하면 액티비티의 ANR 오류를 해결하는 동시에 개발자가 만든 수행 흐름에서 화면 변경하기도 가능해진다.

 

코루틴이란?

한마디로 비동기 경량 스레드(non-blocking lightweight thread)라고 요약할 수 있다. 이건 안드로이드 시스템이 아니라 프로그래밍 언어에서 제공한다.

coroutine이라는 단어에서 co는 '함께'라는 뜻이고 routine은 작업의 처리 단위를 뜻한다. 즉, 코루틴은 어떤 작업을 함께 처리한다는 의미이다. 프로그램이 단일 흐름이면 작업이 차례대로 이뤄지고 함께 처리되지는 않는다. 이와 반대로 코루틴은 수행 흐름을 여러 갈래로 만들어 여러 작업을 함께 처리한다. 결국 비동기 처리 방식과 같다.

일반적으로 비동기 처리라면 스레드를 생각하기 쉬운데 스레드보다 코루틴이 더 가벼우면서 더 많은 기능을 제공한다.

구글도 다음과 같이 코루틴을 추천한다.

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

 

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

안드로이드 앱에서 코루틴을 이용하려면 그래들 파일의 dependencies 항목에 다음처럼 등록해야 한다.

// 코루틴 등록
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'

 

시간이 오래 걸리는 작업을 가정해서 코루틴 알아보자.

화면에서 버튼을 클릭하면 다음 코드처럼 시간이 오래 걸리는 작업이 진행된 후 결괏값이 버튼 아래의 텍스트 뷰에 출력되는 예이다.

// 시간이 오래 걸리는 작업 예
var sum = 0L
var time = measureTimeMillis {
	for (i in 1..2_000_000_000) {
    	sum += 1
    }
}
Log.d("kkang", "time : $time")
binding.resultView.text = "sum : $sum"

1부터 20억까지 더하는 단순 작업이다. 결과 나오기 전에 글 입력하려고 화면 터치하면  ANR 오류가 발생한다.

ANR 오류를 해결하고자 스레드-핸들러 구조로 다시 작성해본다고 되어있으나 API 레벨 30에서 deprecated 되었으니 난 타이핑을 생략하겠음

// 스레드-핸들러 구조로 작성한 소스
(... 생략 ...)

사용자가 버튼을 클릭했을 때 실행되며, 시간이 오래 걸리는 작업을 메인 스레드가 아닌 개발자 스레드로 작성했으므로 ANR 문제는 발생하지 않는다. 연산 작업이 실행되는 동안에도 사용자가 화면의 에디트 텍스트에 글을 입력할 수 있다.

그런데 개발자 스레드에서 UI는 변경할 수 없으므로 텍스트 뷰에 결과를 직접 출력하지 않고 메인스레드에 의뢰하여 처리했다. 개발자 스레드에서 sendMessage() 함수를 호출하면서 매개변수로 데이터를 넘기면 그 순간 메인 스레드가 handleMessage() 함수를 자동으로 호출한다.

 

이제 이 코드를 코루틴으로 작성해보자.

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

코루틴 스코프는 CoroutineScope를 구현한 클래스의 객체이며 직접 구현할 수도 있고 GlobalScope, ActorScope, ProducerScope 등 코틀린 언어가 제공하는 스코프를 이용할 수도 있다.

// 코루틴으로 작성한 소스
val channel = Channel<Int>()
val backgroundScope = CoroutineScope(Dispatchers.Default = Job()) 
// 디스패처가 디폴트라서 백그라운드에서 동작(시간소요작업담당)
backgroundScope.launch {
	var sum = 0L
    var time = measureTimeMillis {
    	for (i in 1..2_000_000_000) {
        	sum  += 1
        }
    }
    Log.d("kkang", "time : $time")
    channel.send(sum.toInt())
}
val mainScope = GlobalScope.launch(Dispatchers.Main) { 
// 디스패처가 메인이라서 메인 스레드에서 동작(화면에 결괏값 표시)
	channel.consumeEach {
    	binding.resultView.text = "sum : $it"
    }
}

위 코드에서는 backgroundScope와 mainScope를 만들었다. 여기서 주목할 점은 스코프를 만들면서 지정한 디스패처(Dispatcher)이다. 디스패처는 이 스코프에서 구동한 코루틴이 어디에서 동작해야 하는지를 나타낸다. backgroundScope에는 Dispatchers.Default를 지정했으며 mainScope에는 Dispatchers.Main을 지정했다.

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

Dispatchers.Main으로 만든 스코프에서 실행한 코루틴은 메인 스레드에서 동작한다. 따라서 UI를 변경할 수 있다. 하지만 메인 스레드는 사용자 이벤트를 처리하는 곳이므로 이 코루틴에는 빨리 끝나는 작업을 맡기는 것이 좋다.

위 예에서는 시간이 오래 걸리는 작업을 Dispatchers.Default로 지정한 스코프에서 구동한 코루틴이 처리하고, 그 결과를 Dispatchers.Main으로 지정한 스코프의 코루틴에서 화면에 출력하도록 작성했다.

또한 위 예를 보면 Channel을 이용했는데 이 클래스는 코루틴의 값을 전달받을 수 있는 방법을 제공한다. Channel은 큐(queue) 알고리즘과 비슷하며 Channel의 send() 함수로 데이터를 전달하면 그 데이터를 받는 코루틴에서는 receive()나 consumeEach() 등의 함수로 데이터를 받는다

'깡샘 코틀린' 카테고리의 다른 글

14-1 브로드캐스트 리시버 이해하기  (0) 2023.07.03
섹션 0. 들어가기에 앞서  (0) 2023.07.02
13-4 태스크 관리  (0) 2023.07.01
13-3 액티비티 제어  (0) 2023.07.01
13-2 액티비티 생명주기  (0) 2023.07.01