동기와 비동기는 정말 익숙해서 둘이 뭔 차이고 어떻게 동작하느냐고 물어본다면 바로바로 대답할 수 있지만, 블로킹은 부끄럽게도 아직 잘 알지 못한다. 이번 포스팅에서는 동기/비동기와 함께 블로킹에 대해 알아보고자 한다.
동기/비동기
동기/비동기는 '결과를 언제 처리하는가(제어권의 흐름)'에 초점을 둔다.
동기란?
작업의 흐름(제어권)이 호출자 기준으로 순차적으로 진행되는 방식이다.
요청한 작업의 결과가 나올 때까지 기다리거나, 결과가 나오면 즉시 처리하는 방식
결과가 와야 다음 작업이 가능하다. 작업의 순서가 보장된다. (요청 -> 응답 -> 다음 작업)
단점: 결과 받을 때까지 대기 시간동안 아무것도 못함
예시: 마트에서 대기줄 기다리는 상황
// 예시
String result = service.call(); // 여기서 결과 나올 때까지 흐름 유지
System.out.println(result);
비동기란?
작업을 요청한뒤, 결과를 나중에 받는 방식이다. 호출하는 함수는 호출되는 함수에게 콜백(Callback) 함수를 건네주고, 자신의 할 일을 계속한다.
요청과 응답의 흐름이 분리됨 (요청한 후, 응답을 받을 때까지 기다리는 게 아니라 보내놓고 계속 흐름을 이어간다.)
호출된 함수가 작업을 마치면 넘겨받은 콜백을 실행하여 결과를 처리한다.
호출하는 함수는 작업 완료 시점에 콜백 / 이벤트 등으로 결과를 받는다.
단점: 흐름 추적과 디버깅이 어렵다. 완료 순서가 보장되지 않는다.
예시: 카페에서 주문 후 진동벨 받고, 벨 울리면 가지러 가는 상황
// 예시
service.callAsync(result -> {
System.out.println(result);
});
정리
| 구분 | 동기 | 비동기 |
| 결과 처리 | 직접 받음 | 나중에 받음 |
| 흐름 | 순차적 | 분기됨 |
| 복잡도 | 낮음 | 높음 |
블로킹/논블로킹
블로킹과 논블로킹은 스레드 관점에서 '제어권을 누가 가지고 있는가?'에 집중한다.
블로킹
호출된 함수가 작업을 완료할 때까지 제어권을 계속 가지고 있는 상태이다.
즉, 요청한 작업이 끝날 때까지 현재 스레드는 멈추게 된다.
특징: 호출한 함수는 제어권이 없기 때문에 아무것도 못하고 멈춰(Block) 있게 된다. (이 때, CPU 자원이 놀게되어 효율이 떨어질 수 있음)
많은 요청을 처리하려면 스레드를 많이 만들어야 함 -> 스레드 수만큼 메모리 소비
예를 들어, 함수 A와 B가 있다고 하자.
- 함수 A가 함수 B를 호출
- 함수 B가 작업을 수행하는 동안 운영체제가 함수 A가 실행 중인 스레드를 일시 중지
- 함수 B에 대한 호출 방식은 블로킹

논블로킹
호출된 함수가 작업을 마치지 않았더라도 제어권을 즉히 호출한 함수에게 돌려주는 상태이다.
작업이 즉시 끝나지 않더라도, 스레드를 멈추지 않는다.
특징: 호출한 함수는 제어권을 바로 받았기 때문에, 다른 작업을 계속 수행할 수 있다.
스레드가 I/O를 기다리며 낭비되지 않고, 하나의 스레드로 여러 I/O를 처리 가능
- 함수 A가 함수 B를 호출
- 함수 B는 바로 리턴
- 작업 완료 여부는 나중에 다시 확인 (완료 감지되면 콜백 핸들러 등 실행)
- 함수 B에 대한 호출 방식은 논블로킹
- 스레드가 멈추지 않아 다른 작업들이 가능하나 반복 확인(Polling) 또는 이벤트 필요

블로킹 / 논블로킹 비교
| 구분 | 블로킹 | 논블로킹 |
| 제어권 | 호출받은 쪽 (B) | 호출한 쪽 (A) |
| 스레드 상태 | 대기(멈춤) | 실행 가능 |
| 자원 효율 | 낮을 수 있음 | 높음 |
| 관심사 | 스레드 제어 | CPU 활용 |
그래서 동기/비동기, 블로킹/논블로킹이 무슨 관계가 있는 것인가?
동기/비동기와 블로킹/논블로킹은 서로 다른 축의 개념이다. 직접적인 포함 관계는 없고, 조합 가능한 개념이라고 볼 수 있다.
동기/비동기 = 결과를 언제, 어떻게 받는가
블로킹/논블로킹 = 제어권을 어떻게 넘기는가
| 구분 | Blocking (멈춤) | Non-blocking (안 멈춤) |
| Sync (순서 중요) | [Sync-Blocking] 가장 일반적. 작업이 끝날 때까지 기다렸다가 다음 작업 진행. (예: JDBC 조회) |
[Sync-Nonblocking] 제어권은 바로 받아서 내 할 일은 하는데, 작업이 끝났는지 계속 물어봄. (Polling) |
| Async (결과 따로) | [Async-Blocking] 제어권은 돌려줄 법도 한데 안 돌려줌 (드문 사례, 의도치 않은 상황에서 발생) -> 이 방식은 거의 안씀 |
[Async-Nonblocking] 제어권 바로 돌려주고, 내 할 일 하다가 알림(콜백) 오면 결과 처리 (예: Node.js I/O) |

Thread 기반 모델이란?
왜 Thread 기반 모델이 등장했을까?
앞서 동기/비동기, 논블로킹/블로킹에 대해 정리해보았다. 그런데 실제 서버 개발을 한다고 생각하면 이런 질문이 떠오를 것이다.
블로킹 I/O가 발생했을 때, 그 대기(멈춤) 시간동안 다른 API 요청은 어떻게 처리하지?
이러한 의문점에 대한 답으로 서버의 동시성 처리 모델이 있다. 즉, 'Thread를 여러 개 쓰는 방식'이다. Thread 기반 모델은 블로킹 I/O의 한계를 극복하기 위해 등장한 구체적인 구현 전략이다.
전통적인 서버의 요구사항
- 동시에 여러 요청을 처리해야 함
- 각 요청은 독립적인 흐름을 가짐
- 코드 흐름은 직관적이어야 함
이 요구사항에 따른 가장 단순한 해결책은 "요청 하나당 스레드 하나"이다. 그래서 등장한 게 아래에서 소개할 Thread-per-Request 모델이다.
정의
Thread 기반 모델이란, 작업을 처리하는 기본 단위를 스레드(Thread)로 사용하는 실행 모델이다.
즉, 요청마다 스레드(Thread)를 할당해서 동시에 여러 요청을 처리하는 모델이다.
스레드 기반 모델은 누가 일하는지와 요청을 어떻게 처리하는지를 정의한다.
누가 일하는지 : 스레드
요청을 어떻게 처리하는지 : 스레드를 어떻게 할당하는지
- 장점
- 동기 코드 작성 가능
- 디버깅 쉬움
- 단점
- 스레드 생성 비용 큼
- 컨텍스트 스위칭 비용
- I/O 대기 시 스레드 낭비
Thread-per-Request 모델 (가장 기본형)
- 각 요청은 전용 스레드에서 처리
- 블로킹 I/O 사용
클라이언트 요청 1 -> Thread 1 [DB 조회 중... 블로킹 대기]
클라이언트 요청 2 -> Thread 2 [파일 읽는 중... 블로킹 대기]
클라이언트 요청 3 -> Thread 3 [API 호출 중... 블로킹 대기]
Spring MVC(Tomcat) 예시
[Tomcat 스레드 풀]
요청 1 -> thread1 -> DB 쿼리 (블로킹) -> 응답
요청 2 -> thread2 -> 파일 I/O (블로킹) -> 응답
요청 3 -> thread3 -> 외부 API (블로킹) -> 응답
...
요청 201 -> ??? (Thread 없음 -> 큐에서 대기 or 거절)
그래서 Thread 기반 모델이 블로킹/논블로킹이란 무슨 상관인가?
블로킹/논블로킹은 Thread 기반 모델에서 '스레드를 어떻게 쓰느냐'를 결정하는 핵심 요소이다.
Thread 기반 모델의 전제
요청 처리의 주체 = Thread
- 스레드가 멈추면 -> 요청 처리도 멈춤
- 스레드가 놀면 -> 서버 자원 낭비
즉, Thread 기반 모델은 '요청을 스레드가 처리한다' 는 구조이고,
블로킹/논블로킹은 '그 스레드가 멈추느냐, 계속 일하느냐' 를 결정한다
둘은 완전히 분리된 개념이 아니라, 실행 구조 안에서 직접 연결된 관계다
그럼 위의 문장을 하나로 요약하자면, Thread 기반 모델은 블로킹/논블로킹 방식으로 스레드를 어떻게 쓰느냐를 결정하고, 그 위에서 동기 또는 비동기 방식으로 결과를 처리한다고 볼 수 있다.
블로킹이 Thread 기반 모델에 미치는 영향 (블로킹 I/O + Thread 기반 모델)
Client 요청 -> Thread-1 -> DB 요청
-> [Thread-1 BLOCKED] -> DB 응답 -> 처리 재개
- 요청이 들어오면 스레드 1개가 요청 1개에 할당됨
- 요청 처리 완료까지 해당 스레드 대기, 대기 시간 동안 아무 일도 못 함
- 요청 많아질수록 스레드 고갈
전통적인 Spring MVC + JDBC 구조
논블로킹이 Thread 기반 모델에 미치는 영향 (논블로킹 I/O + Thread 기반 모델)
Client 요청 -> Event Thread -> DB 요청 등록
-> 즉시 반환 -> 다른 요청 처리 -> (DB 완료 이벤트) -> 결과 처리
- 스레드가 멈추지 않음
- 소수 스레드로 다수 작업 처리 가능
- CPU 활용률 높아짐
- Context Switching 감소
Netty, WebFlux 구조
정리
Thread 기반 모델에서는 요청을 처리하는 단위가 스레드이기 때문에,
블로킹 I/O를 사용하면 스레드가 대기 상태에 빠져 자원 낭비가 발생한다.
이를 해결하기 위해 논블로킹 I/O가 도입되었고,
스레드를 점유하지 않고도 여러 요청을 처리할 수 있게 되었다.
[실행 구조] Thread 기반 모델 ->
[스레드 제어] 스레드가 멈추면? 둘 중 하나 택1 Blocking(대기) / Non-blocking(다음 작업) ->
[결과 처리] Sync / Async
Thread 기반 모델이 항상 좋은가?
논블로킹의 목적은 성능 향상 그 자체가 아니라,스레드를 효율적으로 사용하기 위함이기 때문에 상황에 따라 달라질 수 있으므로, 항상 좋다고 볼 수는 없다.
즉, 논블로킹은 '스레드 낭비를 줄이기 위한 전략'이지, 빠르게 만드는 방법은 아니다.
논블로킹이 좋은 경우
- I/O 대기가 많은 경우 (요청 많음 + 대기 시간 길 때, 예: DB, 외부 API 호출 시)
- 소수의 스레드로 다수 요청 처리 가능
- 스레드 폭증 방지
- 동시 요청 수가 매우 많은 경우
- 채팅 서버
- 실시간 스트리밍
- 게이트웨이 서버
논블로킹이 안좋은 경우
아래와 같이 CPU를 계속 써야 하는 작업은 논블로킹의 이점이 없다.
- CPU 연산 위주 작업 (이미지 처리, 암호화 등)
- Polling 잘못 쓰면 CPU 낭비
- 코드 복잡도가 중요한 경우
- 논블로킹 + 비동기
- 콜백 / 스트림 / 체인
- 가독성이 떨어지고, 디버깅이 어려워지기에 팀 숙련도 낮으면 유지보수 지옥
- 논블로킹 위에 '몰래 블로킹'이 있는 경우
- Mono.fromCallable(() -> blockingCall())
- 겉은 논블로킹, 속은 블로킹인 경우 => 이벤트 루프 스레드가 막히고, 전체 시스템이 지연됨
논블로킹은 스레드를 효율적으로 사용하기 위한 전략이지,
모든 상황에서 성능을 보장하는 만능 해결책은 아니다.
I/O 대기가 많고 동시 요청이 많은 경우에는 효과적이지만,
CPU 연산 위주이거나 복잡도가 중요한 경우에는 오히려 불리할 수 있다.
Thread 기반 모델 추가글
Thread의 구조와 비용
Thread 기반 모델을 이해하려면 Thread 자체의 비용을 알아야 합니다.
Thread 하나가 갖는 것들
[Thread]
├── Stack 메모리 (기본 512KB ~ 1MB)
│ ├── 지역 변수
│ ├── 메서드 호출 정보
│ └── 복귀 주소
├── Program Counter (현재 실행 위치)
└── CPU 레지스터 상태
Thread 하나를 만들면 기본적으로 수백 KB의 메모리가 필요합니다. Thread 200개면 최소 100MB~200MB가 스택 메모리로만 쓰입니다.
컨텍스트 스위칭 비용
CPU 코어는 하나인데 Thread는 여러 개입니다. OS는 이를 번갈아가며 실행하는데, 이때 현재 Thread의 상태를 저장하고, 다음 Thread의 상태를 복원하는 과정이 필요합니다.
Thread 1 실행 중
→ 저장: PC, 레지스터, 스택 포인터...
→ 복원: Thread 2의 PC, 레지스터, 스택 포인터...
Thread 2 실행
→ 저장: Thread 2 상태...
→ 복원: Thread 3 상태...
Thread 3 실행
...
Thread가 많을수록 컨텍스트 스위칭 횟수가 늘어나고, 실제 유용한 작업보다 스위칭 자체에 CPU를 더 쓰는 상황이 발생할 수 있습니다.
Thread 기반 모델의 한계 : C10K 문제
C10K 문제란?
2000년대 초, 동시 접속자 10,000명(10K)을 처리하는 것이 서버 설계의 큰 난제였습니다. Thread-per-Request 모델로는 이게 왜 어려울까요?
동시 접속 10,000명
= Thread 10,000개 필요
= 스택 메모리만 ~10GB
+ 컨텍스트 스위칭 폭발
= 서버 다운
특히 현대 서비스의 요청 대부분은 이런 구조입니다.
[요청 처리 시간 분석]
DB 조회 ████████████████████ 80ms (Thread는 그냥 대기)
외부 API ████████████ 50ms (Thread는 그냥 대기)
실제 연산 ██ 5ms (Thread가 실제로 일함)
-----------------------------------------
총 시간 135ms 중 Thread가 실제 일한 시간: 5ms (약 3.7%)
Thread가 대부분의 시간을 그냥 기다리면서 메모리와 CPU를 점유하고 있는 겁니다. 이게 Thread 기반 블로킹 모델의 핵심 비효율입니다.
해결책의 등장: Thread Pool + 논블로킹 I/O
Thread Pool로 개선
Thread를 매번 생성/삭제하는 비용을 줄이기 위해 미리 Thread를 만들어두고 재사용하는 방식입니다.
[Thread Pool]
┌─────────────────────────────────┐
│ Thread 1 Thread 2 Thread 3 │
│ Thread 4 Thread 5 ... │
└─────────────────────────────────┘
↑ ↑
요청 오면 빌려줌 완료되면 반납
Thread 생성/삭제 비용은 줄었지만, 블로킹 대기 문제는 여전히 존재합니다. Thread 수가 곧 동시 처리 가능한 요청 수의 한계입니다.
논블로킹 I/O + 이벤트 루프로 전환
이 한계를 근본적으로 해결한 게 논블로킹 I/O + 이벤트 루프 모델입니다.
[기존 Thread 기반 블로킹]
Thread 1: 요청A → [==DB 대기==] → 응답
Thread 2: 요청B → [==DB 대기==] → 응답
Thread 3: 요청C → [==DB 대기==] → 응답
(Thread 3개 필요)
[논블로킹 이벤트 루프]
Thread 1: 요청A 등록 → 요청B 등록 → 요청C 등록 → (이벤트 감지) → A응답 → B응답 → C응답
(Thread 1개로 처리 가능)
Node.js가 이 방식의 대표 주자입니다. 단일 Thread로 수만 개의 동시 연결을 처리합니다.
Java 진영의 변화 (Spring WebFlux)
Spring도 이 흐름에 맞춰 Reactor 기반의 WebFlux를 도입했습니다.
[Spring MVC (Thread 기반 블로킹)]
요청 → Tomcat Thread 할당 → 블로킹 처리 → 응답
Thread Pool 고갈 시 병목
[Spring WebFlux (논블로킹 비동기)]
요청 → 이벤트 루프 등록 → 논블로킹 처리 → 콜백으로 응답
소수의 Thread로 대량 처리
전체 관계 정리
[개념 레이어]
동기/비동기, 블로킹/논블로킹
↓ (이 개념들이 실제로 어떻게 구현되느냐)
Thread 기반 모델 (블로킹 I/O + 멀티 Thread로 동시성 확보)
↓ (한계 봉착 → C10K 문제)
논블로킹 I/O + 이벤트 루프 (비동기 논블로킹으로 전환)
↓ (구체적 구현체)
Node.js, Spring WebFlux, Nginx, Netty...
한 줄로 요약하면 이렇습니다.
블로킹 I/O의 대기 시간을 "Thread를 늘려서" 해결하려 한 게 Thread 기반 모델이고, 그 Thread 비용의 한계가 논블로킹/비동기 모델로의 전환을 이끌었다.
참고
https://www.inflearn.com/news/72620?srsltid=AfmBOoruK9pOc0AXoxI6ynFuJNU3ubPa7BQ0XnkgzEtvhUCjG8xjQzrh
추가적인 Blocking / Non-Blocking의 개념에 대해서 - 인프런 | 강의 공지사항
안녕하세요! 앨런입니다. 제가 짧게 강의를 만들어 보았었는데, 많은 도움 되셨었나 모르겠네요! 그래도 도움이 많이 되었다고 좋은 수강평들을 남겨주신 분들이 많아서, 수업을 만들어 보길 잘
www.inflearn.com
블로킹 Vs. 논블로킹, 동기 Vs. 비동기
와 드디어 이해했다 속이 후련~
velog.io
https://yozm.wishket.com/magazine/detail/3160/
주니어 개발자를 위한 블로킹과 논블로킹 개념 잡기 | 요즘IT
현대 소프트웨어 개발에서 효율성과 성능은 필수 요소입니다. 특히, 다양한 사용자 요청을 동시에 처리해야 하는 웹 애플리케이션이나 서버 환경에서는 블로킹과 논블로킹 모델을 제대로 이해
yozm.wishket.com
'TIL' 카테고리의 다른 글
| [TIL] 전략적 DDD와 전술적 DDD (0) | 2026.02.23 |
|---|---|
| [TIL] DDD 개념 & 설계 사고방식 (0) | 2026.02.20 |
| [TIL] 동시성 문제와 낙관락/비관락 (0) | 2026.02.04 |
| [TIL] JPA 엔티티 생명주기 (0) | 2026.01.28 |
| [TIL] JPA 동작 원리 (0) | 2026.01.27 |