ANR
고친 사람 github-actions[bot]
ANR 은 안드로이드 앱이 운영체제가 건넨 일에 제때 답하지 못해서 멈춘 것으로 판정받는 일입니다. 사용자의 터치에 5초 넘게 답하지 못하는 경우가 대표적입니다. 화면에는 앱을 닫을지 더 기다릴지 묻는 창이 뜹니다. 대개 화면을 맡은 스레드 하나가 오래 걸리는 일에 묶여서 생깁니다.
쉽고 빠른 이해
ANR 은 멈춘 것처럼 보이는 앱을 운영체제가 찾아내 사용자에게 알리는 일입니다. 버튼을 눌렀는데 5초가 지나도 앱이 반응하지 않으면 「응답 없음」 창이 뜹니다.
이렇게 하는 까닭은 사용자가 얼어붙은 화면 앞에서 마냥 기다리지 않게 하려는 것입니다. 감시가 없으면 사용자는 앱이 일하는 중인지 죽은 것인지 알 길이 없습니다.
어떻게 도는가:
- 앱의 화면과 입력은 스레드 하나가 차례대로 처리합니다
- 그 스레드가 네트워크 호출처럼 오래 걸리는 일을 처리하는 동안 뒤의 입력은 줄을 서서 기다립니다
- 운영체제가 정한 시간을 넘기면 앱이 멈췄다고 판정합니다
대가는 개발자가 집니다. 오래 걸리는 일은 전부 다른 스레드로 넘겨야 합니다. 결과를 화면에 그릴 때만 원래 스레드로 돌아옵니다.
상세
ANR 은 Application Not Responding 의 줄임말입니다. 「앱이 응답하지 않음」이라는 뜻입니다. 안드로이드 앱이 정해진 시간 안에 할 일을 끝내지 못하면 운영체제가 이 이름으로 판정합니다.
이 절은 화면을 맡은 스레드, 운영체제가 주는 시간, ANR 을 부르는 코드와 피하는 법을 차례로 봅니다.
화면을 맡은 스레드 하나
안드로이드 앱에는 화면을 그리고 입력을 받는 스레드가 하나 있습니다. 이 스레드를 메인 스레드라고 부릅니다. 화면을 맡는다고 해서 UI(User Interface, 사용자 인터페이스) 스레드라고도 합니다. 화면 요소를 고치는 일은 이 스레드에서만 하도록 정해져 있습니다.
메인 스레드는 할 일 목록 하나를 앞에서부터 하나씩 꺼내 처리합니다. 이 목록이 메시지 큐입니다. 터치 입력, 화면 다시 그리기, 개발자가 쓴 콜백이 전부 이 한 줄에 섭니다.
한 줄이라서 앞의 일이 끝나지 않으면 뒤의 일은 시작도 못 합니다. 아래 그림은 클릭 처리 하나가 3초째 끝나지 않을 때 큐의 모습입니다.
flowchart TD
T["메인 스레드"] --> M1
subgraph 큐["메시지 큐"]
M1["클릭 처리 · 네트워크 응답을 3초째 기다리는 중"]
M2["화면 다시 그리기 · 대기"]
M3["터치 입력 · 대기"]
M4["다음 터치 입력 · 대기"]
M1 --> M2 --> M3 --> M4
end
맨 앞의 클릭 처리가 끝나야 화면이 다시 그려집니다. 그동안 사용자가 화면을 몇 번 눌러도 그 입력은 줄 뒤에 쌓이기만 합니다. 사용자 눈에는 앱이 얼어붙은 것으로 보입니다.
운영체제가 주는 시간
운영체제는 앱에 일을 건넬 때 타이머를 함께 켭니다. 정해진 시간 안에 앱이 그 일을 끝냈다고 답하지 않으면 ANR 로 판정합니다. 일의 종류마다 주는 시간이 다릅니다.
입력 말고도 운영체제가 앱에 건네는 일이 있습니다. 브로드캐스트 리시버는 충전 시작처럼 기기에서 일어난 일을 전해 듣는 부품입니다. 서비스는 화면 없이 뒤에서 일을 이어 가는 부품입니다. 둘 다 콜백이 메인 스레드에서 돌기 때문에 입력과 같은 감시를 받습니다.
| 운영체제가 건넨 일 | 기본 시간 제한 |
|---|---|
| 터치 · 키 입력 | 5초 |
| 브로드캐스트 리시버가 받은 급한 알림 | 10~20초 |
| 브로드캐스트 리시버가 받은 보통 알림 | 60~120초 |
| 포그라운드 서비스 시작 | 20초 |
| 그 밖의 서비스 시작 | 200초 |
브로드캐스트의 두 줄은 알림을 보내는 쪽이 가릅니다. 보내는 쪽이 급하다는 표시를 붙이면 급한 알림이 되어 짧은 시간을 받습니다. 표시가 없으면 보통 알림입니다.
서비스의 두 줄은 기준이 다릅니다. 포그라운드 서비스는 사용자가 알도록 알림 막대에 표시를 띄워 둔 채 도는 서비스입니다. 음악 재생이 그런 예입니다. 표시 없이 도는 서비스는 훨씬 긴 시간을 받습니다.
표의 값은 기본값입니다. 기기 제조사가 바꿀 수 있습니다.
브로드캐스트 칸이 범위인 까닭은 기기가 바쁠 때를 봐 주기 때문입니다. 할 일이 몰리면 앱은 CPU(Central Processing Unit, 중앙처리장치)를 차례로 나눠 받느라 일을 늦게 시작합니다. 앱 잘못이 아닌 이 지연이 있으면 운영체제가 시간 제한을 최대 두 배까지 늘려 줍니다. 범위로 바뀐 것은 안드로이드 14부터입니다. 13 이하에서는 10초와 60초로 고정이었습니다.
다른 앱의 콘텐츠 프로바이더에 데이터를 물을 때도 ANR 이 날 수 있습니다. 콘텐츠 프로바이더는 앱이 자기 데이터를 다른 앱에 내주는 창구입니다. 이 경우에는 고정된 시간이 없습니다. 묻는 앱이 감시 시간을 정해 둔 경우에만 감시가 켜집니다. 그 시간 안에 답하지 못하면 데이터를 내주는 앱이 ANR 판정을 받습니다.
시간을 넘긴 뒤
입력 하나가 5초 안에 처리되지 않을 때의 흐름은 이렇습니다. 운영체제는 입력을 건네고 기다립니다. 시간이 다 되면 메인 스레드 대신 사용자에게 말을 겁니다.
sequenceDiagram
participant 사용자
participant 시스템 as 운영체제
participant 메인 as 메인 스레드
사용자->>시스템: 화면을 누른다
시스템->>메인: 입력을 건네고 타이머를 켠다
Note over 메인: 앞선 일이 안 끝나 답하지 못한다
Note over 시스템: 5초가 지난다
시스템->>사용자: 응답 없음 창을 띄운다
이 창이 없으면 사용자는 앱이 아직 일하는 중인지 멈춘 것인지 알 길이 없습니다. 앱이 화면 앞에 있으면 사용자는 이 창에서 앱을 강제로 닫거나 더 기다릴 수 있습니다.
앱이 화면 뒤에 있으면 창 없이 조용히 종료됩니다. 사용자는 왜 앱이 사라졌는지 모르고 넘어갑니다.
판정하는 순간 운영체제는 앱 안 스레드들의 스택 트레이스를 떠서 남깁니다. 스택 트레이스는 각 스레드가 어느 함수의 어느 줄에서 멈춰 있었는지 적은 목록입니다. 원인을 찾을 때 개발자가 제일 먼저 보는 기록입니다.
ANR 을 부르는 흔한 코드
ANR 의 원인은 거의 하나로 모입니다. 메인 스레드가 오래 걸리는 일을 직접 붙잡고 있는 것입니다. 아래는 그런 일의 흔한 모습입니다.
| 메인 스레드에서 한 일 | 왜 오래 걸리나 |
|---|---|
| 네트워크 호출 | 응답이 올 때까지 스레드가 멈춰 기다린다 |
| 파일 · 데이터베이스 읽기와 쓰기 | 저장 장치가 답할 때까지 멈춘다 |
| 큰 계산 | 큰 데이터를 정렬하거나 메모리에 큰 구조를 짓는다 |
| 다른 스레드가 쥔 잠금 기다리기 | 그 스레드가 놓을 때까지 메인 스레드가 선다 |
| 앱이 켜질 때 긴 초기화 | 첫 화면이 뜨기 전에 초기화가 길게 돈다 |
앞의 둘은 블로킹 호출입니다. 블로킹은 결과가 올 때까지 부른 스레드가 다른 일을 못 하고 서 있는 방식입니다. 네 번째는 락 경합이라고 부릅니다. 메인 스레드 코드만 봐서는 원인이 안 보여 찾기 어렵습니다.
아래는 첫 번째 경우를 Kotlin 으로 쓴 것입니다. 버튼을 누르면 서버에서 프로필을 받아 이름을 화면에 적습니다.
button.setOnClickListener {
val name = fetchName() // 응답까지 멈춤
nameView.text = name // 그 뒤에야 그림
}
fetchName() 이 네트워크 응답을 기다리는 동안 메인 스레드는 아무것도 못 합니다. 서버가 5초 넘게 답하지 않으면 그 사이 들어온 터치 입력이 시간 제한에 걸립니다.
오래 걸리는 일을 넘기는 방법
피하는 길은 하나입니다. 오래 걸리는 일은 다른 스레드에 맡깁니다. 메인 스레드는 결과를 화면에 적는 일만 합니다. 메인 스레드가 아닌 스레드는 백그라운드 스레드입니다.
앞의 코드를 코루틴으로 고치면 이렇습니다. 코루틴은 멈췄다가 이어서 도는 함수입니다. 기다리는 동안 스레드를 차지하지 않아서 이런 넘기기를 짧게 적을 수 있습니다.
button.setOnClickListener {
scope.launch {
val name = withContext(IO) {
fetchName() // 다른 스레드
}
nameView.text = name // 메인으로 복귀
}
}
scope.launch 는 코루틴 하나를 시작합니다. 그 안의 withContext(IO) 블록은 입출력용 스레드에서 돕니다. 블록이 끝나면 다음 줄은 다시 메인 스레드에서 돕니다. 그때 이름을 화면에 적습니다. 네트워크를 기다리는 동안에도 메인 스레드는 입력을 계속 받습니다.
브로드캐스트 리시버와 서비스도 같은 원리를 따릅니다. 콜백 안에서는 일을 받아 넘기기만 하고 바로 돌아옵니다. 긴 일은 백그라운드 스레드나 WorkManager 같은 작업 예약 장치에 맡깁니다.
5초보다 훨씬 앞선 선
ANR 은 사용자가 겪는 지연의 마지막 선입니다. 사용자는 반응이 100~200밀리초만 늦어도 앱이 굼뜨다고 느낍니다. ANR 이 안 난다고 사용자가 기다리지 않는 것은 아닙니다.
그래서 안드로이드는 메인 스레드에서 디스크나 네트워크를 건드리는 순간을 개발 중에 미리 알려 주는 StrictMode 라는 검사 모드를 둡니다. 실제 사용자 기기에서 난 ANR 은 Google Play 가 모아서 앱마다 비율로 셉니다.
서버에서 닮은 장면
백엔드에도 같은 구조로 멈추는 곳이 있습니다. Node.js 처럼 스레드 하나가 이벤트 루프를 돌며 요청을 차례로 처리하는 서버가 그렇습니다.
이 서버에서 요청 하나가 오래 걸리는 계산을 돌리면 다른 요청이 전부 줄을 서서 기다립니다. 메인 스레드를 막아 화면이 얼어붙는 것과 같은 일입니다. 다른 점은 이를 감시해 알려 주는 운영체제가 서버 쪽에는 따로 없다는 것입니다. 대개 앞단의 타임아웃이나 헬스 체크가 대신 잡습니다.
관련 항목
ANR 이 감시하는 안드로이드 앱 구조
Android · 메인 스레드 · Looper · MessageQueue · Handler (Android) · 액티비티 · 브로드캐스트 리시버 · 서비스 (Android) · 콘텐츠 프로바이더 · 생명주기 · Application (Android)
ANR 을 부르는 메인 스레드 차단 원인
메인 스레드 차단 · 블로킹 · 블로킹 호출 · 블로킹 입출력 · 락 경합 · 데드락 · 콜드 스타트
ANR 을 피하려고 일을 넘겨받는 도구
백그라운드 스레드 · 코루틴 · Kotlin 코루틴 · 스레드 풀 · WorkManager · 비동기 · 논블로킹 입출력
ANR 을 찾아내고 세는 도구와 지표
StrictMode · 스택 트레이스 · 스레드 덤프 · Android vitals · 사용자 인지 ANR율 · 충돌 보고 · Google Play
ANR 과 같은 방식으로 멈춤을 감시하는 장치
ANR 과 견주는 다른 앱 실패
앱 크래시 · 프레임 드롭 · 잰크 · 메모리 부족 킬러 · 운영체제 판올림 파손
ANR 과 같은 원리로 멈추는 서버 쪽 구조
이벤트 루프 · Node.js · 단일 스레드 · 헤드 오브 라인 블로킹
다른 이름: Application Not Responding · 앱 응답 없음 · 안드로이드 응답 없음