|

안드로이드 ANR은 왜 생길까: 메인 스레드를 막는 진짜 원인 정리

안드로이드 ANR과 메인 스레드 병목을 설명하는 대표 이미지
ANR을 blocking I/O, 락 대기, Binder 호출, 무거운 프레임 작업 관점으로 정리한 글

안드로이드 ANR 원인을 한 줄로 줄이면 메인 스레드가 다음 일을 제때 처리하지 못해서입니다. 많은 설명이 메인 스레드에서 네트워크를 돌리면 안 된다는 말에서 멈추지만, 실제로는 디스크 I/O, 락 대기, 느린 Binder 호출, 무거운 프레임 작업도 결국 같은 문제로 이어집니다. 이번 글에서는 ANR을 메인 스레드 관점으로 다시 정리해보겠습니다.


ANR은 정확히 무엇일까

ANR은 Application Not Responding의 약자입니다. 핵심은 앱이 완전히 죽었다는 뜻이 아니라, 사용자 입력이나 화면 갱신에 필요한 일을 메인 스레드가 너무 오래 처리하지 못했다는 데 있습니다. 안드로이드 공식 문서도 UI thread가 너무 오래 blocked 되면 ANR이 발생한다고 설명합니다.

특히 사용자가 버튼을 누르거나 화면을 터치했는데 일정 시간 안에 앱이 반응하지 않으면 input dispatch timeout 기반 ANR로 이어질 수 있습니다. 기본값 예시로는 5초가 자주 언급되지만, 공식 문서는 이 시간이 기기 제조사에 따라 달라질 수 있다고도 안내합니다. 즉 ANR은 에러 이름이기 전에 메인 스레드 정체 현상입니다.


왜 메인 스레드가 그렇게 중요할까

안드로이드의 main thread는 화면에 보이는 일 대부분을 맡습니다. 사용자 입력을 받고, 콜백을 실행하고, 화면을 그리는 흐름이 여기서 만납니다. 그래서 메인 스레드가 막히면 터치 반응, 화면 갱신, 다음 콜백 실행이 같이 밀립니다.

ANR은 특정 API 하나의 문제가 아니라 메인 스레드 작업 큐가 막힌 결과입니다. 이 관점이 있어야 네트워크만 피하면 끝이라는 식의 좁은 해석에서 벗어날 수 있습니다.

  • 사용자 입력을 제때 처리하지 못한다
  • 화면 그리기 작업이 밀린다
  • 다음 메시지와 콜백 실행도 함께 늦어진다

안드로이드 ANR 원인 1: main thread에서 오래 걸리는 작업

가장 익숙한 원인입니다. 메인 스레드에서 디스크 읽기, 데이터베이스 접근, 큰 JSON 파싱, 무거운 이미지 처리 같은 blocking 작업을 오래 수행하면 입력 이벤트가 뒤로 밀립니다. 공식 문서도 blocking I/O를 대표 원인으로 직접 언급합니다.

viewModelScope.launch {
    val text = File(filesDir, "large.json").readText()
    val result = parseHugeJson(text)
    _uiState.value = UiState.Success(result)
}

이 코드는 코루틴을 쓰고 있지만, 시작 컨텍스트가 메인이라면 파일 읽기와 파싱이 그대로 메인 스레드에 얹힐 수 있습니다. 즉 코루틴 자체가 해답이 아니라 무거운 일을 어떤 스레드에서 돌리느냐가 핵심입니다.


안드로이드 ANR 원인 2: 느린 코드보다 더 자주 놓치는 락 대기

실무에서는 내 코드가 오래 실행됐다기보다, 내 코드는 별로 안 했는데 기다리다 멈췄다가 더 헷갈립니다. 바로 lock contention입니다. 메인 스레드가 어떤 공유 자원의 락을 기다리는데 다른 스레드가 그 락을 오래 잡고 있으면, 메인 스레드는 아무 일도 못 하고 서 있게 됩니다.

  1. 백그라운드 스레드가 큰 데이터 정리를 하면서 락을 오래 잡는다
  2. 메인 스레드가 같은 객체에 접근하려고 한다
  3. 메인 스레드는 대기 상태가 된다
  4. 사용자는 버튼을 눌러도 반응이 없다고 느낀다

이 경우에는 메인 스레드 함수만 보면 답이 잘 안 나옵니다. 그래서 ANR 분석에서는 메인 스레드 스택만이 아니라 락을 쥔 다른 스레드도 같이 봐야 합니다.


안드로이드 ANR 원인 3: Binder 호출도 main thread를 막을 수 있다

ANR을 네트워크와 DB만으로 설명하면 Binder 쪽 병목을 놓치기 쉽습니다. 공식 문서는 slow binder call과 many consecutive synchronous binder calls도 대표 원인으로 언급합니다. 메인 스레드에서 시스템 서비스나 다른 프로세스와 동기적으로 계속 통신하면, 앱 코드가 직접 무거운 계산을 하지 않아도 응답을 기다리느라 메인 스레드가 묶일 수 있습니다.

특히 반복문 안에서 동기 Binder 호출을 여러 번 이어 붙이면 작은 지연이 누적되어 크게 느려질 수 있습니다. 그래서 내 함수는 짧은데 왜 멈추지라는 상황에서는 호출 대상이 로컬 계산인지, 시스템 서비스 응답 대기인지도 같이 봐야 합니다.


안드로이드 ANR 원인 4: 한 프레임 안에 일을 너무 많이 넣는 경우

ANR은 갑자기 생기는 큰 사고처럼 보이지만, 그 전에 버벅임이 먼저 나타나는 경우가 많습니다. 안드로이드는 대략 16ms 안팎으로 다음 화면 갱신 작업을 처리해야 부드럽게 보입니다. 그런데 메인 스레드 큐 안에 긴 작업이 너무 많으면 먼저 jank가 생기고, 더 심해지면 입력 처리까지 밀리면서 ANR로 이어질 수 있습니다.

  • 한 번의 상태 변경에 너무 많은 recomposition 또는 view update가 묶여 있다
  • 리스트 diff, 정렬, 포맷팅, 이미지 준비를 한 번에 메인 스레드에서 처리한다
  • 화면 전환 직전에 무거운 초기화가 몰린다

모든 ANR이 계산량 때문은 아니지만, 무거운 프레임은 ANR로 가는 전조가 될 수 있습니다.


코루틴을 써도 ANR이 생기는 이유

코루틴은 작업을 구조적으로 다루게 도와주지만, 자동으로 main thread를 비워주지는 않습니다. Dispatchers.Main에서 blocking I/O를 수행하거나, main thread에서 락을 기다리게 만들거나, 메인에서 동기 Binder 호출을 반복하거나, 계산량이 큰 변환을 메인에서 하면 코루틴을 써도 ANR은 생깁니다.

  1. 이 작업은 지금 어느 dispatcher에서 도는가
  2. 이 코드 안에 기다리는 지점이 있는가
  3. 결과를 만들기 전에 메인에서 너무 많은 계산을 하는가
  4. 상태 반영 한 번에 UI 일이 과도하게 몰리지 않는가

ANR을 볼 때 가장 먼저 확인할 체크리스트

  1. 메인 스레드가 무엇을 하다 멈췄는지 본다
  2. 직접 실행 중인지, 락을 기다리는지, Binder 응답을 기다리는지 구분한다
  3. 디스크·DB·네트워크·큰 파싱 같은 blocking I/O가 메인에 없는지 본다
  4. 반복되는 동기 호출이 없는지 본다
  5. 최근 UI 갱신 작업이 한 번에 너무 무거워지지 않았는지 본다
  6. 개발 단계라면 StrictMode로 accidental disk or network access를 잡는다

이 체크리스트만 있어도 ANR을 느린 함수 하나로만 보는 오해를 많이 줄일 수 있습니다. 실무에서는 blocking I/O, lock contention, binder 병목, expensive frame 네 갈래로 나눠 대응하면 원인 분류가 훨씬 쉬워집니다.


마무리

안드로이드 ANR 원인은 결국 메인 스레드가 다음 일을 제때 처리하지 못하는 데 있습니다. 그래서 진짜 질문은 무슨 API를 썼나보다 왜 메인 스레드가 비지 못했나입니다. 디스크 I/O일 수도 있고, 락 대기일 수도 있고, Binder 응답일 수도 있고, 한 프레임에 너무 많은 일을 몰아넣은 결과일 수도 있습니다. 이 기준으로 보면 ANR은 훨씬 덜 막연해집니다.

더 넓은 흐름은 repeatOnLifecycle vs launchWhenStarted, 안드로이드 코루틴 기초 정리, 안드로이드 UDF는 왜 중요할까, Jetpack Compose 성능은 어디서 느려질까를 함께 보면 더 잘 연결됩니다. 공식 기준은 ANRs, Diagnose and fix ANRs, Better performance through threading 문서를 같이 보는 것이 가장 안전합니다.

함께보면 좋은 글