우당탕탕
Spring WebFlux 적용하다 헷갈렸던 개념들, 저랑 다르네요 본문
사실 Spring WebFlux를 도입하면서 생각보다 개념적으로 혼란스러운 게 많았어요. 리액티브 프로그래밍 자체가 낯설기도 했고, 공식 문서랑 주변 사례들이랑 비교하다 보니 차이점이 더 눈에 띄더라고요.
이 글에서는 제가 직접 Spring WebFlux를 적용하면서 헷갈렸던 부분들을 중심으로, 다른 개발자분들이 겪은 경험이나 A상품인 Spring MVC와 B상품인 WebFlux의 차이점까지 비교해보려고 해요. 덕분에 여러분도 리액티브를 이해하는 데 도움이 될 거예요.
개발 환경 / 버전 정보
저는 Java 17과 Spring Boot 3.2 기반으로 WebFlux를 적용했어요. 의존성은 spring-boot-starter-webflux를 추가했고, 리액터 코어도 기본적으로 포함돼 있죠.
서블럭킹과 논블럭킹, 여기서 많이 헷갈렸어요
사실 이 부분이 가장 정신없었는데요, 서블럭킹(blocking)과 논블럭킹(non-blocking)의 차이를 이해하는 게 생각보다 쉽지 않았어요. 저도 처음엔 WebFlux를 논블럭킹이라고 해서 단순히 쓰레드가 안 기다리는 줄 알았거든요.
그런데 여기서 저와 다른 개발자분들이 경험한 게 달랐어요. 어떤 분들은 "WebFlux도 내부에서 쓰레드가 블럭킹되는 부분이 있으면 의미가 없다"고 하고, 또 다른 분들은 "논블럭킹이라는 말에는 여러 층위가 있다"면서 조금 더 복잡하게 말하더라고요.
// 전형적인 서블럭킹 예시
String result = blockingService.getData(); // 이 부분이 실제 DB 호출이라 블럭됨
return Mono.just(result); // Mono로 감싸도 이미 블럭킹은 끝난 상태
// 진짜 논블럭킹 방식
Mono resultMono = reactiveService.getData(); // Mono 반환, 내부에서 비동기 처리
return resultMono;
이처럼 단순히 Mono로 감싸는 것과 내부 호출이 리액티브하게 논블럭킹인지가 큰 차이더라고요. 저는 초기에 이걸 잘못 적용해서 의미 없는 WebFlux 코드를 짰던 거죠.
Flux랑 Mono, 뭐가 다르고 언제 써야 할까요?
WebFlux에서 가장 기본적으로 사용하는 두 데이터 타입인 Flux와 Mono의 차이도 사람마다 설명이 조금씩 달라서 헷갈리더라고요. 저는 처음에 둘 다 그냥 비슷하게 사용해도 된다고 생각했는데, 프로젝트 하면서 느낀 점은 의도에 맞게 꼭 구분해서 써야 한다는 거예요.
- Mono: 단일 값 또는 없을 때 사용. 예를 들어 사용자 한 명 조회할 때 적합해요.
- Flux: 0~N개의 값이 있을 때 쓰는데, 예를 들어 게시글 목록 같은 여러 건 데이터 처리 시 주로 사용해요.
이 구분을 무시하면, 예를 들어 단일 조회인데 Flux를 사용하거나 반대로 여러 건인데 Mono를 쓰면서 downstream에서 예외가 발생하거나 퍼포먼스가 떨어지는 경우가 있더라고요.
저는 이렇게 WebFlux 컨트롤러를 작성했어요
다른 분들 사례랑 비교하면서 가장 편리했던 건, Spring WebFlux의 RouterFunction과 HandlerFunction 방식을 활용하는 방법이었어요. 전통적인 @RestController보다 훨씬 선언적이고 테스트하기도 좋더라고요.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
@Configuration
public class UserRouter {
@Bean
public RouterFunction userRoutes(UserHandler handler) {
return route(GET("/users/{id}"), handler::getUserById)
.andRoute(GET("/users"), handler::getAllUsers);
}
}
그리고 핸들러에서는 Mono, Flux를 리턴해서 WebFlux가 알아서 비동기 처리하게 했는데, 이 부분은 제 경험상 가장 직관적이었어요.
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
@Component
public class UserHandler {
private final UserService userService;
public UserHandler(UserService userService) {
this.userService = userService;
}
public Mono getUserById(ServerRequest request) {
String id = request.pathVariable("id");
return userService.findById(id)
.flatMap(user -> ServerResponse.ok().bodyValue(user))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono getAllUsers(ServerRequest request) {
return ServerResponse.ok().body(userService.findAll(), User.class);
}
}
이 방법은 기존 @GetMapping 방식보다 라우팅 로직을 함수형으로 분리해서 재사용성과 테스트 커버리지가 좋아지는 게 눈에 보였어요.
여기서 삽질했던 부분들, 나만 그런 줄 알았는데
가장 골치 아팠던 건, Spring WebFlux와 비동기 데이터베이스 드라이버 사용 시 발생했던 오류였어요. 저는 R2DBC로 Postgres 연결을 시도했는데, 첫 번째 테스트에서 "Connection refused"랑 "No suitable driver found" 같은 에러가 계속 났거든요.
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'r2dbcEntityTemplate'
Caused by: io.r2dbc.spi.R2dbcNonTransientResourceException: Connection refused
// 추가적으로 나타났던 에러 메시지
java.sql.SQLNonTransientConnectionException: No suitable driver found for jdbc:postgresql://localhost:5432/mydb
원인을 찾아보니, 다른 분들 경험과 달리 저는 의존성을 잘못 추가했거나, application.properties 설정에서 spring.r2dbc.url과 spring.datasource.url을 헷갈린 게 문제였더라고요. JDBC 드라이버가 아니라 R2DBC 드라이버를 별도 설정해야 하는 점이요.
그리고 또 하나 삽질했던 건, 기존 동기 코드에서 Mono/Flux를 억지로 감싸는 경우인데, 이러면 큰 효과가 없고 오히려 복잡도만 올라가더라고요. 반면에 외부 API 호출 등 리액티브 리포지토리를 제대로 쓸 수 있으면 성능이나 확장성 측면에서 확실한 이점이 있더군요.
심화: WebFlux 쓸 때 이런 점도 알고 있으면 좋아요
WebFlux는 Netty 기반이라 기본적으로 동기 Servlet 컨테이너가 아니에요. 그래서 필터나 인터셉터 같은 방식이 차이가 나는데, 저는 이 부분에서 처음에 헤맸어요. 예를 들어, WebFilter가 서블럭킹 필터와 다르다는 점.
- 서블럭킹 서블릿에서는
Filter와Interceptor를 많이 쓰죠. - WebFlux에선
WebFilter를 써야 하고, 리액티브 스트림 방식으로 동작해요. - 이걸 잘못 쓰면 요청이 응답이 오기 전에 종료되거나 타임아웃 이슈가 발생해요.
그리고 WebFlux는 스프링 시큐리티 쓸 때도 Reactive Security 모듈을 써야 하는데, 저는 처음에 그냥 동기 방식 하다가 권한 체크가 안 되는 현상이 있었어요.
자주 물어보시는 것들
Q. WebFlux로 항상 더 좋은 성능을 낼 수 있나요?
A. 꼭 그렇진 않아요. WebFlux는 특히 I/O 작업이 많은 서비스에서 효과적이고, CPU 바운드 작업이 많으면 오히려 복잡성과 디버깅 비용이 커질 수 있어요. 저도 CPU 집약적 작업엔 오히려 WebMVC가 더 낫다고 느꼈어요.
Q. 기존 MVC 프로젝트에 WebFlux를 혼합해서 쓸 수 있나요?
A. 네, 가능합니다. Spring Boot 2.x 이상부터 WebFlux와 MVC를 동시에 쓸 수 있는데, 라우팅이나 의존성 관리가 복잡해질 수 있으니 주의가 필요해요. 저도 일부 마이크로서비스에만 WebFlux 적용해서 점진적으로 이전했어요.
WebFlux 도입은 단순히 코드를 바꾸는 게 아니라 아키텍처와 운영 패러다임까지 영향을 주는 일이더라고요. 그래서 저는 처음에 작게 시작해서 문제점과 장점을 직접 체험하는 걸 추천해요. 이런 과정에서 중요한 건 "진짜 리액티브하게" 코드를 작성하는 것과 "표면적으로만" Mono, Flux를 쓰는 것의 차이를 확실히 아는 거예요.
'Tech > Spring' 카테고리의 다른 글
| Spring Cloud Gateway 도입하면서 겪은 라우팅 문제와 해결 체크리스트 (0) | 2026.06.09 |
|---|---|
| Spring Boot에서 Querydsl 도입, 직접 해보고 정리한 절차와 팁 (0) | 2026.06.03 |
| JPA Auditing으로 생성·수정 시간 자동화하다가 겪은 삽질 후기 (0) | 2026.06.01 |
| Spring Boot JWT 로그인 구현하다 겪은 삽질과 해결 과정 공유합니다 (0) | 2026.05.27 |
| Spring Batch 처음 써보면서 겪은 시행착오와 해결 과정 공유합니다 (0) | 2026.05.26 |
