이름은 비슷한데, 둘은 “컨트롤러 작성법”보다 기다리는 방식을 어떻게 다룰지에서 갈라져요.지난 글에서는 작은 Todo API를 Spring MVC로 만들어봤어요.
@RestController를 만들고, GET /todos, POST /todos 같은 요청을 받고, JSON으로 응답했죠.
그런데 Spring Boot 웹 자료를 찾다 보면 곧 이런 단어를 만나게 돼요.
“MVC 말고 WebFlux를 써야 하나요?"처음에는 WebFlux가 Spring MVC의 다음 버전처럼 보일 수 있어요. 근데요, 사실은 그렇게 읽으면 위험해요. Spring MVC와 WebFlux는 구버전/신버전 관계가 아니라, 서로 다른 실행 모델을 가진 두 웹 stack이에요. 오늘은 Annotation 이름보다 먼저, 요청을 처리하는 thread와 I/O 대기 방식을 볼 거예요. 결론부터 말하면 대부분의 일반적인 REST API, 특히 JDBC나 JPA를 쓰는 서비스는 Spring MVC가 더 자연스러워요. WebFlux는 “더 최신이라서”가 아니라, 애플리케이션 전체가 non-blocking 흐름으로 설계될 때 힘을 발휘해요.
"Reactive가 더 빠른 거 아닌가요?"
"요즘은 WebFlux가 최신 방식인가요?"
"JPA 쓰는 프로젝트도 WebFlux로 바꾸면 성능이 좋아지나요?”
이 글은 Spring Boot 4.1.0과 Spring Framework 7.0.8 공식 문서의 Servlet Web Applications, Reactive Web Applications, Spring Web MVC, Spring WebFlux 설명을 기준으로 작성했어요. 개념 자체는 Spring Boot 3.x 프로젝트를 읽을 때도 유효하지만, starter 이름과 auto-configuration package는 Boot 4.x 기준으로 읽어주세요.
겉모습이 비슷해서 더 헷갈려요
Spring MVC controller는 이렇게 생겼어요.@RestController, @GetMapping, @PathVariable을 쓰죠. 그래서 처음에는 “반환 타입만 Mono로 바꾸면 WebFlux인가요?”라고 느끼기 쉬워요.
하지만 중요한 차이는 controller 모양보다 아래쪽에 있어요.
여기서
Mono는 “값이 0개 또는 1개 나중에 올 수 있다”는 reactive 타입이에요. Flux는 “값이 0개 이상 여러 개 흐를 수 있다”는 타입이고요. 단순히 Optional이나 List의 멋진 이름이 아니라, 데이터가 준비되는 시점과 흐름을 runtime이 조합할 수 있게 표현하는 타입이에요.
처음에는 여기까지만 잡아도 충분해요. MVC는 요청마다 일하는 thread가 기다릴 수 있는 모델이고, WebFlux는 기다리는 동안 thread를 붙잡지 않는 모델이에요.
MVC는 “기다리는 thread”를 전제로 해요
식당에 비유하면 Spring MVC는 손님 한 명을 직원 한 명이 맡는 방식에 가까워요. 손님이 주문하고, 주방에서 음식이 나올 때까지 직원이 그 주문을 담당해요. 중간에 주방을 기다리는 시간이 있어도 직원은 그 주문의 흐름 안에 있어요. 손님이 많아지면 직원도 더 필요해지죠. Spring MVC 요청 흐름도 비슷하게 읽을 수 있어요. 이 그림에서 핵심은 thread가 요청 흐름을 따라 끝까지 붙어 있다는 점이에요. DB 호출이나 외부 API 호출처럼 시간이 걸리는 작업이 있으면 그 thread는 기다려요. 이걸 blocking이라고 불러요. blocking이 무조건 나쁜 건 아니에요. 오히려 많은 업무 시스템에서는 이 모델이 읽기 쉽고, 디버깅하기 쉽고, 사용할 수 있는 라이브러리 선택지도 넓어요.findById를 호출하고, 없으면 예외를 던지고, 있으면 response DTO로 바꿔요. JDBC나 JPA repository는 보통 이런 방식과 잘 맞아요. 호출한 thread가 DB 응답을 기다리고, 결과가 오면 다음 줄로 넘어가니까요.
실무에서는 이 단순함이 꽤 큰 장점이에요.
그래서 “Spring MVC는 옛날 방식”이라고 말하면 설명이 너무 거칠어요. Spring MVC는 Servlet 기반의 성숙한 웹 stack이고, blocking 라이브러리를 쓰는 많은 서버 애플리케이션에는 여전히 가장 자연스러운 선택이에요.
WebFlux는 “안 기다리는 thread”를 전제로 해요
WebFlux는 다른 질문에서 출발해요.“요청 중간에 외부 API 응답을 오래 기다려야 한다면, 그동안 thread를 붙잡고 있어야 할까요?”예를 들어 주문 화면 하나를 만들기 위해 세 군데 외부 서비스를 불러야 한다고 해볼게요.
- 주문 서비스에서 주문 정보를 가져와요.
- 배송 서비스에서 배송 상태를 가져와요.
- 포인트 서비스에서 적립 정보를 가져와요.
Mono, Flux, map, flatMap, zip 같은 단어가 자주 나와요.
WebFlux가 항상 더 빠른 건 아니에요
가장 흔한 오해가 이거예요.“Reactive면 더 빠르다.”사실은 아니에요. non-blocking과 reactive는 요청 하나의 CPU 계산을 마법처럼 빠르게 만들지 않아요. 오히려 작은 요청 하나만 보면 reactive pipeline을 구성하고 흐름을 추적하는 비용이 더 복잡할 수 있어요. WebFlux의 장점은 보통 이런 상황에서 드러나요.
반대로 이런 상황이라면 WebFlux로 바꾸는 이득이 작거나, 오히려 복잡도만 커질 수 있어요.
여기부터는 조금 더 깊게 볼게요. WebFlux에서 가장 위험한 실수는 event loop에서 blocking 작업을 하는 거예요.
Mono<OrderResponse>를 반환하니까 WebFlux처럼 보여요. 하지만 orderRepository.findById(id)가 JPA repository라면 이미 그 줄에서 DB 응답을 기다려요. Mono.just(...)는 blocking 호출을 non-blocking으로 바꿔주는 도구가 아니에요. 이미 계산된 값을 Mono 안에 넣을 뿐이에요.
blocking API를 꼭 호출해야 한다면 별도 scheduler로 밀어내는 escape hatch가 있긴 해요.
WebClient로 쓰는 편이 더 단순할 수 있어요.
의존성을 보면 어느 stack으로 뜨는지 감이 와요
Spring Boot에서는 starter가 중요한 신호예요. MVC 앱은 보통 이렇게 시작해요.DispatcherServlet이 요청을 controller로 보내고, WebFlux는 reactive stack 위에서 DispatcherHandler가 handler 흐름을 이어가요.
Spring Boot 3.x 자료에서는 MVC starter를
spring-boot-starter-web으로 설명하는 경우가 많아요. 이 블로그의 Spring Boot 4.x 기준 예제에서는 MVC starter를 spring-boot-starter-webmvc로 읽고 있어요. 기존 프로젝트를 유지보수한다면 실제 build.gradle이나 pom.xml의 의존성을 먼저 확인하세요.Spring MVC 앱에서도 WebClient는 쓸 수 있어요
여기서 또 하나 헷갈리는 지점이 있어요.“외부 API를 많이 부르면 무조건 WebFlux 앱으로 바꿔야 하나요?”그렇지는 않아요. Spring MVC 앱에서도 reactive
WebClient를 사용할 수 있어요. 웹 서버 stack은 MVC로 유지하면서, outbound HTTP 호출만 WebClient로 다루는 식이에요.
예를 들어 MVC controller가 있고, service 안에서 외부 API를 호출한다고 해볼게요.
.block()을 호출하죠. MVC 앱에서는 요청 thread가 기다리는 모델이므로, 이 선택이 코드상으로는 자연스러울 수 있어요. 다만 timeout, retry, circuit breaker 같은 outbound API 경계 설계를 반드시 같이 해야 해요. 이 주제는 뒤의 HTTP client 글에서 더 자세히 볼 거예요.
반대로 WebFlux 앱에서는 controller 흐름 안에서 .block()을 호출하면 매우 조심해야 해요. event loop를 막을 수 있기 때문이에요.
Mono로 이어가는 쪽이 맞아요.
테스트와 디버깅도 선택 기준이에요
Spring MVC와 WebFlux는 테스트 도구의 감각도 달라져요. MVC controller를 테스트할 때는 보통 MockMvc를 자주 봐요.
처음 팀이 Spring Boot를 배우는 중이라면 이 차이가 큽니다. WebFlux는 “컨트롤러만 조금 바꾸면 되는 선택”이 아니라, 장애를 읽는 방식까지 함께 바꾸는 선택이에요.
그러면 무엇을 선택하면 될까요?
선택을 너무 어렵게 만들 필요는 없어요. 먼저 지금 애플리케이션이 무엇을 기다리는지 보면 돼요.
실무에서는 이 질문들이 더 직접적이에요.
- 우리 DB 접근은 JPA/JDBC인가요, R2DBC인가요?
- 외부 API latency가 실제 병목인가요?
- thread pool과 connection pool 중 무엇이 먼저 고갈되나요?
- reactive operator와 scheduler를 팀이 코드 리뷰할 수 있나요?
- 장애가 났을 때 event loop blocking을 찾을 관측 도구가 있나요?
- WebFlux로 바꿨을 때 테스트와 운영 runbook도 같이 바뀌나요?
자, 정리해볼까요?
- Spring MVC와 WebFlux는 구버전/신버전 관계가 아니라 서로 다른 웹 stack이에요.
- MVC는 Servlet 기반이고, 요청 처리 중 thread가 blocking 호출을 기다릴 수 있다는 전제로 동작해요.
- WebFlux는 reactive 기반이고, event loop를 막지 않는 non-blocking 흐름을 전제로 동작해요.
Mono.just()로 blocking 결과를 감싼다고 non-blocking 코드가 되지는 않아요.- JPA/JDBC 중심의 일반적인 CRUD API라면 Spring MVC가 보통 더 단순하고 자연스러워요.
- WebFlux는 많은 원격 I/O, streaming, reactive persistence처럼 stack 전체가 non-blocking으로 이어질 때 검토할 가치가 커져요.
참고한 링크
- Spring Boot Reference Documentation: Web
- Spring Boot Reference Documentation: Servlet Web Applications
- Spring Boot Reference Documentation: Reactive Web Applications
- Spring Framework Reference Documentation: Spring Web MVC
- Spring Framework Reference Documentation: Spring WebFlux
- Spring Framework Reference Documentation: WebFlux Overview