이 글에서 얻는 것
- 동기/비동기와 블로킹/논블로킹을 서로 다른 축으로 구분해서 설명할 수 있습니다.
- “동시 요청이 많을수록 왜 스레드가 문제”가 되는지(대기·컨텍스트 스위칭·큐 포화)를 실행 모델로 이해합니다.
- 스레드풀 기반(MVC)과 이벤트 루프 기반(WebFlux/Netty)의 trade-off를 근거 있게 비교할 수 있습니다.
- 실무에서 흔한 설계 실수(블로킹 작업 섞기, 무제한 큐, 타임아웃/취소 부재)를 피할 기준이 생깁니다.
0) 먼저 정리: 사람들이 가장 많이 섞어 쓰는 단어
아래 두 개는 비슷해 보이지만 전혀 다른 질문입니다.
- 동기/비동기: “요청을 보낸 쪽이, 결과를 ‘어떻게’ 받는가?”(제어 흐름/콜백/완료 통지)
- 블로킹/논블로킹: “작업을 수행하는 ‘스레드’가 기다리며 멈추는가?”(자원 대기 방식)
이 둘을 분리해서 생각하면, 실행 모델이 선명해집니다.
1) 동기 vs 비동기: 결과를 받는 방식(제어 흐름)
동기(Synchronous)
호출자가 결과를 얻기 전까지 다음 단계로 진행하지 않는 형태입니다.
String body = httpClient.get(url); // 여기서 결과가 올 때까지 흐름이 멈춤(동기)
process(body);
비동기(Asynchronous)
호출자는 “작업을 등록”하고, 완료 통지(콜백/퓨처/이벤트)로 결과를 처리합니다.
CompletableFuture<String> future = httpClient.getAsync(url);
future.thenAccept(this::process);
핵심은 “스레드를 쓰냐 안 쓰냐”가 아니라, 제어 흐름이 누구 손에 있느냐입니다.
2) 블로킹 vs 논블로킹: 스레드가 멈추는가(대기 방식)
블로킹(Blocking)
IO/락/큐 대기처럼 “조건이 만족될 때까지” 현재 스레드가 멈춥니다.
- 장점: 코드가 단순합니다(직선적인 흐름).
- 단점: 동시 요청이 늘면 “대기 중인 스레드”가 늘고, 컨텍스트 스위칭/메모리 사용량/큐 포화가 생깁니다.
논블로킹(Non-blocking)
“지금 당장 못 하면” 바로 제어를 돌려주고, 나중에 준비되면 다시 처리합니다(이벤트/폴링/콜백 등).
- 장점: 대기 때문에 스레드가 묶이는 것을 줄일 수 있습니다.
- 단점: 코드 흐름이 복잡해지기 쉽고(콜백/퓨처), 끝까지 논블로킹으로 구성하지 않으면 이득이 깨집니다.
3) 2x2로 정리하면 헷갈림이 사라진다
동기/비동기와 블로킹/논블로킹은 조합이 가능합니다.
- 동기 + 블로킹: 가장 흔한 형태(전통적인 JDBC/동기 HTTP 클라이언트)
- 비동기 + 논블로킹: 이벤트 루프 기반(예: Netty, WebFlux의 핵심 흐름)
- 비동기 + 블로킹: “비동기 API처럼 보이지만 내부에서 스레드를 막는” 형태(전용 풀로 감싸는 경우)
- 동기 + 논블로킹: 매우 드뭅니다(호출자가 즉시 결과를 가져와야 하는데, 논블로킹은 보통 ‘나중에 준비됨’이 핵심)
실무에서 문제는 주로 3번입니다.
겉은 비동기인데 내부가 블로킹이면, 결국 스레드풀/큐가 병목이 됩니다.
4) 서버 실행 모델로 연결하기: MVC(스레드풀) vs WebFlux(이벤트 루프)
4-1) 스레드풀 기반(대부분의 Spring MVC)
- 요청 하나를 처리하는 동안, 보통 “요청 스레드”가 작업을 수행합니다.
- DB/외부 API 호출이 블로킹이면, 그 시간 동안 스레드는 대기합니다.
이 모델의 장점은 단순함입니다. 다만 동시 요청이 늘고, IO 대기가 길어질수록 스레드가 빠르게 소진됩니다.
4-2) 이벤트 루프 기반(대부분의 WebFlux/Netty)
- 이벤트 루프는 “많은 연결”을 적은 스레드로 관리하면서, 준비된 작업만 처리합니다.
- IO 대기 때문에 스레드가 묶이지 않도록 설계됩니다.
중요한 전제:
- DB/외부 호출/파일 IO까지 끝까지 논블로킹(예: WebClient, R2DBC 등)이어야 장점이 커집니다.
- 논블로킹 모델에서도 CPU가 무거운 작업은 별도 풀로 넘겨야 합니다(이벤트 루프를 막지 않기).
5) 선택 기준(실무 감각)
MVC(동기/블로킹)가 충분히 좋은 경우
- 요청당 작업이 짧고, IO 대기가 짧거나 제한적일 때
- 팀의 디버깅/운영 경험이 동기 모델에 최적화되어 있을 때
- 의존 라이브러리(드라이버)가 블로킹 중심이고, 전체를 논블로킹으로 바꾸기 어려울 때
WebFlux(비동기/논블로킹)가 이득을 주기 쉬운 경우
- 한 요청에서 외부 호출을 여러 개 fan-out 하고, 대기 시간이 지배적일 때
- 동시 연결 수가 매우 크고(예: SSE/스트리밍), 스레드가 병목이 될 때
- 백프레셔/타임아웃/취소 같은 “흐름 제어”가 중요한 시스템일 때
6) 공통 실수 4가지(모델과 무관하게 터진다)
- 무제한 큐: 큐가 안전장치가 아니라 “지연을 숨기는 쓰레기통”이 됩니다(OOM/지연 폭발).
- 타임아웃/취소 부재: 느린 의존성 하나가 전체를 묶어 장애가 전파됩니다.
- 블로킹 작업을 섞기: 이벤트 루프/요청 스레드를 블로킹 IO로 붙잡아 둡니다(전용 풀로 분리).
- 관측 불가: 큐 길이, active 스레드, reject, 타임아웃 비율 같은 포화 신호가 없으면 원인을 추측하게 됩니다.
연습(추천)
- “외부 API 10개 fan-out” 예제를 만들고, 동기(MVC) vs 비동기(CompletableFuture/WebFlux)로 지연/처리량을 비교해보기
- 전용 풀을 두지 않고 블로킹 작업을 섞었을 때(또는 무제한 큐일 때) 지연이 어떻게 폭발하는지 재현해보기
연결해서 읽기
- 동시성 기본기(락/스레드풀/JMM):
/learning/deep-dive/deep-dive-java-concurrency-basics/ - OS 관점(스케줄링/컨텍스트 스위칭):
/learning/deep-dive/deep-dive-os-concurrency-basics/ - Spring MVC vs WebFlux 선택 기준:
/learning/deep-dive/deep-dive-spring-webflux-vs-mvc/
💬 댓글