사전 메인 스레드 차단
문제

메인 스레드 차단

gabury1고친 사람 github-actions[bot]

메인 스레드 차단은 화면을 맡은 스레드가 오래 걸리는 일 하나에 묶여 다른 일을 못 하게 되는 고장입니다. 그동안 화면은 다시 그려지지 않습니다. 버튼을 눌러도 아무 반응이 없습니다. 사용자 눈에는 앱이 얼어붙은 것으로 보입니다.

쉽고 빠른 이해

메인 스레드 차단은 화면을 맡은 스레드가 딴 일에 붙들려 화면이 굳는 고장입니다. 버튼을 누르면 서버에 묻는 코드가 응답을 기다리는 동안 화면이 멈추는 것이 대표적입니다.

이 이름이 따로 있는 까닭은 화면 일을 스레드 하나가 도맡기 때문입니다. 그 스레드가 막히면 그리기와 입력이 함께 멈춥니다. 다른 스레드가 놀고 있어도 대신 그려 주지 못합니다.

어떻게 터지나:

  1. 화면을 맡은 스레드는 할 일을 줄 세워 하나씩 처리합니다
  2. 그중 하나가 네트워크 응답을 기다리거나 큰 계산을 돌립니다
  3. 그 일이 끝날 때까지 뒤에 선 그리기와 입력이 모두 기다립니다

화면이 없는 배치 스크립트에서는 문제가 안 됩니다. 기다리는 동안 대신 해야 할 일이 따로 없습니다.

피하는 데도 대가가 있습니다. 오래 걸리는 일은 다른 스레드에 넘겨야 합니다. 결과는 나중에 돌려받습니다. 그래서 코드가 일을 맡기는 쪽과 결과를 받아 화면에 적는 쪽으로 갈라집니다. 결과가 언제 올지 모르는 경우도 다뤄야 합니다.

상세

메인 스레드 차단은 메인 스레드가 한 가지 일을 오래 붙들어서 화면 그리기와 입력 처리가 밀리는 고장입니다. 이 절은 먼저 메인 스레드가 무엇이고 왜 하나뿐인지 봅니다. 이어서 무엇이 그 스레드를 막고, 막히면 어떻게 보이고, 언제 재현되는지를 짧은 코드 한 토막까지 곁들여 봅니다. 끝으로 막힘을 덜어 내고 알아채는 법, 서버에서 생기는 같은 고장, ANR·데드락과의 차이를 봅니다.

화면을 맡은 스레드 하나

스레드는 프로그램 안에서 따로 나아가는 실행 흐름입니다. 프로그램이 시작할 때 처음 생기는 스레드를 메인 스레드라고 부릅니다. 화면이 있는 앱에서는 이 스레드가 화면을 맡습니다. 그래서 UI(User Interface, 사용자 인터페이스) 스레드라고도 부릅니다.

대부분의 화면 프레임워크는 화면 요소를 메인 스레드에서만 고치게 합니다. 여러 스레드가 같은 버튼을 동시에 고치면 화면 상태가 꼬이기 때문입니다. 모든 화면 요소에 잠금(락, 한 번에 한 스레드만 들어가게 막는 장치)을 거는 대신 고치는 일을 스레드 하나에 몰아준 설계입니다.

이 설계 덕분에 화면 코드는 잠금 없이 짤 수 있습니다. 대신 그 한 스레드가 막히면 화면 전체가 멈춥니다. 이 고장이 따로 이름을 얻은 까닭이 이 맞바꿈에 있습니다.

할 일을 하나씩 꺼내는 반복

메인 스레드는 이벤트 루프를 돕니다. 이벤트 루프는 할 일 줄에서 맨 앞의 일을 꺼내 처리하는 반복입니다. 하나를 끝내면 줄로 돌아와 다음 일을 꺼냅니다.

개발자가 쓴 화면 코드는 대개 콜백으로 이 줄에 들어옵니다. 콜백은 버튼이 눌렸을 때처럼 어떤 일이 생기면 불러 달라고 미리 등록해 둔 함수입니다. 이 줄에는 콜백 말고도 터치 입력과 화면 다시 그리기가 함께 섭니다.

루프는 지금 처리 중인 일이 끝나야 다음 일을 꺼냅니다. 처리 도중에 끼어들 방법은 없습니다. 아래 그림은 버튼 콜백 하나가 서버 응답을 기다리며 끝나지 않을 때의 줄입니다.

flowchart TD
    L["메인 스레드"] --> Q1
    subgraph 줄["할 일 줄"]
        Q1["버튼 콜백 · 서버 응답을 기다리는 중"]
        Q2["화면 다시 그리기 · 대기"]
        Q3["터치 입력 · 대기"]
        Q4["다음 터치 입력 · 대기"]
        Q1 --> Q2 --> Q3 --> Q4
    end

맨 앞의 콜백이 끝나야 화면이 다시 그려집니다. 그 사이 사용자가 화면을 몇 번 눌러도 입력은 줄 뒤에 쌓이기만 합니다.

무엇이 메인 스레드를 막나

막는 일은 두 갈래입니다. 하나는 무언가를 기다리는 일입니다. 다른 하나는 계산하느라 쉴 틈이 없는 일입니다. 어느 쪽이든 그동안 루프는 다음 일을 꺼내지 못합니다.

블로킹은 결과가 올 때까지 부른 스레드가 다른 일을 못 하고 서 있는 방식입니다. 기다리는 갈래는 이런 블로킹 호출을 메인 스레드에서 부를 때 생깁니다.

갈래 메인 스레드에서 한 일 그동안 스레드는
기다림 네트워크 호출 · 파일이나 데이터베이스 읽기 응답이 올 때까지 서 있다
기다림 다른 스레드가 쥔 잠금 잡기 그 스레드가 놓을 때까지 서 있다
계산 큰 목록 정렬 · 이미지 변환 · 큰 응답 문자열 해석 쉬지 않고 계산한다

계산 갈래는 코드만 봐서는 잘 안 드러납니다. 데이터가 작을 때는 금방 끝납니다. 데이터가 커진 뒤에야 화면이 굳기 시작합니다.

막힌 시간에 따라 보이는 모습

화면은 1초에 정해진 횟수만큼 다시 그려집니다. 이 한 장 한 장을 프레임이라고 합니다. 1초에 60장을 그리는 화면이라면 한 장에 쓸 수 있는 시간은 60분의 1초, 밀리초로 따지면 스무 밀리초가 채 안 됩니다. 이 시간을 프레임 예산이라고 부릅니다.

메인 스레드가 한 일에 이 예산보다 오래 머물면 그 장은 제때 못 그려집니다. 막힌 시간이 길어질수록 사용자가 겪는 일도 달라집니다.

막힌 시간 사용자가 보는 것
프레임 예산을 조금 넘김 스크롤이나 애니메이션이 툭툭 끊긴다(프레임 드롭)
수백 밀리초 버튼을 눌러도 반응이 늦게 온다(입력 지연)
수 초 화면이 얼어붙는다. 운영체제나 브라우저가 응답 없음을 알린다

마지막 줄은 운영체제가 앱을 멈춘 것으로 판정하는 단계입니다. 안드로이드는 이 판정을 ANR(Application Not Responding, 앱 응답 없음)이라고 부릅니다. 브라우저도 오래 굳은 페이지에는 기다릴지 닫을지 묻는 창을 띄웁니다.

재현되는 조건

셋이 다 서면 메인 스레드 차단이 됩니다.

  1. 어떤 일이 메인 스레드 위에서 돕니다
  2. 그 일이 끝나기까지 프레임 예산보다 오래 걸립니다
  3. 그 일이 도는 동안 하던 일을 끊고 남은 몫을 줄 뒤로 미루는 지점이 없습니다

하나만 빠져도 화면은 안 굳습니다. 첫째가 빠지면 일은 다른 스레드에서 돕니다. 그동안 메인 스레드는 계속 그립니다. 둘째가 빠지면 일이 한 장 안에 끝납니다. 사용자는 알아채지 못합니다.

셋째가 놓치기 쉬운 조건입니다. 오래 걸리는 일도 잘게 나누면 됩니다. 한 조각을 마치면 남은 조각을 할 일 줄 맨 뒤에 다시 넣습니다. 그러면 줄에서 기다리던 그리기와 입력이 그 사이에 먼저 처리됩니다.

이렇게 하던 일을 잠시 끊고 줄로 돌아가는 것을 루프에 차례를 돌려준다고 합니다. 흔히 쓰는 말로는 제어를 양보한다(yield)고 합니다.

화면도 이벤트 루프도 없는 프로그램에는 이 고장이 없습니다. 밤에 도는 배치 스크립트의 메인 스레드는 파일을 기다리며 서 있어도 됩니다. 그동안 대신 해야 할 다른 일이 없기 때문입니다.

코드에서 나타나는 꼴

브라우저의 JavaScript 로 버튼 하나를 만들면 이 고장을 바로 볼 수 있습니다. 브라우저에서는 JavaScript 코드와 화면 그리기가 같은 메인 스레드에서 돕니다. 아래 코드는 버튼을 누르면 안내 문구를 바꾼 뒤 긴 반복문을 돌립니다. 1e9 는 10억이라는 뜻입니다.

JavaScript
button.onclick = () => {
  msg.textContent = "계산 중"; // 안 뜬다
  let sum = 0;
  for (let i = 0; i < 1e9; i++) sum += i;
  msg.textContent = "끝"; // 이것만 뜬다
};

첫 줄에서 문구를 「계산 중」으로 바꿨습니다. 그래도 화면에는 나타나지 않습니다. 브라우저는 콜백 하나가 끝난 뒤에야 화면을 다시 그리기 때문입니다. 반복문이 도는 동안 버튼을 다시 눌러도 반응이 없습니다. 반복문이 끝나면 화면은 곧바로 「끝」으로 넘어갑니다.

메인 스레드에서 일을 덜어 내는 길

고치는 길은 오래 걸리는 일을 메인 스레드 밖으로 보내는 것입니다. 메인 스레드가 아닌 스레드를 백그라운드 스레드라고 부릅니다.

메인 스레드는 일을 맡기자마자 루프로 돌아갑니다. 결과는 나중에 할 일 줄에 새 일로 들어옵니다. 메인 스레드는 그 일을 꺼낼 때 결과를 화면에 적습니다.

sequenceDiagram
    participant 사용자
    participant 메인 as 메인 스레드
    participant 뒤 as 백그라운드 스레드
    사용자->>메인: 버튼을 누른다
    메인->>뒤: 오래 걸리는 일을 맡긴다
    Note over 메인: 루프로 돌아가 입력과 그리기를 계속한다
    사용자->>메인: 스크롤한다
    메인-->>사용자: 바로 반응한다
    뒤-->>메인: 결과를 할 일 줄에 넣는다
    메인-->>사용자: 결과를 화면에 그린다

일을 맡긴 뒤에도 스크롤이 바로 반응하는 대목이 핵심입니다. 같은 생각을 구현하는 수단은 여럿입니다.

수단 하는 일
백그라운드 스레드 · 스레드 풀 일을 다른 스레드에서 돌린다. 결과는 메인 스레드의 줄에 넣는다
논블로킹 입출력 요청만 보내고 곧바로 돌아온다. 응답은 콜백으로 줄에 들어온다
코루틴 기다리는 동안 스레드를 내준다. 결과가 오면 멈춘 곳부터 이어서 돈다
일 쪼개기 큰 계산을 조각으로 나눈다. 조각 사이마다 루프에 차례를 돌려준다
웹 워커 브라우저에서 JavaScript 를 메인 스레드 밖에서 돌린다

어느 수단을 쓰든 화면을 고치는 마지막 한 줄은 메인 스레드로 돌아와서 돕니다. 화면 요소는 메인 스레드에서만 고칠 수 있기 때문입니다.

알아채는 방법

화면이 굳는 순간은 짧아서 눈으로 원인을 찾기 어렵습니다. 그래서 메인 스레드가 시간을 어디에 썼는지 기록으로 봅니다.

프로파일러는 스레드마다 어느 함수가 얼마 동안 돌았는지 시간축 위에 그려 주는 도구입니다. 메인 스레드 줄에서 유난히 긴 막대를 찾으면 화면을 붙든 일이 무엇인지 나옵니다.

스택 트레이스는 스레드가 그 순간 어느 함수의 어느 줄을 돌고 있었는지 적은 목록입니다. 화면이 굳은 순간에 메인 스레드의 목록을 뜨면 무엇을 기다리며 서 있었는지 보입니다.

보는 것 막혔을 때 보이는 모습
프로파일러의 메인 스레드 시간축 한 일이 프레임 예산보다 긴 막대로 누워 있다
화면이 굳었을 때 뜬 메인 스레드 스택 트레이스 네트워크 · 파일 · 잠금을 기다리는 함수에서 멈춰 있다
프레임 드롭 수 특정 화면이나 동작에서만 몰려서 늘어난다

개발 중에 미리 잡아 주는 장치도 있습니다. 안드로이드의 StrictMode 는 메인 스레드에서 디스크나 네트워크를 건드리는 순간을 알려 줍니다.

서버에서 같은 모양으로 터지는 장면

백엔드에도 스레드 하나가 이벤트 루프를 도는 서버가 있습니다. Node.js 가 대표적입니다. 자바의 Netty 도 연결들을 몇 개의 이벤트 루프 스레드에 나눠 맡깁니다.

이런 서버에서 요청 하나가 긴 계산이나 블로킹 호출을 하면 같은 루프에 붙은 요청이 모두 기다립니다. Node.js 에서 요청을 처리하다 파일을 동기 방식으로 읽는 함수를 부르는 경우가 그렇습니다. 화면은 없지만 증상은 같습니다. 그 요청과 상관없는 요청까지 응답이 늦어집니다.

서버 쪽에서는 이 고장을 이벤트 루프 차단이라고 부릅니다. 막히는 스레드가 화면을 맡았든 요청을 맡았든 원인과 고치는 길은 같습니다.

ANR·데드락과 어떻게 갈리나

이름이 나란히 불리는 셋은 가리키는 것이 다릅니다. 차단은 메인 스레드가 한 일에 묶인 상태 자체입니다. 데드락은 그 상태를 끝나지 않게 만드는 원인 하나입니다. ANR 은 그 상태가 오래가면 운영체제가 붙이는 판정입니다.

이름 무엇인가 풀리나
메인 스레드 차단 메인 스레드가 한 일에 오래 묶이는 고장 그 일이 끝나면 풀린다
ANR 차단이 정해진 시간을 넘겼을 때 운영체제가 내리는 판정 차단이 풀리면 판정도 사라진다. 그 전에 앱이 닫힐 수 있다
데드락 두 스레드가 서로 상대가 쥔 것을 기다리며 멈춘 고장. 메인 스레드가 그 한쪽이면 차단이 된다 밖에서 끊기 전엔 안 풀린다

메인 스레드 차단은 대개 붙든 일이 끝나면 저절로 풀립니다. 메인 스레드가 데드락에 걸린 경우는 다릅니다. 그것도 차단이지만 끝나는 때가 없는 차단입니다. 화면이 영영 돌아오지 않습니다.

관련 항목

메인 스레드 차단이 일어나는 실행 구조

메인 스레드 · 스레드 · 이벤트 루프 · 콜백 · 태스크 큐 · Looper · MessageQueue

메인 스레드를 막는 호출과 대기

블로킹 · 블로킹 호출 · 블로킹 입출력 · 입출력 대기 · 락 경합 · 동기 호출 · CPU 바운드

메인 스레드 차단이 화면에 남기는 증상

프레임 드롭 · jank · 입력 지연 · ANR · 콜드 스타트

메인 스레드 차단을 재는 지표

프레임 예산 · 프레임 시간 · 프레임률 · 응답 시간 · 꼬리 지연

메인 스레드에서 일을 덜어 내는 수단

백그라운드 스레드 · 스레드 풀 · 워커 · 웹 워커 · 논블로킹 입출력 · 비동기 · 코루틴 · 타임 슬라이싱 · WorkManager

메인 스레드 차단을 찾아내는 도구

프로파일러 · 프로파일링 · 스택 트레이스 · StrictMode · 롱 태스크

같은 구조로 서버에서 터지는 장애

이벤트 루프 차단 · Node.js · Netty · 헤드 오브 라인 블로킹 · 데드락 · 스레드 기아

메인 스레드 차단이 문제가 되는 화면 개발 분야

모바일 개발 · Android · iOS · 프론트엔드 · 브라우저 · 렌더링 · DOM

다른 이름: main thread blocking · 메인 스레드 블로킹 · UI 스레드 차단 · UI 스레드 블로킹