WebMVC vs WebFlux

Lofri·2024년 4월 9일

WebFlux

목록 보기
1/3

1. WebFlux

Webflux 는 기존 blocking 방식의 Servlet container 기반의 웹 어플리케이션에서
non-blocking 한 작업의 필요성으로 인해 시작된 reactive-stream 기반의 web 기술이다.

MVC 대비 높은 확장성을 가지고 있으며 보다 적은 자원을 가지고 더 많은 수의 유저를
처리할 수 있는 환경을 제공하고 있다.

어떤 이유에서 Webflux 가 보다 적은 자원으로 트래픽을 감당할 수 있을 지
천천히 알아보자.

reactive-stream? 이벤트 루프, 비 차단 실행 모델을 기반으로 보다 적은 하드웨어 리소스로 높은 동시성을 제공하는 기술

2. Spring MVC vs WebFlux

Spring MVCWebFlux
동작 방식동기적 방식에 기반한 전통적 웹 어플리케이션비동기 및 반응형 프로그래밍
구현Servlet API 기반Reactor 라이브러리 기반
처리sequentialevent-driven
I/Oblockingnon-blocking
concurrencysyncasync
처리Thread-per-requestRreactive-Stream
javaJDK8+JDK8+

위 표를 통해 Spring MVC 와 Webflux 의 차이를 확인해보자. 여기서 주의깊게 봐야할 것은
I/O 와 동시성에 대한 처리 방식의 차이이다.

우리가 Spring MVC 환경에서 일반적으로 사용하는 간단한 예제 코드를 봐보자.

2.1 일반적인 코드

@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 확인해보자.

2.2 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 에서는 비동기적인 처리에 대한 기능을 지원하지 않는다.

요약하자면 아래 문제들을 가지고 있다

  • async 처리를 위해 해야할 작업이 너무 많다.
  • 프레임 워큳 단에서 처리를 지원하지 않는다.

그렇기 때문에 Webflux 가 탄생되었다. (아래에선 Annotated Controller 를 예시로 설명을 진행하겠다)

2.3 Webflux

@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 를 사용함으로서 프레임워크 단에서 지원하는
강력한 비동기적인 처리를 진행할 수 있게 된다.


3. 공통점과 차이점

applicability

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

  1. Annotated Controllers 지원
  2. 스프링 생태계와의 호환
  3. Reactive clients (WebClient)

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

  1. Servlet API 가 아닌 reactor API 사용
  2. 다양한 실행 환경 지원 (Netty, Tomcat, Undertow)

4. 장단점

장점

  • 비동기 처리를 통한 높은 성능과 효율을 가지고 있다. (요청마다 쓰레드 하나가 무조건 적으로 점유될 필요가 없다!)
  • BackPressure 를 이용한 오버헤드 방지, 효율적 자원 사용이 가능하다.

단점

  • 디버깅이 어렵다.
  • 코드가 복잡해질 가능성이 있다.
  • blocking 하게 작성할 경우 오히려 성능이 나빠질 가능성이 있다
  • 러닝커브가 높다.
  • JPA 사용을 비권장한다.

고려해할 점

  1. 러닝커브가 높다
  2. 지원하지 않는 라이브러리가 꽤 있다.
  3. 단순히 WebFlux 를 사용한다 해서 성능이 좋아지지는 않는다.
  4. 동기 작업 수행은 적은 자원을 사용하는 MVC 보다 성능에 더 큰 악영향을 끼칠 수 있다.
profile
Java BE

0개의 댓글