이 글에서 얻는 것

  • 동기/비동기와 블로킹/논블로킹을 서로 다른 축으로 구분해서 설명할 수 있습니다.
  • “동시 요청이 많을수록 왜 스레드가 문제”가 되는지(대기·컨텍스트 스위칭·큐 포화)를 실행 모델로 이해합니다.
  • 스레드풀 기반(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로 정리하면 헷갈림이 사라진다

동기/비동기와 블로킹/논블로킹은 조합이 가능합니다.

  1. 동기 + 블로킹: 가장 흔한 형태(전통적인 JDBC/동기 HTTP 클라이언트)
  2. 비동기 + 논블로킹: 이벤트 루프 기반(예: Netty, WebFlux의 핵심 흐름)
  3. 비동기 + 블로킹: “비동기 API처럼 보이지만 내부에서 스레드를 막는” 형태(전용 풀로 감싸는 경우)
  4. 동기 + 논블로킹: 매우 드뭅니다(호출자가 즉시 결과를 가져와야 하는데, 논블로킹은 보통 ‘나중에 준비됨’이 핵심)

실무에서 문제는 주로 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가지(모델과 무관하게 터진다)

  1. 무제한 큐: 큐가 안전장치가 아니라 “지연을 숨기는 쓰레기통”이 됩니다(OOM/지연 폭발).
  2. 타임아웃/취소 부재: 느린 의존성 하나가 전체를 묶어 장애가 전파됩니다.
  3. 블로킹 작업을 섞기: 이벤트 루프/요청 스레드를 블로킹 IO로 붙잡아 둡니다(전용 풀로 분리).
  4. 관측 불가: 큐 길이, 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/