Ver2.5 Spring WebFlux로 리액티브 정복하기: 개발자, 이제 '기다림'의 시대를 끝내자!

Spring WebFlux로 리액티브 정복하기: 개발자, 이제 '기다림'의 시대를 끝내자!
맨날 막히고 기다리는 코드에 지쳤어? 커피 한 잔 타와. 이제부터 너의 개발 라이프를 바꿀 리액티브 프로그래밍의 세계로 떠나볼 거니까!
들어가며: 왜 우리는 또 새로운 걸 배워야 할까?
안녕! 코딩하다 보면 그런 생각 들 때 있지 않아? "아, 이거 API 응답 올 때까지 그냥 놀아야 하나?" 혹은 "사용자가 몰리니까 서버가 버벅대네... 스레드는 왜 맨날 놀고먹는 애들뿐이지?" 이런 고민, 나만 해본 거 아닐걸?
우리가 지금까지 익숙하게 사용해온 Spring MVC는 '스레드-퍼-리퀘스트(Thread-per-Request)' 모델을 기반으로 동작해. 클라이언트 요청이 하나 들어올 때마다 스레드를 하나씩 할당하는 방식이지. 마치 은행 창구에 손님이 오면 직원 한 명이 처음부터 끝까지 전담 마크하는 거랑 비슷해. 간단하고 직관적이라서 오랫동안 사랑받아왔어.
하지만 이 방식엔 치명적인 단점이 있어. 만약 직원이 손님의 서류를 처리하다가 다른 부서의 확인이 필요해서 전화를 걸었다고 생각해봐. 그 직원은 전화 통화가 끝날 때까지 아무것도 못 하고 멀뚱멀뚱 기다려야 해. 그동안 뒤에 줄 서 있는 다른 손님들은? 그냥 하염없이 기다리는 거지. 이게 바로 **블로킹(Blocking) I/O**의 현실이야.
서버 애플리케이션에서 이런 블로킹은 대부분 데이터베이스 조회나 다른 외부 API를 호출할 때 발생해. 스레드는 비싼 자원인데, 이렇게 I/O 작업을 기다리느라 아무 일도 못 하고 낭비되는 거지. 사용자가 폭주하는 현대적인 서비스 환경에서는 이런 비효율이 곧 서비스 장애로 이어질 수 있어. "서버 터졌다"는 말이 괜히 나오는 게 아니라고.
그래서 등장한 슈퍼스타가 바로 **리액티브 프로그래밍(Reactive Programming)**이야. 이건 완전히 새로운 패러다임이야. "기다리지 말고, 일이 끝나면 나에게 알려줘!"라는 철학을 가지고 있지. 은행 창구 직원이 서류 확인 전화를 걸어놓고, 전화가 올 때까지 다른 손님들의 간단한 업무를 처리해주는 것과 같아. 훨씬 효율적이지? 이렇게 하면 적은 수의 스레드(직원)로도 훨씬 많은 요청(손님)을 처리할 수 있게 돼.
그리고 이 리액티브 프로그래밍을 Spring 생태계에서 아주 우아하게 구현할 수 있도록 도와주는 프레임워크가 바로 **Spring WebFlux**야. WebFlux는 비동기-논블로킹(Asynchronous & Non-Blocking) 방식으로 동작해서, 시스템 자원을 극한까지 쥐어짜 내 최고의 성능을 보여줘. 특히 마이크로서비스 아키텍처(MSA)처럼 수많은 서비스가 서로 통신하며 데이터를 주고받는 환경에서 진가를 발휘하지.
자, 이제 왜 우리가 WebFlux를 배워야 하는지 감이 좀 와? 이건 그냥 '새로운 기술'이 아니라, '더 나은 애플리케이션'을 만들기 위한 필수 교양 같은 거야. 그럼 지금부터 이 매력적인 리액티브의 세계로 한 걸음 더 들어가 보자고!
리액티브 프로그래밍 핵심 개념 뽀개기
WebFlux를 제대로 이해하려면, 그 근간이 되는 리액티브 프로그래밍의 핵심 개념들을 먼저 알아야 해. 처음엔 좀 낯설 수 있지만, 하나씩 뜯어보면 별거 아니야. 내가 쉽게 설명해 줄게.
데이터의 흐름, 스트림(Stream)
리액티브 프로그래밍의 모든 것은 '데이터 스트림'이라는 개념에서 시작해. 기존 프로그래밍이 데이터를 '덩어리'로 취급했다면, 리액티브는 데이터를 **'시간의 흐름에 따라 발생하는 이벤트의 연속'**으로 봐.
이게 무슨 말이냐고? 유튜브 영상을 생각해보자. 1시간짜리 4K 영상을 볼 때, 우리는 영상 파일 전체가 다운로드될 때까지 기다리지 않잖아. 재생 버튼을 누르면 영상 데이터가 실시간으로 조금씩 흘러들어오고(스트리밍), 우리는 그걸 바로 볼 수 있지. 이게 바로 스트림이야.
리액티브 프로그래밍에서 데이터는 이런 스트림을 통해 흐르는 이벤트(값, 완료, 에러)로 표현돼. 예를 들어, 데이터베이스에서 100만 건의 사용자 데이터를 가져온다고 해보자.
- **전통적인 방식:** 100만 건 데이터를 모두 메모리에 올린 `List
`를 반환. 메모리가 터질 수도 있고, 첫 번째 데이터를 보기 위해 100만 건 조회가 끝날 때까지 기다려야 함. - **리액티브 방식:** `Flux
`라는 스트림을 반환. 데이터가 준비되는 대로 하나씩 흘려보내 줌. 클라이언트는 데이터가 도착하는 즉시 처리할 수 있고, 메모리 부담도 훨씬 적어.
이 스트림을 통해 데이터가 흐르는 파이프라인을 만들고, 중간중간에 데이터를 가공(map, filter 등)하는 연산자(Operator)들을 끼워 넣는 것이 리액티브 프로그래밍의 기본이야.
리액티브 스트림즈 명세 (Reactive Streams Specification)
리액티브 프로그래밍이 여기저기서 중구난방으로 구현되면 혼란스럽겠지? 그래서 똑똑한 아저씨들이 모여서 표준 명세를 만들었어. 이게 바로 **리액티브 스트림즈(Reactive Streams)**야. Java 9부터는 `java.util.concurrent.Flow` API로 표준에 포함되기도 했어. 이 명세는 딱 4개의 핵심 인터페이스만 정의하고 있어.
1. Publisher (발행자) 데이터를 생성하고 발행하는 주체. 데이터 스트림 그 자체라고 볼 수 있어. "나 데이터 줄 수 있어! 구독할 사람?" 하고 외치는 역할이야.
2. Subscriber (구독자)
Publisher가 발행하는 데이터를 소비하는 주체. "데이터 주세요! 받아서 처리할게요!" 하고 응답하는 역할이지. Publisher를 구독(subscribe)해야만 데이터를 받을 수 있어.
3. Subscription (구독 정보)
Publisher와 Subscriber 사이의 연결고리. 구독이 성립되면 Publisher가 Subscriber에게 이 Subscription 객체를 전달해. Subscriber는 이걸 통해 "데이터 n개만 더 줘!" 하고 요청하거나 "이제 그만 줘!" 하고 구독을 취소할 수 있어.
4. Processor (처리자)
Publisher이면서 동시에 Subscriber인 특별한 녀석. 데이터를 받아서 가공한 뒤, 다시 다른 Subscriber에게 발행할 수 있어. 데이터 파이프라인의 중간 처리기 역할을 해.
이 네 가지 구성요소의 상호작용이 리액티브 시스템의 핵심이야. Publisher는 Subscriber가 요청하지 않으면 데이터를 보내지 않아. 모든 것은 **Subscriber의 주도**하에 이루어지지. 이게 바로 뒤에서 설명할 '배압(Backpressure)'의 기반이 되는 중요한 원칙이야.
논블로킹(Non-Blocking) & 비동기(Asynchronous)
이 두 단어는 항상 같이 나오지만, 미묘하게 다른 개념이라 헷갈리기 쉬워. 확실하게 짚고 넘어가자.
논블로킹 (Non-Blocking): 어떤 함수를 호출했을 때, 그 함수의 작업이 끝날 때까지 **기다리지 않고 바로 리턴**하는 걸 말해. 결과는 나중에 콜백(Callback)이나 다른 메커니즘을 통해 받아. 즉, 호출한 쪽의 실행 흐름을 막지(Block) 않는다는 의미야.
비동기 (Asynchronous): 어떤 작업을 요청하면, 그 작업이 **별도의 스레드나 컨텍스트에서 실행**되고, 완료되면 알려주는 방식이야. 작업의 시작과 끝이 동기화되지 않는다는 뜻이지.
WebFlux는 이 두 가지를 조합해서 사용해. 클라이언트 요청이 들어오면, 이벤트 루프(Event Loop)라는 아주 적은 수의 스레드가 요청을 받아. 그리고 시간이 오래 걸리는 I/O 작업(DB 조회 등)이 필요하면, 그 작업을 다른 작업 스레드(Worker Thread)에게 위임하고 자기는 바로 다음 요청을 받으러 가. 작업이 끝나면 작업 스레드가 이벤트 루프에게 "일 끝났어!" 하고 알려주고, 이벤트 루프는 그 결과를 클라이언트에게 전달해주는 거지.
덕분에 메인 스레드는 절대 기다리는 법이 없어. 항상 바쁘게 요청을 접수하고 결과를 전달하는 역할만 하니까, 적은 스레드로도 엄청난 동시성(Concurrency)을 감당할 수 있는 거야.
배압 (Backpressure): "천천히 좀 보내!"
리액티브 프로그래밍의 꽃, **배압(Backpressure)**이야. 이건 정말 중요한 개념이야.
Publisher가 데이터를 생성하는 속도가 Subscriber가 데이터를 소비하는 속도보다 훨씬 빠르면 어떻게 될까? Subscriber는 밀려드는 데이터를 감당하지 못하고 결국 메모리 부족으로 뻗어버릴 거야. 마치 컨베이어 벨트에서 물건이 쏟아져 나오는데, 포장하는 사람의 손이 느려서 물건이 바닥에 쌓이는 상황과 같아.
배압은 바로 이런 상황을 막기 위한 장치야. Subscriber가 자신이 처리할 수 있는 만큼만 Publisher에게 데이터 개수를 요청하는 거지. "나 지금 10개 처리할 수 있으니까 10개만 줘!" 라고 `Subscription`을 통해 요청하는 거야. Publisher는 정확히 10개의 데이터만 보내주고, Subscriber가 다음 요청을 할 때까지 기다려. Subscriber가 10개를 다 처리하고 나서 "이제 여유 생겼어. 10개 더 줘!"라고 하면, 그때 다시 10개를 보내주는 식이지.
이처럼 데이터의 흐름을 소비자가 제어함으로써, 시스템 전체가 안정적으로 동작할 수 있게 돼. 이게 바로 리액티브 스트림즈가 'PULL' 기반이 아닌, **'PUSH-PULL' 하이브리드 모델**이라고 불리는 이유야. Publisher가 데이터를 밀어주지만(Push), 그 속도는 Subscriber가 당기는(Pull) 힘에 의해 조절되니까.
프로젝트 리액터 (Project Reactor): WebFlux의 심장
자, 그럼 Spring WebFlux는 이 리액티브 스트림즈 명세를 어떻게 구현했을까? 바로 **프로젝트 리액터(Project Reactor)**라는 라이브러리를 통해서야. 리액터는 리액티브 스트림즈를 구현한 강력하고 풍부한 기능을 제공하는 라이브러리로, WebFlux의 핵심 의존성이야.
리액터는 Publisher를 대표하는 두 가지 핵심 타입을 제공해. 앞으로 WebFlux 코드를 짜면서 지겹도록 보게 될 녀석들이니 눈에 잘 익혀둬.
Mono<T>: 0 또는 1개의 데이터 아이템을 발행하는 Publisher. 결과가 있거나, 없거나, 또는 에러가 발생하는 경우를 표현해. 예를 들어, `findById(id)`처럼 단 건의 결과를 반환하는 API에 딱 맞아. `Mono
`는 User 객체 하나를 주거나, 결과가 없거나(empty), 에러를 발생시키는 스트림이야. Flux<T>: 0부터 N개(무한 포함)의 데이터 아이템을 발행하는 Publisher. 여러 건의 결과를 반환하는 스트림을 표현해. `findAll()`처럼 목록을 반환하는 API에 사용되지. `Flux
`는 여러 개의 Product 객체를 순차적으로 계속해서 발행하는 스트림이야.
Mono와 Flux는 수많은 연산자(Operator)를 제공해. `map`, `filter`, `flatMap`, `zip` 등등... 이 연산자들을 체이닝해서 복잡한 비동기 로직을 선언적으로, 마치 물 흐르듯 우아하게 작성할 수 있어. 이게 바로 리액티브 프로그래밍의 매력이지!
// 예시: 사용자 목록에서 이름이 "Dev"로 시작하는 사람만 찾아서 대문자로 바꾸고,
// 그 사람의 주문 내역을 비동기적으로 가져와서 합치는 코드
Flux<User> users = userRepository.findAll(); // Flux<User>
users.filter(user -> user.getName().startsWith("Dev")) // 이름 필터링
.map(user -> user.getName().toUpperCase()) // 대문자로 변환
.flatMap(name -> orderService.getOrdersByName(name)) // 비동기 주문 조회 (결과가 Flux<Order>)
.subscribe(order -> System.out.println("주문 발견: " + order)); // 최종 소비
위 코드를 봐. `for`문도, `if`문도 복잡하게 얽혀있지 않아. 데이터가 어떻게 흘러가며 변환되는지가 한눈에 보이지? 이게 바로 리액티브 스타일이야.
Spring WebFlux 본격 탐험: 어떻게 쓰는 건데?
이제 이론은 충분히 다졌으니, 실전으로 들어가 보자. Spring WebFlux는 개발자가 편하게 리액티브 웹 애플리케이션을 만들 수 있도록 두 가지 프로그래밍 모델을 제공해.
1. 어노테이션 기반 모델 (@Controller)
기존에 Spring MVC를 써봤다면 정말 반가울 방식이야. `@RestController`, `@GetMapping`, `@PostMapping` 같은 어노테이션을 그대로 사용하거든! 덕분에 MVC에서 WebFlux로 넘어오는 학습 곡선이 아주 완만해.
차이점은 딱 하나. 컨트롤러 메소드의 파라미터와 반환 타입으로 `Mono`와 `Flux`를 사용한다는 것!
백문이 불여일견, 코드를 보자.
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
// 생성자 주입
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public Mono<User> getUserById(@PathVariable String id) {
// 서비스는 Mono<User>를 반환
return userService.findById(id);
}
@GetMapping
public Flux<User> getAllUsers() {
// 서비스는 Flux<User>를 반환
return userService.findAll();
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<User> createUser(@RequestBody Mono<User> userMono) {
// 요청 본문도 Mono로 받을 수 있다!
return userService.save(userMono);
}
}
어때? Spring MVC랑 거의 똑같지? `ResponseEntity
2. 함수형 모델 (RouterFunctions & HandlerFunctions)
두 번째는 좀 더 함수형 프로그래밍 스타일에 가까운 방식이야. 라우팅(Routing, 요청 경로와 핸들러를 매핑하는 것)과 핸들링(Handling, 실제 요청을 처리하는 로직)을 코드로 명확하게 분리해서 정의해.
`HandlerFunction`: 요청을 받아서 처리하고 `Mono
`를 반환하는 함수. MVC의 컨트롤러 메소드와 같은 역할을 해. `RouterFunction`: 들어온 요청(`ServerRequest`)을 보고 어떤 `HandlerFunction`에게 전달할지 결정하는 라우터. 경로, HTTP 메소드, 헤더 등을 조합해서 라우팅 규칙을 만들어.
이 방식은 모든 설정이 자바 코드로 이루어지기 때문에, 컴파일 타임에 타입 체크가 가능하고 좀 더 유연하며 가독성이 높다는 장점이 있어. (물론 취향 차이는 존재해!)
위의 어노테이션 기반 컨트롤러를 함수형 모델로 바꿔볼까?
@Component
public class UserHandler {
private final UserService userService;
public UserHandler(UserService userService) {
this.userService = userService;
}
public Mono<ServerResponse> getUserById(ServerRequest request) {
String id = request.pathVariable("id");
Mono<User> userMono = userService.findById(id);
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(userMono, User.class);
}
public Mono<ServerResponse> getAllUsers(ServerRequest request) {
Flux<User> userFlux = userService.findAll();
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(userFlux, User.class);
}
public Mono<ServerResponse> createUser(ServerRequest request) {
Mono<User> userMono = request.bodyToMono(User.class);
Mono<User> savedUser = userService.save(userMono);
return ServerResponse.status(HttpStatus.CREATED)
.contentType(MediaType.APPLICATION_JSON)
.body(savedUser, User.class);
}
}
@Configuration
public class UserRouter {
@Bean
public RouterFunction<ServerResponse> route(UserHandler userHandler) {
return RouterFunctions
.route(GET("/functional/users/{id}").and(accept(MediaType.APPLICATION_JSON)), userHandler::getUserById)
.andRoute(GET("/functional/users").and(accept(MediaType.APPLICATION_JSON)), userHandler::getAllUsers)
.andRoute(POST("/functional/users").and(accept(MediaType.APPLICATION_JSON)), userHandler::createUser);
}
}
`RouterFunctions.route()`를 사용해서 `GET /functional/users/{id}` 요청이 오면 `userHandler`의 `getUserById` 메소드를 실행하라고 명시적으로 연결해주는 거야. 마치 작은 DSL(Domain-Specific Language)을 쓰는 것 같지? 이 방식은 특히 라우팅 규칙이 복잡하거나 동적으로 변경되어야 할 때 강력한 힘을 발휘해.
어떤 모델을 써야 할까?
정답은 없어! 둘 다 장단점이 있고, 심지어 한 프로젝트 안에서 섞어 쓰는 것도 가능해.
- **어노테이션 모델**: Spring MVC에 익숙하고, 빠르게 개발을 시작하고 싶을 때 좋아. 코드가 더 간결해 보일 수 있어.
- **함수형 모델**: 함수형 프로그래밍을 선호하고, 라우팅 로직을 완벽하게 제어하고 싶을 때 적합해. 테스트하기도 좀 더 용이할 수 있어.
팀의 성향과 프로젝트의 특성에 맞게 선택하면 돼. 중요한 건 두 가지 방식 모두 WebFlux의 리액티브 스택 위에서 동일하게 동작한다는 거야.
WebFlux의 심장, Netty
Spring MVC는 기본적으로 톰캣(Tomcat) 같은 서블릿 컨테이너 위에서 동작해. 서블릿 API 자체가 블로킹 기반으로 설계되었기 때문에, 리액티브 프로그래밍의 장점을 100% 살리기엔 한계가 있었지. (물론 서블릿 3.1 스펙부터 비동기 지원이 추가되긴 했어.)
그래서 Spring WebFlux는 기본 내장 서버로 **네티(Netty)**를 사용해. 네티는 고성능 네트워크 애플리케이션 프레임워크로, 처음부터 비동기-이벤트 기반으로 설계되었어. 논블로킹 I/O를 완벽하게 지원하기 때문에 WebFlux와는 찰떡궁합이지. 적은 수의 스레드로 수많은 클라이언트 연결을 효율적으로 처리하는 이벤트 루프 모델을 사용해서 WebFlux가 최고의 성능을 내도록 도와줘.
물론 원한다면 톰캣이나 언더토우(Undertow) 같은 서블릿 3.1+ 컨테이너에서도 WebFlux를 실행할 수 있지만, 완전한 리액티브 스택의 이점을 누리고 싶다면 네티를 사용하는 것이 정석이야.
실전! 리액티브 CRUD API 만들어보기
자, 이제 진짜 코드를 짜면서 감을 익혀보자. 간단한 상품(Product) 정보를 관리하는 CRUD API를 WebFlux로 만들어 볼 거야.
1. 프로젝트 생성
먼저 [start.spring.io](https://start.spring.io)에 접속해서 프로젝트를 만들자.
- **Project**: Gradle Project (또는 Maven)
- **Language**: Java
- **Spring Boot**: 최신 안정 버전
- **Dependencies**:
- `Spring Reactive Web` (이게 바로 WebFlux야!)
- `Lombok` (보일러플레이트 코드를 줄여주는 친구)
- `Spring Data R2DBC` (리액티브 버전의 JDBC)
- `H2 Database` (인메모리 DB, 테스트용)
여기서 **R2DBC(Reactive Relational Database Connectivity)**가 눈에 띌 거야. 기존의 JDBC는 API 자체가 블로킹 방식이라 리액티브 스트림에 그대로 쓸 수 없어. R2DBC는 관계형 데이터베이스와 논블로킹 통신을 하기 위한 새로운 표준 사양이야. 이걸 써야 DB 접근까지 완벽한 리액티브 스택을 구성할 수 있어.
2. 도메인과 레포지토리 만들기
먼저 상품을 나타내는 `Product` 엔티티와, DB와 통신할 `ProductRepository`를 만들자.
import org.springframework.data.annotation.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data // @Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor
@NoArgsConstructor
@AllArgsConstructor
public class Product {
@Id
private Integer id;
private String name;
private double price;
}
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ProductRepository extends ReactiveCrudRepository<Product, Integer> {
}
놀랍지? `CrudRepository` 대신 `ReactiveCrudRepository`를 상속받기만 하면 돼! 그럼 `findById`는 `Mono
그리고 `application.properties`에 R2DBC 설정을 추가하고, 애플리케이션 시작 시 테이블을 생성하는 스키마 파일도 만들어주자.
spring.r2dbc.url=r2dbc:h2:mem:///testdb;DB_CLOSE_DELAY=-1;
spring.r2dbc.username=sa
spring.r2dbc.password=
CREATE TABLE product (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255),
price DOUBLE
);
3. 서비스 레이어 만들기
서비스 레이어에서는 레포지토리에서 받은 `Mono`와 `Flux`를 가공해서 컨트롤러에 전달해. 지금은 간단한 CRUD라 비즈니스 로직이 거의 없지만, 실제로는 여기서 `flatMap`, `map` 등을 사용해 복잡한 로직을 조합하게 될 거야.
@Service
@RequiredArgsConstructor // final 필드에 대한 생성자를 만들어주는 Lombok 어노테이션
public class ProductService {
private final ProductRepository productRepository;
public Flux<Product> getAllProducts() {
return productRepository.findAll();
}
public Mono<Product> getProductById(Integer id) {
return productRepository.findById(id);
}
public Mono<Product> createProduct(Product product) {
return productRepository.save(product);
}
public Mono<Product> updateProduct(Integer id, Product product) {
return productRepository.findById(id)
.flatMap(existingProduct -> {
existingProduct.setName(product.getName());
existingProduct.setPrice(product.getPrice());
return productRepository.save(existingProduct);
});
}
public Mono<Void> deleteProduct(Integer id) {
return productRepository.deleteById(id);
}
}
`updateProduct` 메소드를 주목해봐. `findById`로 기존 상품을 `Mono`로 가져온 뒤, `flatMap`을 사용해서 결과를 받아. `flatMap`은 `Mono` 안의 값을 꺼내서 새로운 비동기 작업(여기서는 `save`)을 수행하고 그 결과를 다시 `Mono`로 감싸서 반환할 때 사용해. 그냥 `map`을 쓰면 `Mono
4. 컨트롤러 레이어 만들기
이제 클라이언트의 요청을 직접 받을 컨트롤러를 만들자. 우리는 친숙한 어노테이션 기반으로 만들어 볼 거야.
@RestController
@RequestMapping("/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
@GetMapping
public Flux<Product> getAllProducts() {
return productService.getAllProducts();
}
@GetMapping("/{id}")
public Mono<ResponseEntity<Product>> getProductById(@PathVariable Integer id) {
return productService.getProductById(id)
.map(product -> ResponseEntity.ok(product)) // 결과가 있으면 200 OK
.defaultIfEmpty(ResponseEntity.notFound().build()); // 결과가 없으면 404 Not Found
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<Product> createProduct(@RequestBody Product product) {
return productService.createProduct(product);
}
@PutMapping("/{id}")
public Mono<ResponseEntity<Product>> updateProduct(@PathVariable Integer id, @RequestBody Product product) {
return productService.updateProduct(id, product)
.map(updatedProduct -> ResponseEntity.ok(updatedProduct))
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public Mono<ResponseEntity<Void>> deleteProduct(@PathVariable Integer id) {
return productService.getProductById(id)
.flatMap(existingProduct ->
productService.deleteProduct(existingProduct.getId())
.then(Mono.just(ResponseEntity.noContent().build()))
)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
}
단건 조회를 하는 `getProductById`를 보면, `map`을 써서 `Product`를 `ResponseEntity
삭제 로직도 흥미로워. `deleteById`는 `Mono
5. 테스트: WebTestClient
리액티브 API는 기존의 `MockMvc`로 테스트할 수 없어. 대신 WebFlux는 논블로킹 클라이언트인 **`WebTestClient`**를 제공해. 이걸 사용하면 리액티브 엔드포인트를 쉽게 테스트할 수 있어.
@SpringBootTest
@AutoConfigureWebTestClient
class ProductControllerTest {
@Autowired
private WebTestClient webTestClient;
@Autowired
private ProductRepository productRepository;
@BeforeEach
void setUp() {
// 각 테스트 전에 DB를 깨끗하게 비우고 테스트 데이터 삽입
productRepository.deleteAll()
.thenMany(Flux.just(
new Product(null, "Test Product 1", 10.0),
new Product(null, "Test Product 2", 20.0)
))
.flatMap(productRepository::save)
.doOnNext(product -> System.out.println("Inserted: " + product))
.blockLast(); // 테스트 준비 과정은 블로킹으로 동기화
}
@Test
void getAllProductsTest() {
webTestClient.get().uri("/products")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBodyList(Product.class)
.hasSize(2);
}
@Test
void createProductTest() {
Product newProduct = new Product(null, "New Gadget", 99.99);
webTestClient.post().uri("/products")
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(newProduct), Product.class)
.exchange()
.expectStatus().isCreated()
.expectBody()
.jsonPath("$.name").isEqualTo("New Gadget")
.jsonPath("$.price").isEqualTo(99.99);
}
}
`WebTestClient`는 `exchange()` 메소드를 호출하는 시점에 실제 HTTP 요청을 보내. 그리고 `expect...()` 메소드들을 체이닝해서 응답 상태, 헤더, 본문 등을 검증할 수 있어. 정말 직관적이지?
여기까지 따라왔다면, 너는 이제 완벽하게 동작하는 리액티브 CRUD API를 하나 완성한 거야! 축하해! 생각보다 어렵지 않지? 이런 경험들이 쌓이면, 너도 리액티브 전문가가 될 수 있을 거야. 혹시 막히는 부분이 있다면 **재능넷** 같은 플랫폼에서 경험 많은 개발자 멘토에게 도움을 구하는 것도 좋은 방법이야.
WebFlux, 언제 써야 할까? (feat. 은탄환은 없다)
WebFlux가 엄청난 장점을 가진 건 사실이지만, 그렇다고 모든 상황에 맞는 만병통치약, 즉 '은탄환(Silver Bullet)'은 아니야. 언제 WebFlux를 선택하는 게 좋고, 언제 그냥 MVC를 쓰는 게 나은지 현명하게 판단할 줄 알아야 해.
👍 WebFlux를 쓰면 좋은 경우
높은 동시성 요구: 적은 리소스로 수많은 동시 접속을 처리해야 하는 서비스 (ex: 실시간 채팅, 라이브 스트리밍, 대규모 이벤트 트래픽)
I/O 집약적인 애플리케이션: DB 호출, 외부 API 호출 등 I/O 작업이 많은 마이크로서비스. 스레드가 놀지 않고 계속 일하기 때문에 효율이 극대화돼.
스트리밍 서비스: 대용량 파일 다운로드, 비디오 스트리밍, Server-Sent Events(SSE)처럼 데이터를 지속적으로 흘려보내야 하는 경우. `Flux`가 이런 시나리오에 완벽하게 부합해.
완전한 리액티브 스택: API 게이트웨이(Spring Cloud Gateway), 리액티브 DB 드라이버(R2DBC, Reactive MongoDB Driver), 리액티브 메시징(Kafka, RabbitMQ) 등 생태계 전체를 리액티브로 구성할 때.
🤔 WebFlux 사용을 고민해봐야 할 경우
CPU 집약적인 애플리케이션: 복잡한 계산, 암호화, 이미지 처리 등 CPU를 많이 쓰는 작업이 주를 이룬다면 논블로킹의 이점이 크지 않아. 오히려 스레드 풀을 늘리는 전통적인 방식이 더 나을 수 있어.
팀의 학습 곡선: 리액티브 프로그래밍은 생각의 전환이 필요해. `flatMap`과 `map`의 차이, 에러 처리, 디버깅 등 새로운 개념에 익숙해질 시간이 필요해. 팀원들이 준비되지 않았다면 도입 비용이 클 수 있어.
블로킹 라이브러리 의존성: JDBC, JPA, `RestTemplate` 등 아직 리액티브를 지원하지 않는 블로킹 라이브러리를 반드시 써야 한다면 WebFlux의 장점이 반감돼. 물론 `Scheduler`를 이용해 블로킹 코드를 별도 스레드에서 실행하는 우회 방법도 있지만, 코드가 복잡해지고 성능상 병목 지점이 될 수 있어.
간단한 CRUD 애플리케이션: 트래픽이 많지 않고 복잡한 비동기 로직이 없는 단순한 서비스라면, 굳이 WebFlux를 도입할 필요 없이 검증되고 익숙한 Spring MVC로도 충분히 빠르고 안정적으로 만들 수 있어.
결론적으로, 기술 선택은 항상 트레이드오프를 동반해. 우리 서비스의 특성과 요구사항, 팀의 역량을 종합적으로 고려해서 가장 적합한 도구를 선택하는 것이 진정한 실력 있는 개발자의 자세라고 할 수 있지.
마치며: 리액티브는 '미래'가 아닌 '현재'
오늘 우리는 Spring WebFlux와 리액티브 프로그래밍의 세계를 함께 여행했어. 블로킹 모델의 한계에서 시작해서, 리액티브의 핵심 철학, 그리고 `Mono`와 `Flux`를 이용한 실제 코드 구현까지. 꽤 긴 여정이었지만, 이 글을 끝까지 읽은 너는 이제 더 이상 '기다리는 코드'에 만족하지 못하는 개발자가 되었을 거야.
리액티브 프로그래밍은 처음에는 조금 낯설고 어렵게 느껴질 수 있어. 하지만 그 벽을 한번 넘고 나면, 비동기 로직을 훨씬 더 우아하고 직관적으로 다룰 수 있는 새로운 세상을 만나게 될 거야. 더 적은 자원으로 더 많은 트래픽을 감당하고, 사용자에게 더 빠른 응답을 제공하는 애플리케이션을 만들 수 있다는 건 개발자로서 정말 짜릿한 경험이거든.
이제 리액티브는 더 이상 소수의 전문가만 사용하는 어려운 기술이 아니야. 클라우드와 마이크로서비스가 대세가 된 지금, 이건 모든 백엔드 개발자가 알아야 할 **필수 소양**이 되어가고 있어. 그러니 두려워하지 말고, 오늘 만든 작은 CRUD 프로젝트부터 시작해서 조금씩 기능을 확장해봐. `WebClient`로 다른 리액티브 API를 호출해보기도 하고, `Server-Sent Events`로 실시간 알림 기능을 구현해보는 거야.
코드를 짜다가 막히면 좌절하지 마. 원래 다 그런 거야. 그럴 땐 잠시 쉬면서 리액티브 스트림의 데이터 흐름을 머릿속으로 그려보거나, 공식 문서를 다시 한번 읽어보는 것도 좋아. 주변에 나보다 더 잘하는 동료나 **재능넷** 같은 플랫폼의 전문가에게 질문하는 용기도 필요해.
자, 이제 너의 개발 세계에 '리액티브'라는 강력한 무기가 하나 추가됐어. 이 무기를 갈고닦아서, 더 빠르고, 더 효율적이고, 더 우아한 코드를 만드는 개발자로 성장하길 진심으로 응원할게. 화이팅!
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.