안드로이드 ANR 방지 전략
안드로이드 ANR 방지 전략
ANR(Application Not Responding)은 앱이 메인 스레드를 너무 오랫동안 블로킹했을 때 안드로이드 시스템이 보여주는 응답 없음 경고입니다. 메인 스레드가 시스템이 정한 타임아웃 시간 내에 입력 이벤트를 처리하지 못하거나 BroadcastReceiver.onReceive()가 제때 반환되지 않으면, 안드로이드는 "대기" 또는 "앱 종료"를 묻는 다이얼로그를 표시합니다. ANR은 앱 스토어 평점과 사용자 이탈률에 직접적인 영향을 미치므로, 실무에서 가장 주의해야 할 성능 이슈 중 하나입니다.
면접에서 ANR 관련 질문은 단순히 "무엇인지" 아는 것을 넘어, 발생 원인 분석부터 아키텍처 수준의 예방 전략까지 설명할 수 있는지를 평가합니다. 실무 경험을 바탕으로 구체적인 사례를 함께 언급하시면 좋은 인상을 줄 수 있습니다.
이 글을 학습하시면 다음 내용을 이해하고 설명할 수 있게 됩니다.
- ANR을 유발하는 조건과 시스템이 적용하는 타임아웃 기준값을 설명할 수 있습니다.
- 메인 스레드를 블로킹하여 ANR로 이어지는 대표적인 패턴을 식별할 수 있습니다.
- 코루틴과 디스패처(Dispatcher)를 활용해 작업을 메인 스레드 밖으로 이동하는 방법을 적용할 수 있습니다.
StrictMode와 Android Profiler를 사용하여 잠재적 ANR 원인을 탐지하는 방법을 익힐 수 있습니다.- 메인 스레드를 UI 렌더링과 이벤트 처리에만 집중시키는 아키텍처를 설계할 수 있습니다.
ANR 발생 조건과 타임아웃 기준
안드로이드 시스템은 두 가지 메커니즘으로 메인 스레드를 감시합니다. 첫째, 입력 이벤트 처리의 경우, 메인 스레드가 터치나 키 입력과 같은 입력 이벤트에 5초 이내에 응답하지 않으면 ANR이 발생합니다. 둘째, 브로드캐스트 리시버의 경우, onReceive()가 포그라운드에서 10초, 백그라운드에서 60초 이내에 반환되지 않으면 ANR이 발생합니다.
메인 스레드는 Looper와 MessageQueue를 통해 UI 렌더링, 입력 이벤트, 생명주기 콜백, 브로드캐스트 리시버 호출 등을 순차적으로 처리하는 단일 스레드입니다. 이 스레드에서 타임아웃 시간보다 오래 걸리는 작업을 수행하면 후속 메시지 처리가 멈추게 됩니다.
면접 팁: ANR 타임아웃 수치(5초, 10초, 60초)는 자주 출제되는 포인트입니다. 각 수치가 어떤 상황에 적용되는지 정확히 구분하여 답변하시면 깊이 있는 이해를 보여줄 수 있습니다.
// 네트워크 호출이 5초 이상 걸리면 ANR을 유발합니다
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 메인 스레드에서 동기 네트워크 호출, 절대 금지!
val data = api.fetchDataSynchronously() // 메인 스레드 블로킹
displayData(data)
}
ANR로 이어지는 대표적인 블로킹 패턴은 다음과 같습니다.
- 동기 네트워크 요청: 서버 응답 지연 시 메인 스레드가 무기한 대기
- 대용량 데이터베이스 쿼리: 복잡한 조인이나 대량 레코드 조회
- 파일 I/O 작업: 대용량 파일 읽기/쓰기
- 무거운 JSON 파싱: 대규모 응답 데이터 파싱
- 복잡한 비트맵 디코딩: 고해상도 이미지 처리
Thread.sleep()호출: 테스트 목적으로 넣었다가 제거하지 않은 경우
네트워크 상태가 불안정하거나 저사양 디바이스에서는 위 작업이 5초 기준을 쉽게 초과할 수 있습니다. 특히 실제 사용자 환경은 개발 환경보다 훨씬 열악하다는 점을 항상 염두에 두어야 합니다.
메인 스레드에서 작업 분리하기
ANR을 방지하는 가장 근본적인 방법은 메인 스레드에서 블로킹 작업을 절대 수행하지 않는 것입니다. 코틀린 코루틴은 적절한 디스패처에서 작업을 일시 중단하면서도 호출 코드는 순차적으로 유지할 수 있어, 가장 간결하고 효과적인 접근법을 제공합니다.
class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _users = MutableStateFlow<List<User>>(emptyList())
val users: StateFlow<List<User>> = _users.asStateFlow()
fun loadUsers() {
// viewModelScope는 기본적으로 Dispatchers.Main에서 시작
viewModelScope.launch {
val result = withContext(Dispatchers.IO) {
repository.fetchUsers() // IO 스레드 풀에서 실행
}
_users.value = result // 메인 스레드에서 UI 상태 업데이트
}
}
}