블로킹
어떤 작업이 완료될 때까지 호출한 쪽의 실행 흐름이 멈추고 대기하는 상태를 뜻합니다.
비유
[ 블로킹 — 식당 대기 ]
주문 → 카운터 앞에서 음식 나올 때까지 서서 기다림
↑ 그동안 아무것도 못 함
[ 논블로킹 — 진동벨 ]
주문 → 진동벨 받음 → 자리에서 다른 일 → 벨 울리면 음식 수령
↑ 기다리는 동안 다른 일 가능
코드로 보는 차이
블로킹
// 블로킹 — readFileSync는 파일을 다 읽을 때까지 멈춤
const data = fs.readFileSync("big-file.txt"); // 3초간 멈춤
console.log("파일 읽기 완료"); // 3초 후 실행
console.log("다음 작업"); // 그 다음 실행
시간 ──────────────────────────────────────→
readFileSync ████████████████████ (3초 블로킹)
"파일 읽기 완료" ██
"다음 작업" ██
논블로킹
// 논블로킹 — readFile은 즉시 반환, 완료 시 콜백 호출
fs.readFile("big-file.txt", (err, data) => {
console.log("파일 읽기 완료"); // 3초 후 실행
});
console.log("다음 작업"); // 즉시 실행
시간 ──────────────────────────────────────→
readFile 호출 █ (즉시 반환)
"다음 작업" ██ (바로 실행)
파일 I/O ████████████████████ (백그라운드)
"파일 읽기 완료" ██ (콜백 실행)
블로킹이 발생하는 대표적인 상황
1. I/O 작업
파일 읽기/쓰기 디스크 접근 대기
네트워크 요청 서버 응답 대기
DB 쿼리 쿼리 결과 대기
사용자 입력 키보드·마우스 입력 대기
2. 동기 함수 호출
// Java — 블로킹 I/O
InputStream in = socket.getInputStream();
int data = in.read(); // 데이터가 올 때까지 스레드 멈춤
3. Lock 대기
synchronized (lock) {
// 다른 스레드가 lock을 쥐고 있으면
// 이 스레드는 여기서 블로킹
}
4. sleep
Thread.sleep(3000); // 3초간 스레드 블로킹
블로킹이 문제가 되는 이유
단일 스레드에서 블로킹
[ 브라우저 메인 스레드 ]
사용자 클릭 → 서버 요청 (블로킹 3초) → 응답 처리
↑
이 동안 화면 멈춤
버튼 클릭 안 됨
스크롤 안 됨
애니메이션 정지
→ "페이지가 응답하지 않습니다"
서버에서 블로킹
[ 스레드 1개당 요청 1개 처리 — 전통적 블로킹 서버 ]
스레드 1: 요청 A 처리 중 ── DB 쿼리 (블로킹 500ms) ── 응답
스레드 2: 요청 B 처리 중 ── 파일 읽기 (블로킹 200ms) ── 응답
스레드 3: 요청 C 처리 중 ── API 호출 (블로킹 1초) ── 응답
...
스레드 500: 요청 처리 중
501번째 요청 → 스레드 없음 → 대기 또는 거부
↑ 스레드가 블로킹 상태로 놀고 있는데 새 요청 못 받음
CPU 사용률:
스레드 1: 작업 ██──── 블로킹(대기) ─────██ 작업
스레드 2: 작업 ██── 블로킹(대기) ──██ 작업
스레드 3: 작업 ██─────── 블로킹(대기) ────────██ 작업
→ 대부분의 시간을 대기에 소비
→ CPU는 놀고 있는데 스레드는 점유 중
→ 자원 낭비
블로킹 vs 논블로킹 비교
| 항목 | 블로킹 | 논블로킹 |
|---|---|---|
| 호출 후 | 완료까지 대기 | 즉시 반환 |
| 스레드 상태 | 멈춤 (Waiting) | 계속 실행 |
| 코드 흐름 | 순차적, 직관적 | 콜백/이벤트 기반 |
| 자원 효율 | 낮음 (대기 중 점유) | 높음 |
| 코드 복잡도 | 단순 | 상대적으로 복잡 |
I/O 모델 4가지 분류
블로킹/논블로킹과 동기/비동기는 다른 개념이며, 조합됩니다.
동기 비동기
(결과를 직접 확인) (결과를 통보받음)
블로킹 Sync + Blocking 거의 사용 안 함
(기다림) ───────────── ──────────────
read()
→ 데이터 올 때까지 멈춤
논블로킹 Sync + Non-Blocking Async + Non-Blocking
(안 기다림) ───────────── ──────────────
read() → 없으면 즉시 aio_read()
반환 → 반복 확인 → 완료되면 콜백 호출
(폴링)
| 모델 | 설명 | 예시 |
|---|---|---|
| Sync Blocking | 요청 후 결과 나올 때까지 대기 | Java InputStream.read() |
| Sync Non-Blocking | 요청 후 즉시 반환, 반복 확인 (폴링) | Java NIO channel.read() |
| Async Non-Blocking | 요청 후 즉시 반환, 완료 시 통보 | Node.js fs.readFile() |
[ Sync Blocking ]
스레드: 요청 ─── 대기 ─── 대기 ─── 결과 수신 ─── 처리
[ Sync Non-Blocking (폴링) ]
스레드: 요청 → "됐어?" → 아니 → "됐어?" → 아니 → "됐어?" → 응 → 처리
↑ 반복 확인하느라 CPU 낭비 가능
[ Async Non-Blocking ]
스레드: 요청 → 다른 작업 → 다른 작업 → 콜백 호출 → 처리
↑ 기다리지도, 확인하지도 않음
OS가 완료되면 알려줌
언어·프레임워크별 블로킹 처리 방식
| 환경 | 블로킹 처리 전략 |
|---|---|
| Java (전통) | 스레드 풀로 블로킹 I/O 처리, 요청당 스레드 |
| Java NIO | Selector + Channel로 논블로킹 I/O |
| Java Virtual Thread | 블로킹 코드를 작성하되 경량 스레드가 처리 |
| Node.js | 이벤트 루프 + 논블로킹 I/O, 단일 스레드 |
| Spring WebFlux | Reactor 기반 논블로킹 리액티브 |
| Go | 고루틴 + 내부적으로 논블로킹 I/O |
| Nginx | 이벤트 기반 논블로킹 |
Java 전통 vs Virtual Thread
// 전통 — 블로킹이 스레드를 점유
Thread thread = new Thread(() -> {
var data = db.query("SELECT ..."); // 스레드 블로킹
});
// 스레드 1개 = 수 MB 메모리, 수천 개가 한계
// Virtual Thread (Java 21) — 블로킹 코드 그대로, 경량 처리
Thread.startVirtualThread(() -> {
var data = db.query("SELECT ..."); // 코드는 동일
// 내부적으로 블로킹 시 OS 스레드 반납
// → 수백만 개 동시 실행 가능
});
Node.js — 블로킹을 원천 차단
// Node.js는 메인 스레드가 하나이므로 블로킹하면 전체가 멈춤
// X 절대 하면 안 됨
const data = fs.readFileSync("file.txt"); // 메인 스레드 블로킹
// → 이 동안 모든 요청 처리 중단
// O 반드시 논블로킹 사용
const data = await fs.promises.readFile("file.txt");
// → 이벤트 루프가 다른 요청 처리 가능
핵심 정리
블로킹은 작업 완료까지 호출자의 실행이 멈추는 상태입니다. 코드가 직관적이라는 장점이 있지만, 대기 시간 동안 스레드를 점유하면서 아무 일도 못 하는 자원 낭비가 핵심 문제입니다. 이를 해결하기 위해 논블로킹 I/O, 이벤트 루프, 리액티브 프로그래밍, Virtual Thread 등 다양한 기술이 발전해왔으며, 현대 서버 아키텍처는 블로킹을 최소화하여 적은 스레드로 많은 요청을 처리하는 방향으로 진화하고 있습니다.