Spring MVC 와 WebFlux
Spring MVC 와 WebFlux
Spring 에는 두 가지 웹 스택이 공존합니다. 오래된 Spring MVC (Servlet 기반, 동기 블로킹) 와 비교적 새로운 Spring WebFlux (Reactor 기반, 비동기 논블로킹).
1. 두 스택에 대한 이야기
| 항목 | Spring MVC | Spring WebFlux |
|---|---|---|
| 등장 | Spring 1.0 (2004) 부터 | Spring 5.0 (2017) 부터 |
| 기반 | Servlet API (Tomcat · Jetty · Undertow) | Reactor (Netty 기본, Servlet 컨테이너도 가능) |
| 모델 | 요청당 스레드 점유 | 이벤트 루프 + 비동기 콜백 |
| 핵심 타입 | ResponseEntity<T>, 동기 반환 |
Mono<T> · Flux<T> |
Reactor 는 Pivotal/VMware 가 주도한 JVM 의 reactive streams 구현입니다. Reactive Streams 사양 (2015) 의 구현체 중 하나입니다. Mono 는 0–1 개, Flux 는 0–N 개의 비동기 시퀀스를 나타냅니다.
R2DBC (Reactive Relational Database Connectivity) 는 2018 년경 시작되어 2019 년 0.8 으로 비교적 안정화된 사양으로 알려져 있습니다. JDBC 가 본질적으로 블로킹 API 인 점을 우회하기 위한 별도 사양입니다.
2. 동기 블로킹 (MVC)
요청 한 건이 스레드 한 개를 점유합니다. DB · 외부 API · 파일 IO 동안 그 스레드는 대기합니다. Tomcat 의 기본 워커 풀 (보통 200) 만큼만 동시에 처리할 수 있고, 그 너머의 요청은 큐에서 기다립니다. 코드는 직선적이고 디버깅·예외 추적이 쉽습니다.
3. 비동기 논블로킹 (WebFlux)
이벤트 루프 스레드는 요청을 받고 콜백을 등록한 뒤 다음 요청으로 넘어갑니다. IO 가 완료되면 다시 깨어나 처리합니다. 적은 스레드로 많은 동시 연결을 다룰 수 있습니다. 대신 콜백/연산자 체인이 길어지면 흐름 추적이 어려워집니다.
// MVC
@GetMapping("/u/{id}")
public User get(@PathVariable Long id) {
return userRepo.findById(id).orElseThrow();
}
// WebFlux
@GetMapping("/u/{id}")
public Mono<User> get(@PathVariable Long id) {
return userRepo.findById(id);
}
4. Reactor 핵심
Mono.just(x)·Mono.empty()·Mono.error(e)같은 생성자.map·flatMap·filter·zip·merge같은 연산자.- 구독 (subscribe) 시점에야 실제 실행됩니다 (콜드 시퀀스).
- 백프레셔 (backpressure) 지원 — 다운스트림이 처리 가능한 만큼만 요청합니다.
5. R2DBC
JDBC 를 reactive 환경에서 그대로 쓰면 결국 어딘가에서 스레드를 막게 됩니다. R2DBC 는 드라이버 수준에서 논블로킹을 보장합니다. PostgreSQL · MySQL · SQL Server · H2 등의 R2DBC 드라이버가 존재합니다. 다만 JDBC 만큼 도구 생태계가 두텁지는 않고 일부 기능 (저장 프로시저·복잡한 트랜잭션 패턴) 은 제한적입니다.
6. Java 21 가상 스레드
Java 21 Virtual Threads (Project Loom, JEP 444, 2023-09 GA) 는 JVM 이 운영체제 스레드 대신 가상 스레드를 제공합니다. Servlet 기반 MVC 에 가상 스레드를 켜면 코드는 그대로 (동기 블로킹) 두면서 동시성 한계를 크게 풀 수 있다는 점이 자주 언급됩니다.
WebFlux 의 동기적 매력 중 일부를 MVC 가 흡수하는 흐름이라 평가됩니다. WebFlux 가 항상 정답이 아닐 수 있다는 의미입니다.
7. Kotlin Coroutines · 다른 프레임워크
Spring 은 WebFlux 위에서 코루틴 (suspend fun) 을 1 급으로 지원합니다. Reactor 연산자 대신 순차 코드처럼 작성하면서 비동기 이득은 유지합니다.
JVM 위의 다른 프레임워크 — Quarkus · Micronaut. 컴파일 타임 DI · GraalVM 친화 같은 지점이 다릅니다.
8. .block() 점진 제거
WebFlux 코드에서 .block() 은 현재 스레드를 막습니다. 이벤트 루프 스레드에서 호출하면 전체 처리량이 무너질 수 있습니다. 흔한 점진 제거 순서:
① 컨트롤러 반환 타입을 Mono/Flux 로 변경
② 서비스 계층의 T 반환을 Mono<T> 로 끌어올림
③ 리포지토리(R2DBC)까지 닿으면 중간 .block() 모두 제거
④ 외부 HTTP 호출은 WebClient (RestTemplate 의 비동기 대체)
9. 두 스택의 혼용
같은 모듈에서 MVC 와 WebFlux 컨트롤러를 동시에 쓰는 것은 권장되지 않습니다. 모듈을 분리하거나, 한쪽 스택 위에서 다른 쪽 라이브러리만 (예: MVC 위에서 WebClient) 도입하는 식이 안전합니다.
10. 자주 걸리는 자리
이벤트 루프 위에서의 블로킹 호출 — JDBC · 파일 IO · Thread.sleep · 무거운 CPU 연산. Schedulers.boundedElastic() 같은 별도 스케줄러로 옮겨야 합니다.
Mono.subscribe() 만 호출하고 결과 무시 — WebFlux 컨트롤러는 반환된 Mono/Flux 를 프레임워크가 구독합니다. 직접 subscribe() 를 호출하면 내부적으로 fire-and-forget 이 되어 트랜잭션·에러 처리가 누락될 수 있습니다.
트랜잭션 경계 — R2DBC 트랜잭션은 TransactionalOperator 또는 @Transactional (반응형 매니저 설정 필요) 로 다뤄야 하고 JDBC 와 같은 의미가 아닙니다.
디버깅 — 스택 트레이스가 연산자 체인 위에서 끊깁니다. Hooks.onOperatorDebug() 또는 BlockHound · checkpoint 연산자가 도움이 됩니다.
하고픈 말
WebFlux 가 항상 더 빠르거나 더 모던한 것은 아닙니다. 진짜 이득은 IO 대기 시간이 처리 시간보다 훨씬 큰 자리에서 보입니다. Java 21 가상 스레드 도입 후에는 MVC 의 단순함이 다시 매력적인 자리가 늘 것입니다.
Next
- fastapi-philosophy
Spring MVC 공식 · Spring WebFlux 공식 · Project Reactor · Reactive Streams 사양 · R2DBC 공식 · JEP 444 Virtual Threads · Spring + Virtual Threads 을 참고합니다.