동기·비동기와 블로킹·논블로킹은 비슷해 보이지만 서로 다른 질문에 답한다. 동기(synchronous)·비동기(asynchronous)는 결과를 언제 어떻게 받느냐의 문제다. 호출한 쪽이 끝날 때까지 기다려 결과를 직접 받으면 동기, 일단 다음 일을 하다가 콜백·Promise·이벤트로 나중에 받으면 비동기다. 블로킹(blocking)·논블로킹(non-blocking)은 기다리는 동안 호출한 스레드가 멈추느냐의 문제다. 결과가 준비될 때까지 제어권을 돌려주지 않으면 블로킹, 준비가 안 됐어도 곧바로 돌려주면 논블로킹이다.
두 축 조합
| 블로킹 | 논블로킹 | |
|---|---|---|
| 동기 | 가장 흔한 형태. requests.get(), JDBC 쿼리, fs.readFileSync | 바로 돌아오는 호출을 되풀이하며 준비됐는지 확인(폴링) |
| 비동기 | 비동기 API를 부르고 곧바로 결과를 기다림. future.get(), WebClient의 .block(). 결국 동기 호출과 다를 바 없다 | 이벤트 루프 방식. fetch().then(), await, Netty·WebFlux |
이 용어들은 운영체제·언어·프레임워크마다 조금씩 다르게 쓰인다. 위 표는 엄밀한 정의라기보다 생각을 정리하고 대화를 맞추는 틀이다.
서버가 동시 요청을 다루는 세 방식
- 요청마다 스레드(thread-per-request): Spring MVC(Spring MVC 요청 흐름), 동기 Django처럼 요청 하나를 스레드 하나가 처음부터 끝까지 맡는다. 코드는 위에서 아래로 읽히지만, DB·외부 API를 기다리는 동안 스레드가 멈춰 있다. 스레드는 메모리를 차지하므로 풀 크기에 상한이 있다(Spring Boot 내장 Tomcat 기본 최대 200개). 느린 외부 호출 하나가 풀을 다 붙잡으면 다른 요청까지 멈춘다(스프링에서 외부 API 호출에서 타임아웃을 꼭 정해야 하는 이유)
- 이벤트 루프(event loop): Node.js, Python asyncio, Spring WebFlux(Netty)는 소수의 스레드가 논블로킹 I/O를 걸어 두고 완료 이벤트가 오면 이어서 처리한다(이벤트 루프). 기다리는 요청이 많아도 스레드가 늘지 않는다. 대신 코드 전체가 비동기여야 한다
- 가상 스레드(virtual thread): Java 21에 정식으로 들어왔다(JEP 444). 코드는 요청마다 스레드 방식처럼 블로킹으로 쓰고, 가상 스레드가 I/O를 기다리면 JVM이 그 가상 스레드를 실제 OS 스레드에서 떼어 내고, 비어 있는 OS 스레드에 다른 가상 스레드를 올린다. Spring Boot 3.2부터
spring.threads.virtual.enabled=true로 켠다
이벤트 루프를 막는 블로킹 호출
이벤트 루프 방식에서는 스레드 하나가 수많은 요청을 나눠 맡는다. 그래서 그 스레드를 블로킹 호출이나 무거운 계산으로 붙잡으면 모든 요청이 함께 멈춘다.
async def handler():
time.sleep(1) # 루프 전체가 1초 멈춤
requests.get(url) # 동기 HTTP 클라이언트도 마찬가지
async def handler_fixed():
await asyncio.sleep(1) # 비동기 대기
await asyncio.to_thread(requests.get, url) # 피할 수 없는 동기 호출은 스레드로 넘김 (3.9+)- JavaScript: Node.js 서버가 요청을 처리하다
fs.readFileSync를 부르거나 큰 JSON을 파싱하면 다른 요청이 모두 기다린다. 브라우저에서는 긴 동기 루프가 메인 스레드를 붙잡아 INP를 나쁘게 만든다 - WebFlux: 이벤트 루프 스레드에서
.block()을 부르면 Reactor가IllegalStateException을 던진다 async를 붙였다고 비동기가 되지 않는다. 안에서 부르는 라이브러리가 논블로킹이어야 한다
비동기는 병렬이 아니다
비동기는 기다리는 시간을 다른 일에 쓰게 해 줄 뿐, 계산을 빨라지게 하지는 않는다. 자바스크립트는 스레드 하나로 여러 일을 번갈아 진행하는 동시성(concurrency)이지, 여러 CPU(Central Processing Unit) 코어에서 동시에 계산하는 병렬성(parallelism)이 아니다. CPU를 오래 쓰는 작업은 Web Worker, Node.js worker_threads, 파이썬 ProcessPoolExecutor처럼 다른 스레드나 프로세스로 보낸다.
| 작업 성격 | 맞는 선택 |
|---|---|
| I/O 대기가 길고 동시 요청이 많음 | 이벤트 루프 또는 가상 스레드 |
| I/O가 있지만 동시 요청이 많지 않음 | 요청마다 스레드(가장 단순) |
| CPU 계산이 무거움 | 워커 스레드·프로세스로 병렬 처리 |
같은 두 축은 시스템 사이에서도 나타난다. DB의 비동기 복제(데이터베이스 다중화)나 메시지 큐(Kafka)는 "결과를 지금 받지 않는다"는 선택이고, 그 대가로 최종 일관성을 다뤄야 한다. 파이썬 비동기에서 예외가 사라지는 함정은 asyncio 태스크 예외가 조용히 사라지는 문제에 있다.
출처: MDN — Introducing asynchronous JavaScript · Node.js — Overview of Blocking vs Non-Blocking · Node.js — Don't Block the Event Loop · Python 문서: Developing with asyncio — Running Blocking Code · Spring Framework 문서: WebFlux Overview · JEP 444: Virtual Threads · Spring Boot 문서: Common Application Properties