13.1 Spring WebFlux 개요 및 MVC와의 차이
스프링 5.0부터 도입된 Spring WebFlux 는 비동기-논블로킹(Asynchronous Non-blocking) 방식의 리액티브 웹 프레임워크입니다. 전통적인 Spring MVC와 어떤 차이가 있고 왜 필요한지 알아봅니다.
1. WebFlux가 등장한 배경
기본적인 Spring MVC(Servlet 기반)는 요청당 스레드 하나를 할당하는 Thread-per-Request 모델을 사용합니다. 이 방식은 동시 접속자가 급증할 경우 스레드 수가 늘어나며 메모리 소모가 커지고, 스레드 간 전환(Context Switching) 비용이 발생하는 한계가 있습니다.
이를 해결하기 위해 적은 수의 스레드만으로 대량의 동시 접속을 효율적으로 처리하기 위해 등장한 것이 바로 WebFlux입니다.
핵심 개념: 반응형 프로그래밍(Reactive Programming)
반응형 프로그래밍이란 데이터 스트림(Data Stream) 과 변경 사항의 전파(Propagation of Change) 에 중점을 둔 프로그래밍 패러다임입니다. 데이터가 생성될 때마다 이를 소비하는 측에 전달(Push)하는 방식으로 동작하며, 비동기 처리를 기본으로 합니다.
2. Spring MVC vs Spring WebFlux 핵심 비교
| 비교 항목 | Spring MVC | Spring WebFlux |
|---|---|---|
| 모델 | 동기 + 블로킹 (Blocking) | 비동기 + 논블로킹 (Non-blocking) |
| 처리 방식 | Thread-per-request | Event Loop (적은 스레드로 많은 요청 처리) |
| 스택 | Servlet API (Tomcat 등) | Reactive Streams (Netty 등) |
| 데이터베이스 | JDBC (Blocking) | R2DBC, NoSQL (Non-blocking) |
| 권장 용도 | 일반적인 웹 앱, 전통적인 CRUD | 대규모 스트리밍, 채팅, 서버 간 호출이 많은 MSA |
3. 리액티브 스트림 (Mono와 Flux)
WebFlux를 이해하려면 Reactor 라이브러리의 두 가지 핵심 타입을 알아야 합니다.
Project Reactor와 Publisher-Subscriber 패턴
Reactor는 리액티브 스트림즈 표준을 구현한 라이브러리로, 데이터를 발행하는 Publisher 와 이를 구독하여 소비하는 Subscriber 간의 관계를 정의합니다.
- 구독(Subscribe): Subscriber가 Publisher를 구독합니다.
- 데이터 요청(Request): Subscriber가 받을 수 있는 데이터의 개수를 Publisher에게 요청합니다. (** 백프레셔, Backpressure**)
- 데이터 전달(onNext): Publisher가 데이터를 전달합니다.
- 완료/에러(onComplete/onError): 모든 데이터 전달이 완료되거나 에러가 발생하면 종료됩니다.
핵심 데이터 타입: Mono와 Flux
Mono<T>: 0개 또는 1개의 데이터를 발행 (단건 응답)Flux<T>: 0개 또는 N개의 데이터를 발행 (다건 응답, 스트리밍)
// Mono 예시: 성공 시 "Hello" 발행 후 종료
Mono<String> name = Mono.just("홍길동")
.map(String::toUpperCase)
.flatMap(s -> Mono.just("Name: " + s));
// Flux 예시: 여러 개일 때 유용 (주로 리스트 대신 사용)
Flux<Integer> numbers = Flux.just(1, 2, 3, 4, 5)
.filter(n -> n % 2 == 0)
.map(n -> n * 10);
4. 컨트롤러 구현 방식
WebFlux는 기존 MVC와 동일한 @Controller 애너테이션 방식(Annotated Controller)과, 새로운 함수형 모델(Functional Endpoint) 두 가지 방식을 지원합니다.
1) 애너테이션 방식 (Annotated Controller)
기존 MVC 사용자에게 익숙한 방식이지만, 리턴 타입이 Mono나 Flux여야 합니다.
@RestController
public class HelloController {
@GetMapping("/hello")
public Mono<String> hello() {
return Mono.just("Hello, WebFlux!");
}
}
2) 함수형 방식 (Functional Endpoint)
Router와 Handler를 분리하여 더 선언적이고 명확하게 엔드포인트를 정의합니다.
@Component
public class HelloHandler {
public Mono<ServerResponse> hello(ServerRequest request) {
return ServerResponse.ok().bodyValue("Hello, Reactive!");
}
}
@Configuration
public class HelloRouter {
@Bean
public RouterFunction<ServerResponse> route(HelloHandler handler) {
return RouterFunctions.route(RequestPredicates.GET("/hello"), handler::hello);
}
}
5. 실무 고려 사항
- 논블로킹의 연쇄: WebFlux 환경에서 중간에 블로킹 코드(예: 일반 JDBC, Thread.sleep)가 섞이면 성능이 급격히 저하됩니다. 모든 라이브러리가 논블로킹을 지원해야 효과를 볼 수 있습니다.
- 이벤트 루프(Multi Event Loop): WebFlux는 적은 수의 고정된 스레드(보통 CPU 코어 수 * 2)로 수많은 요청을 처리하는 멀티 이벤트 루프 모델을 사용합니다. 특정 요청이 스레드를 오랫동안 점유하지 않도록 주의해야 합니다.
- 백프레셔(Backpressure) 관리: 데이터를 소비하는 쪽의 속도가 공급하는 쪽보다 느릴 때 부하를 제어하는 방식이 포함되어 있어 시스템 안정성을 높일 수 있습니다.
- 러닝 커브: 리액티브 프로그래밍은 기존 명령형 프로그래밍에 비해 사고의 전환이 필요하며 디버깅이 까다로울 수 있습니다.