
안드로이드 메인 스레드는 왜 이렇게 쉽게 막히는 걸까요. 결론부터 말하면, 메인 스레드는 여러 일을 동시에 처리하는 넓은 작업장이 아니라 Looper가 MessageQueue에서 다음 일을 하나씩 꺼내 실행하는 단일 차선에 가깝기 때문입니다.
이 감각이 잡히면 frame drop과 ANR도 따로 외울 필요가 없습니다. 둘 다 결국 메인 스레드가 제때 다음 일을 시작하지 못할 때 생기는 문제이기 때문입니다.
메인 스레드를 단일 차선으로 보자
메인 스레드를 그냥 UI thread라고만 외우면, 왜 작은 실수 하나가 전체 반응성을 무너뜨리는지 체감하기 어렵습니다. 안드로이드 공식 문서는 메인 스레드가 작업 큐에서 블록을 가져와 실행한다고 설명합니다.
여기서 중요한 건 아주 단순합니다. 메인 스레드는 지금 잡은 일을 끝내기 전까지 다음 일을 처리하지 못합니다. 사용자 터치, 버튼 클릭 콜백, 레이아웃 계산, 그리기 준비, 화면 상태 반영이 모두 같은 흐름 위에 올라오면, 앞의 일이 길어지는 순간 뒤의 일은 전부 밀립니다.
Looper와 MessageQueue 역할
Looper는 스레드의 메시지 루프를 돌리는 존재입니다. MessageQueue는 그 루프가 나중에 dispatch할 메시지 목록을 들고 있는 저수준 큐입니다. 보통 앱 개발자는 MessageQueue에 직접 손대기보다, Looper와 연결된 Handler를 통해 작업을 넣습니다.
- 어딘가에서 메인 스레드가 나중에 처리해야 할 일이 들어옵니다.
- 그 일은 메시지나 Runnable 같은 형태로 큐에 들어갑니다.
- Looper는 큐에서 지금 처리할 차례의 일을 꺼냅니다.
- 메인 스레드는 그 일을 끝까지 실행합니다.
- 일이 끝나야 다음 입력과 다음 그리기 작업이 진행됩니다.
이 구조를 이해하면 메인 스레드가 왜 예민한지 바로 보입니다. 문제는 메인 스레드 위에 일이 올라온다는 사실이 아니라, 한 번 올라온 일이 길어지면 뒤에 줄 선 모든 일이 같이 늦어진다는 점입니다.
왜 blocking이 입력 지연이 될까
사용자가 화면을 터치하면 앱은 바로 반응하는 것처럼 보이지만, 실제로는 그 입력도 결국 메인 스레드가 처리해야 하는 일입니다. 그런데 메인 스레드가 이미 긴 파일 읽기나 무거운 계산을 하고 있다면, 새 입력은 끼어들 수 없습니다.
단일 차선 도로에서 앞차가 멈추면 뒤차가 아무리 급해도 기다려야 하는 것과 비슷합니다. 그래서 메인 스레드 문제는 CPU를 많이 썼느냐보다, 다음 중요한 일을 제때 시작할 수 있느냐로 보는 편이 더 정확합니다.
이 관점으로 보면 디스크 I/O, 락 대기, Binder 응답 대기, 긴 JSON 파싱은 전부 같은 그룹으로 묶입니다. 겉모습은 달라도 메인 스레드를 오래 붙잡는다는 점에서는 같은 병목이기 때문입니다.
frame drop과 ANR의 관계
메인 스레드가 조금만 늦어도 먼저 체감되는 것은 버벅임입니다. 화면은 일정한 리듬으로 다음 프레임을 준비해야 하는데, 그 사이에 메인 스레드가 다른 일을 오래 하고 있으면 제때 그리기 작업으로 넘어가지 못합니다.
이때 생기는 것이 jank나 frame drop입니다. 즉 화면이 끊겨 보인다는 것은 메인 스레드의 큐가 이미 건강하지 않다는 신호일 수 있습니다. 더 나아가 사용자 입력까지 오래 처리하지 못하면 ANR로 이어질 수 있습니다.
안드로이드 공식 문서도 UI thread가 너무 오래 blocked 되면 ANR이 발생한다고 설명하고, 입력 이벤트에 5초 안에 반응하지 못하면 input dispatch timeout 기반 ANR이 발생할 수 있다고 안내합니다.
frame drop과 ANR은 다른 장애처럼 보이지만, 둘 다 메인 스레드가 제때 비지 못한다는 같은 문제의 정도 차이로 이해하면 훨씬 쉽습니다.
왜 작은 코드도 위험할까
메인 스레드 문제는 꼭 거대한 함수에서만 생기지 않습니다. 짧아 보이는 코드라도 메인 스레드에서 기다림을 만들면 충분히 위험합니다.
lifecycleScope.launch {
val text = File(filesDir, "large.json").readText()
val model = parseHugeJson(text)
render(model)
}코루틴을 썼다고 해서 자동으로 안전해지는 것은 아닙니다. 중요한 질문은 코루틴을 썼는가가 아니라, 지금 이 일이 어느 스레드에서 돌고 있느냐입니다. 메인 스레드에서 시작한 긴 I/O와 파싱은 결국 Looper가 다음 메시지를 꺼내는 시간을 늦춥니다.
락 대기도 같은 병목이다
가끔은 메인 스레드 코드 자체가 길지 않은데도 앱이 멈춘 것처럼 보입니다. 이때 자주 놓치는 것이 lock contention입니다. 메인 스레드가 어떤 공유 자원에 접근하려는데, 다른 스레드가 그 락을 오래 잡고 있으면 메인 스레드는 자기 일도 못 하고 기다리게 됩니다.
겉으로는 버튼 클릭 콜백이 멈춘 것처럼 보이지만, 실제로는 다른 스레드가 길을 막은 셈입니다. Looper 관점에서 보면 이것도 본질은 같습니다. 메인 스레드가 다음 메시지로 넘어가지 못하고 현재 위치에서 멈춰 있다는 점에서는 긴 계산과 다를 것이 없습니다.
Binder 호출을 조심해야 하는 이유
안드로이드 공식 ANR 진단 문서는 slow binder call과 many consecutive synchronous binder calls도 대표 원인으로 듭니다. 이 말은 앱 코드 안에서 직접 무거운 계산을 하지 않아도, 메인 스레드가 다른 프로세스의 응답을 동기적으로 기다리느라 오래 묶일 수 있다는 뜻입니다.
특히 반복문 안에서 동기 Binder 호출을 여러 번 이어 붙이면 작은 지연이 한 번에 누적됩니다. 그러면 Looper 입장에서는 큐가 막혀 있는 시간이 길어지고, 사용자는 터치가 씹히거나 화면이 뚝뚝 끊기는 것으로 느끼게 됩니다.
비동기라고 끝이 아니다
실무에서는 비동기라는 단어가 너무 넓게 쓰여서 오해가 많습니다. 백그라운드에서 계산하다가 마지막 결과만 메인에 반영하면 대체로 안전한 방향이 맞습니다.
하지만 중간에 메인 스레드로 너무 자주 돌아오거나, 메인에서 큰 리스트 변환과 정렬을 수행하거나, 상태 반영 한 번에 UI 작업을 과하게 몰아 넣으면 여전히 문제가 생깁니다. Compose를 쓰든 View를 쓰든 이 원리는 같습니다.
실무 점검 포인트
- 메인 스레드가 지금 직접 실행 중인 일은 무엇인가
- 오래 계산 중인가, 아니면 락이나 Binder 응답을 기다리는 중인가
- 디스크 I/O나 네트워크, 데이터베이스, 큰 파싱이 메인에 올라와 있지는 않은가
- 한 프레임 안에 상태 반영과 UI 계산이 과하게 몰려 있지는 않은가
- 짧아 보이는 동기 호출이 반복문 안에서 누적되고 있지는 않은가
안드로이드 공식 문서도 blocking I/O, lock contention, slow binder call, many consecutive synchronous binder calls, expensive frame을 주요 점검 대상으로 안내합니다. 즉 문제를 API 이름으로 외우기보다, 메인 스레드를 오래 붙잡는 형태가 무엇인지로 분류하는 편이 실무에서 더 잘 먹힙니다.
Handler도 같은 흐름이다
예전 안드로이드 코드를 보면 Handler와 post가 많이 나옵니다. 이것도 결국 메인 스레드의 Looper에 연결된 큐로 일을 보내는 방식입니다. 그래서 Handler를 많이 쓰는 코드가 낯설어 보여도, 핵심은 복잡하지 않습니다.
메인 스레드가 나중에 처리해야 할 일을 큐에 올리고, Looper가 순서에 맞게 꺼내 실행한다는 것입니다. 이 이해가 있으면 코루틴의 메인 dispatcher도 덜 추상적으로 보입니다. 표면 API는 달라져도 마지막에는 메인 스레드에서 실행 가능한 시점과 시간을 두고 경쟁한다는 사실은 같습니다.
정리
안드로이드 메인 스레드는 왜 쉽게 막힐까라는 질문의 답은 생각보다 단순합니다. 메인 스레드는 Looper가 MessageQueue에서 다음 일을 하나씩 꺼내 처리하는 단일 차선이기 때문입니다.
그래서 긴 계산, blocking I/O, 락 대기, 느린 Binder 호출, 무거운 프레임은 모두 같은 문제로 만납니다. 지금 하고 있는 일이 끝나기 전에는 다음 입력과 다음 그리기 작업이 시작되지 못한다는 점입니다.
ANR 원인 자체를 더 넓게 정리한 글은 안드로이드 ANR은 왜 생길까: 메인 스레드를 막는 진짜 원인 정리에서 이어서 볼 수 있습니다. 공식 참고 자료로는 Better performance through threading, Looper, MessageQueue, ANRs, Diagnose and fix ANRs를 함께 읽어보면 연결이 더 잘 됩니다.