비동기는 무조건 빠른가? NO
IO 병목 심한 API 서비스를 WebFlux로 바꿔야 할까?
비동기는 블로킹을 줄이는 것일 뿐, 연산 자체가 빠른 게 아님
오히려 잘못 설계된 비동기 처리는 컨텍스트 전환 비용으로 더 느려질 수도 있음
동기(Synchronous) 처리
- 개념: 클라이언트 요청이 들어오면, 해당 요청을 처리하는 동안 쓰레드가 작업을 마칠 때까지 대기한다. 결과가 나올 때까지 다른 작업은 못한다.
- Spring MVC (Servlet 기반) 같은 전통적인 웹 프레임워크가 동기처리에 해당됨
- 작동방식 : 요청 → 컨트롤러 → 서비스 → DAO → 응답 (이 모든 과정이 한 쓰레드에서 순차적으로 실행됨)
- 장점:
- 구조가 단순하고 디버깅이 쉽다.
- 기존 스프링 생태계와 연동이 잘 된다 (ex. Spring Security, AOP 등)
- 단점:
- I/O 작업(예: DB, 외부 API 호출 등)이 느리면 쓰레드가 블로킹되어 서버 자원이 낭비됨.
- 많은 요청이 동시에 들어오면 쓰레드 수가 급증해서 성능이 급격히 저하될 수 있음.
비동기(Asynchronous) 처리
- 요청을 받았을 때, I/O 등 시간이 오래 걸리는 작업은 다른 작업으로 미뤄두고, 쓰레드는 즉시 반환되어 다른 요청을 처리할 수 있도록 한다.
- Spring WebFlux (Reactor 기반) 가 대표적인 비동기 논블로킹 방식
- 작동 방식: 요청 → Mono/Flux 기반의 논블로킹 처리 → 이벤트 발생 시 응답 반환
- 장점:
- 적은 수의 쓰레드로도 많은 요청을 동시에 처리 가능 → 높은 확장성(Scalability)
- 특히 외부 API 호출, DB I/O 등 네트워크 병목이 많은 서비스에 적합
- 단점:
- 콜백 지옥 혹은 복잡한 흐름 제어(이를 해결하기 위한 연산자 학습 필요)
- 기존 라이브러리와의 호환성 문제 (블로킹 API 사용 금지 등)
- 디버깅이 어렵고 StackTrace가 끊어져서 추적이 힘듦
Webflux의 동작 방식
- 요청이
Mono
스트림으로 만들어지고 - 그 스트림 위에 각종 변환 연산자
map
,flatMap
가 체인으로 연결되고 - 응답이 올 때 그 체인에 따라 처리되므로, 중간상태를 따로 저장하지 않아도 요청-응답 맥락이 이어지는 구조
요청과 응답은 Mono
나 Flux
라는 리액티브 타입을 통해 흐름이 계속 이어져있고,
그 흐름 안에 있는 람다나 콜백 체인 (then, flapMap, map 등)을 통해 context를 유지하고 있다.
예제
1
2
3
4
5
6
7
8
9
10
11
public Mono<String> getUserInfo(String userId) {
return webClient.get()
.uri("http://external-api.com/user/" + userId)
.retrieve()
.bodyToMono(String.class)
.map(response -> {
// 이 response는 바로 위에서 요청한 userId에 대한 결과
log.info("userId: {}, response: {}", userId, response);
return "유저 정보: " + response;
});
}
✅ Servlet vs Reactor
동기 블로킹 방식이냐, 비동기 논블로킹 방식이냐 ?
Servlet은 요청마다 쓰레드를 고정 배치하는 방식, Reactor는 이벤트로 등록해 쓰레드를 비워두는 방식이다.
MVC는 직관적이지만 확장성이 떨어지고, WebFlux는 추상적이지만 고성능에 유리하다.
항목 | Servlet (Spring MVC) | Reactor (Spring WebFlux) |
---|---|---|
기반 모델 | 동기 + 블로킹 | 비동기 + 논블로킹 |
기반 스펙 | Java Servlet API (javax.servlet) | Reactive Streams + Project Reactor |
IO 처리 방식 | 요청마다 하나의 쓰레드 | 이벤트 루프 기반 (Netty 등) |
대표 컨테이너 | Tomcat, Jetty (Servlet 지원) | Netty (논블로킹 지원) |
요청 처리 흐름 | 쓰레드가 요청~응답까지 전부 책임 | 요청 이벤트 등록 후 쓰레드는 반환, 응답 시점에 다시 처리 |
리턴 타입 | String, ResponseEntity 등 (즉시 리턴) | Mono, Flux (지연 평가) |
🔍 Servlet 기반 (Spring MVC)
▶️ 특징
- 요청이 들어오면 하나의 쓰레드가 요청을 끝까지 처리
- IO 작업(DB 조회, API 호출)이 느리면 그 쓰레드는 계속 기다림 → 블로킹
- 일반적인 웹 애플리케이션에는 충분히 적합하고, 디버깅도 쉽다
▶️ 예시 흐름
1
[요청] --> DispatcherServlet --> Controller --> Service --> DAO --> DB 응답 대기 --> 결과 응답
▶️ 장점
- 기존 생태계와 호환성 좋음 (Spring Security, AOP 등)
- 학습 난이도 낮음
▶️ 단점
- 요청마다 쓰레드 1개씩 필요 → 동시 요청 증가 시 OutOfThread 가능
- 비효율적인 리소스 사용 (특히 IO 병목 심한 경우)
🚀 Reactor 기반 (Spring WebFlux)
▶️ 특징
- 요청이 들어오면 이벤트로 등록 후 쓰레드를 반환
- Mono / Flux를 사용해 작업 흐름을 연결 (비동기 체인)
- Netty처럼 논블로킹 네트워크 처리 가능한 컨테이너 사용
▶️ 예시 흐름
1
2
3
[요청] --> 이벤트 등록 --> Netty 이벤트 루프에서 대기
↓
외부 API 응답 도착 → map/flatMap 체인 실행 → 응답 전송
▶️ 장점
- 쓰레드 수를 줄이면서도 많은 요청 처리 가능 (High scalability)
- 지연 평가로 효율적인 리소스 사용
- 이벤트 기반이므로 고성능 시스템에 적합
▶️ 단점
- 디버깅 어렵고 StackTrace가 끊김
- 기존 블로킹 라이브러리와 호환 어려움
- 학습 난이도 높음