Webflux 는 기존 blocking 방식의 Servlet container 기반의 웹 어플리케이션에서
non-blocking 한 작업의 필요성으로 인해 시작된 reactive-stream 기반의 web 기술이다.
MVC 대비 높은 확장성을 가지고 있으며 보다 적은 자원을 가지고 더 많은 수의 유저를
처리할 수 있는 환경을 제공하고 있다.
어떤 이유에서 Webflux 가 보다 적은 자원으로 트래픽을 감당할 수 있을 지
천천히 알아보자.
| Spring MVC | WebFlux | |
|---|---|---|
| 동작 방식 | 동기적 방식에 기반한 전통적 웹 어플리케이션 | 비동기 및 반응형 프로그래밍 |
| 구현 | Servlet API 기반 | Reactor 라이브러리 기반 |
| 처리 | sequential | event-driven |
| I/O | blocking | non-blocking |
| concurrency | sync | async |
| 처리 | Thread-per-request | Rreactive-Stream |
| java | JDK8+ | JDK8+ |
위 표를 통해 Spring MVC 와 Webflux 의 차이를 확인해보자. 여기서 주의깊게 봐야할 것은
I/O 와 동시성에 대한 처리 방식의 차이이다.
우리가 Spring MVC 환경에서 일반적으로 사용하는 간단한 예제 코드를 봐보자.
@GetMapping("/api/user/{id}")
public User getUserDetails(Long id) {
User user = userService.getUser(id);
UserPreference pref = userPreferenceService.getUserPreference(id);
user.serPreference(pref);
return user;
}
위 코드에서 어떤 문제점이 있어 보이는가?
유저 id 를 통해 유저 상세 정보를 받아오는 코드로서
별 다른 로직이 존재하지 않고, 코드를 보면서 그 기능을 이해하기도 쉬워 문제가 없어 보인다.
하지만 이 코드에는 오버헤드가 발생하고 있다.
getUserPreference 는 getUser 가 종료된 이후 실행된다.
성능상에 큰 문제가 없어 보이지만 각각 서비스의 실행 시간이 극단적으로 길다 가정해본다면 확실하게 체감할 수 있다.
만약 getUser 가 400 ms, getUserPreference 가 300 ms 의 실행 시간을 가진다면,
총 실행 시간은 700+ ms 이다.
sync / blocking 방식의 동작이기 때문에 자원이 불필요하게 wait 되는 시간이 매우 길다.
그렇다면 이를 비동기적으로 처리한다면 괜찮을까?
자바에서 기본적으로 제공하는 CompletableFuture 확인해보자.
@GetMapping("/api/user/{id}")
public User getUserDetails(Long id) {
CompletableFuture<User> userAsync =
CompletableFuture.supplyAsync(() -> userService.getUser(id));
CompletableFuture<UserPreference> prefAsync =
CompletableFuture.supplyAsync(() -> userPreferenceService.getUserPreference(id));
CompletableFuture<Void> bothFeatures = CompletableFuture.allOf(userAsync, prefAsync)
bothfeatures.join();
User user = userAsync.join();
UserPreferences pref = prefAsync.join();
user.setPreference(pref);
return user;
}
CompletableFuture 를 통해 getUser 와 getUserPreference 의 호출을 async 하게 변경하였지만
여기에서도 문제는 있다.
일단 코드가 너무 복잡하다. 4줄로 끝나던 단순한 기능이 8줄까지 늘어났다.
또한 호출 종료 이후, 유저 객체 처리 위해 blocking 이 발생하고 처리 이후에야 어플리케이션에 이 값을 반환할 수 있다.
우리가 만약 User 에 대한 CompletableFuture 를 반환할 수 있다면 이를 해결할 수 있겠지만
Spring MVC 에서는 비동기적인 처리에 대한 기능을 지원하지 않는다.
요약하자면 아래 문제들을 가지고 있다
그렇기 때문에 Webflux 가 탄생되었다. (아래에선 Annotated Controller 를 예시로 설명을 진행하겠다)
@GetMapping("/api/user/{id}")
public Mono<User> getUserDetails(Long id) {
return serService.getUser(id)
.zipWith(userPreferenceService.getPreference(id))
.map(t -> {
User user = t.getT1();
user.setPreference(t.getT2();
return user;
});
}
reactor 를 통해 제공되는 다양한 operator 를 통해 수많은 처리를 진행할 수 있고,
CompletableFuture 보다 비교적 편한 방법으로 처리가 가능하다.
그리고 가장 중요하게, 프레임워크 단에서 처리를 지원한다!
아래 그림을 통해 간략하게 요약하자면

MVC 와 달리 프레임워크 단에서 지원하는 이벤트 루프 방식을 통해 요청당 쓰레드가 최소 하나가 무조건적으로 소모될 필요가 없으며!
그렇기 때문에 우리는 Webflux 를 사용함으로서 프레임워크 단에서 지원하는
강력한 비동기적인 처리를 진행할 수 있게 된다.

다음과 같은 공통점을 가지고 있다.
다음과 같은 차이점을 가지고 있다