76일차 Spring WebFlux란?

2022. 8. 12. 11:12카테고리 없음

Spring WebFlux란?

 

Spring WebFlux란?

Spring 5 버전에 새롭게 추가된 기술 스택은 바로 리액티브(Reactive) 스택입니다.

그리고 리액티브(Reactive) 스택이 Spring 5에 추가되면서 항상 언급되는 기술이 바로 WebFlux입니다.


WebFlux란 무엇일까요?

여러분들이 만약 Reactor를 학습하지 않았다면 WebFlux라는 용어가 무척이나 생소할 거라 생각합니다.

하지만 여러분들이 앞에서 이미 Reactor를 학습했기 때문에 WebFlux라는 용어는 어디서 많이 들어본 듯한 용어인 것 같다는 생각이 들 수도 있겠네요. ^^


Reactor는 기본적으로 두 가지 타입을 지원한다고 했습니다.

첫 번째 타입은 0개 또는 1개의 데이터만 emit하는 Mono이고, 두 번째 타입은 N(0개 이상)개의 데이터를 emit하는 Flux라고 설명했던 것 기억날까요?


Reactor의 타입 중 하나인 Flux가 WebFlux라는 용어에 사용되었습니다. ^^


쉽게 말하면 WebFlux라는 용어는 Reactor의 타입인 Flux가 Web에서 사용된다라고 말할 수 있습니다.

그런데 조금 더 넓게 생각해 보면 WebFlux는 ****리액티브한 웹 애플리케이션을 구현하기 위한 기술 자체를 상징하고 있다고 보는게 더 적절할 것 같습니다.


Spring 공식 사이트에서는 Spring WebFlux에 대해서 다음과 같이 설명하고 있습니다.

The reactive-stack web framework, Spring WebFlux, was added later in version 5.0.


Spring WebFlux는 Spring 5부터 지원하는 리액티브 웹 애플리케이션을 위한 웹 프레임워크입니다.

여러분들이 Spring MVC 프레임워크를 사용해서 웹 애플리케이션을 구현할 수 있듯이 Spring WebFlux 프레임워크를 사용해서 리액티브한 웹 애플리케이션을 구현할 수 있습니다.


중요한 것은 그냥 웹 애플리케이션이 아니라 리액티브한 웹 애플리케이션이라는 것입니다. ^^


WebFlux라는 용어만 따지고 봐도 **Spring WebFlux**가 Reactor와 얼마나 밀접한 연관이 있는지 어렴풋이 짐작할 수 있을 거라고 생각합니다.

하지만 Spring WebFlux에서 꼭 Reactor만 사용할 수 있는 것은 아닙니다. WebFlux가 리액티브 스트림즈(Reactive Streams)의 사양인 인터페이스를 기반으로 동작하기 때문에 리액티브 스트림즈를 구현한 구현체라면 대부분 Reactor 대신 사용할 수 있습니다.

Reactor 이외의 리액티브 라이브러리는 ReactiveAdapter와 ReactiveAdapterRegistry를 통해 사용 가능합니다.

ReactiveAdapter와 ReactiveAdapterRegistry에 대해서 더 알아보고 싶다면 아래 [심화 학습]을 참고하세요.


그렇다면 리액티브한 웹 애플리케이션이란 도대체 무엇을 의미하는 걸까요?

우리가 이 전 유닛에서 리액티브 프로그래밍(Reactive Programming)에 대한 개념적인 내용만 대략적으로 살펴보았기 때문에 리액티브(Reactive)라는 개념이 애플리케이션에 어떻게 적용되는지 어떤 특성을 가지는지 등 여러분의 궁금증이 해결되지 않았을 거라 생각합니다.


이번 유닛에서 여러분의 궁금증이 완벽하지는 않겠지만 일정 부분 해소가 될거라 생각합니다. ^^


Spring WebFlux를 이해하기 위해서 가장 좋은 방법 중 한가지는 바로 우리가 지금껏 학습했던 Spring MVC를 Spring WebFlux와 비교해 보면서 그 차이점을 이해하는 것입니다.



Spring WebFlux 애플리케이션 vs Spring MVC 애플리케이션

✅ 기술 스택 비교

Spring WebFlux의 기술적인 특징을 이해하기 위해서는 Spring MVC의 기술 스택과 비교해 보는 것이 가장 좋습니다.

[그림 4-15] Spring WebFlux와 Spring MVC 기술 스택 비교.

참고 자료 : http://spring.io/reactive


[그림 4-15]는 Spring WebFlux와 Spring MVC의 기술 스택을 비교한 자료입니다.


[그림 4-15]에서 Reactive Stack이 Spring WebFlux에 해당 되고, Servlet Stack이 Spring MVC에 해당된다고 이해하면 되겠습니다.


Spring MVC와 비교해서 Spring WebFlux의 기술 스택이 어떻게 다른지 하나씩 살펴보겠습니다.

  • (1) Spring WebFlux의 경우 Non-Blocking 통신을 지원하지만 Spring MVC의 경우 Non-Blocking이 아닌 Blocking 통신 방식을 사용합니다.
  • Blocking, Non-Blocking에 대해서는 뒤에서 샘플 코드를 통해 다시 설명하겠습니다.
  • (2) Spring WebFlux의 경우 Reactive Adapter를 사용해서 Reactor 뿐만 아니라 RxJava 등의 다른 리액티브 라이브러리를 사용할 수 있는 유연함을 제공하는 반면, Spring MVC의 경우 Servlet API의 스펙에 의존적입니다.
  • (3) Spring WebFlux와 Spring MVC 모두 보안을 적용하기 위해서 Spring Security를 사용합니다. 다만, Spring WebFlux의 경우 서블릿 필터 방식이 아닌 WebFilter를 사용해 리액티브 특성에 맞게 인증과 권한 등의 보안을 적용합니다.
  • (4)Reactive Stack의 경우, 웹 계층(프리젠테이션 계층, API 계층)에서는 Spring WebFlux를 사용하며 Servlet Stack의 경우, Spring MVC를 사용합니다.
  • (5) Spring WebFlux의 경우 완전한 Non-Blocking 통신을 위해 리액티브 스택을 데이터 액세스 계층까지 확장합니다. R2DBC(Reactive Relation Database Connectivity)는 관계형 데이터베이스에 Non-Blocking 통신을 적용하기 위한 표준 사양(Specification)이며, MySQL, Oracle 등의 데이터베이스 벤더에서는 R2DBC 사양에 맞는 드라이버를 구현해서 공급합니다.

우리가 Spring MVC에서 사용하는 JDBC API의 경우 Non-Blocking 통신을 지원하지 않습니다.



✅ 벤 다이어그램을 통한 비교

Spring MVC와 Spring WebFlux 내부적으로 전혀 다른 기술 스택을 사용한다는 사실을 [그림 4-15]를 통해 확인할 수 있었습니다.


그런데 여러분이 Spring Data JDBC와 Spring Data JPA에 학습에서 데이터베이스와 인터랙션 하기 위해 Repository를 사용했던 기억을 떠올려 보세요.

기술 자체는 달라도 Spring의 추상화 기법을 통해 Repository를 사용하는 입장에서는 거의 동일한 방식으로 데이터베이스에 접근할 수 있었던 것 기억 날까요? ^^


Spring MVC와 Spring WebFlux 역시 전혀 다른 기술임에도 불구하고, 두 기술을 사용하는 입장에서는 최소한의 변경 포인트만 유지하면서 일관된 접근 방식으로 두 기술을 사용할 수 있도록 구성해 두었습니다.

[그림 4-16] Spring WebFlux와 Spring MVC 기술 스택 비교.

참고 자료 : https://docs.spring.io/spring-framework/docs/5.2.5.RELEASE/spring-framework-reference/web-reactive.html#webflux-framework-choice


[그림 4-16]은 벤 다이어그램을 통해 Spring MVC와 Spring WebFlux의 공통점과 차이점을 확인할 수 있는 그림입니다.


여러분들이 익숙하게 사용하고 있는 @Controleller(또는 @RestController) 같은 애너테이션을 Spring WebFlux에서 사용할 수 있으며, RestTemplate을 대체할 수 있는 WebClient를 Spring MVC와 Spring WebFlux에서 모두 사용할 수 있습니다.

Tomcat, Jetty 등의 서블릿 컨테이너는 서블릿 방식과 리액티브 방식의 통신을 모두 지원하고 있습니다.


Spring MVC의 API 엔드포인트인 Controller 클래스에 사용하던 애너테이션을 Spring WebFlux에서 동일하게 사용하는 샘플 코드는 잠시 뒤에 확인해 보도록 하겠습니다.



✅ 샘플 코드로 보는 Spring MVC와 Spring WebFlux

앞에서 Spring MVC와 Spring WebFlux를 비교해 보았는데, 현실적으로 둘 사이의 차이점과 공통점이 와 닿지 않을 가능성이 높지 않을거라 생각합니다.


이럴 땐 샘플 코드를 만들어서 코드의 구조를 비교해 보고, 샘플 코드를 실행해 보는게 가장 좋은 방법입니다. ^^

[그림 4-17] Spring MVC와 Spring WebFlux를 비교하기 위한 샘플 애플리케이션 요청 흐름

그림 4-17은 Spring MVC와 Spring WebFlux의 요청 처리 방식을 비교하기 위한 샘플 애플리케이션의 요청 처리 흐름을 표현한 그림입니다.


우리가 Spring MVC 학습을 위해 만들었던 커피 주문 샘플 애플리케이션은 단일 서버에서 실행되는 서버인 반면 그림 4-17에서는 커피 정보를 요청할 경우 하나의 서버에서 해당 요청을 처리하는 것이 아니라 클라이언트의 요청을 외부 서버에 한번 더 전달해서 외부 서버로부터 커피 정보를 전달 받은 후 최종적으로 클라이언트에게 커피 정보를 응답으로 전송하고 있습니다.


Spring MVC 샘플 애플리케이션과 Spring WebFlux 샘플 애플리케이션 모두 그림 4-17과 같은 구조로 구성됩니다.


애플리케이션이 두 개이므로 애플리케이션 구현 코드 역시 IntelliJ IDE 상에서 각각의 프로젝트에서 구현된다는 사실을 기억하기 바랍니다.


그럼 먼저 Spring MVC 기반의 애플리케이션 구현 코드를 간단하게 살펴보고 해당 애플리케이션을 실행한 후, 요청 처리 결과를 확인해 보겠습니다.


실무에서 그림 4-17과 같이 외부 API와의 통신을 통해 작업을 처리하는 방식은 흔하게 볼 수 있는 방식입니다.

꼭 하나의 서버에서 모든 요청을 처리하는 것은 아님을 기억하세요.

✔ Spring MVC의 Blocking 처리 방식

Spring MVC 기반 메인 애플리케이션의 Controller(SpringMvcMainCoffeeController) 샘플 코드

 

package com.codestates.coffee;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.time.LocalDateTime;

@Slf4j
@RestController
@RequestMapping("/v11/coffees")
public class SpringMvcMainCoffeeController {
    private final RestTemplate restTemplate;

    String uri = "http://localhost:7070/v11/coffees/1";

    public SpringMvcMainCoffeeController(RestTemplateBuilder restTemplateBuilder) {
        this.restTemplate = restTemplateBuilder.build();
    }

    @GetMapping("/{coffee-id}")
    public ResponseEntity getCoffee(@PathVariable("coffee-id") long coffeeId) {
        log.info("# call Spring MVC Main Controller: {}", LocalDateTime.now());
        
        // (1)  
        ResponseEntity<CoffeeResponseDto> response = restTemplate.getForEntity(uri, CoffeeResponseDto.class);
        return ResponseEntity.ok(response.getBody());
    }
}
[코드 4-29] Spring MVC 기반의 메인 애플리케이션 Controller

코드 4-29는 클라이언트의 커피 정보 요청을 전달 받는 메인 애플리케이션의 Controller 클래스의 예입니다.

 

Spring MVC 학습에서는 CoffeeController에 HTTP Method별로 요청 핸들러 메서드가 존재했지만 Spring MVC와 Spring WebFlux의 요청 처리 방식을 비교하기 위한 목적이기때문에 다른 요청 핸들러가 없습니다.

그리고 별도의 데이터베이스와의 연동도 없으니 이 점을 염두에 두고 코드를 확인하기 바랍니다.

 

(1)에 알 수 있다시피 클라이언트의 요청을 메인 애플리케이션에서 직접 처리하는 것이 아니라 Spring의 Rest Client인 RestTemplate을 이용해서 외부에 있는 다른 애플리케이션에게 한번 더 요청을 전송하는 것을 확인할 수 있습니다.

 

애플리케이션을 실제로 실행시키면 메인 애플리케이션은 8080 포트에서 실행되며, 외부 애플리케이션은 7070 포트에서 실행됩니다.

 

그리고 클라이언트 쪽에서 getCoffee() 핸들러 메서드로 요청을 전송하면 "# call Spring MVC Main Controller: {요청 수신 시간}"와 같이 로그가 출력될 것입니다.

 

Spring MVC 기반 외부 애플리케이션의 Controller(SpringMvcOutboundCoffeeController) 샘플 코드

 

package com.codestates.coffee;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/v11/coffees")
public class SpringMvcOutboundCoffeeController {
    @GetMapping("/{coffee-id}")
    public ResponseEntity getCoffee(@PathVariable("coffee-id") long coffeeId) throws InterruptedException {
        CoffeeResponseDto responseDto = new CoffeeResponseDto(coffeeId, "카페라떼", "CafeLattee", 4000);

        // (1)
        Thread.sleep(5000);
        return ResponseEntity.ok(responseDto);
    }
}
[코드 4-30] Spring MVC 기반의 외부 애플리케이션 Controller

코드 4-30은 코드 4-29의 메인 애플리케이션 Controller(SpringMvcMainCoffeeController) 의 getCoffee() 핸들러 메서드에서 RestTemplate으로 전송한 요청을 전달 받는 외부 애플리케이션의 Controller(SpringMvcOutboundCoffeeController) 코드입니다.

 

Spring MVC 애플리케이션의 요청 처리 방식을 확인하는 것이 주목적이기 때문에 별도의 데이터베이스 연동 없이 단순히 Stub 데이터(responseDto)를 응답으로 넘겨주고 있습니다.

 

그런데, 주목해야 될 부분은 외부 애플리케이션의 요청 처리 시간이 오래 걸리는 것을 시뮬레이션 하기 위해서 (1)과 같이 요청 처리 쓰레드에 지연 시간을 주었다는 것입니다.

 

이렇게 Thread.sleep(5000)을 설정하면 하나의 요청이 들어오면 5초간 쓰레드의 동작이 멈춥니다.



Spring MVC 기반 메인 애플리케이션을 호출하는 클라이언트 샘플 코드

package com.codestates;

import com.codestates.coffee.CoffeeResponseDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

import java.time.LocalTime;

@Slf4j
@SpringBootApplication
public class SpringMvcMainSampleApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringMvcMainSampleApplication.class, args);
	}

	@Bean
	public CommandLineRunner run() {
		return (String... args) -> {
			log.info("# 요청 시작 시간: {}", LocalTime.now());

      // (1)
			for (int i = 1; i <= 5; i++) {
				CoffeeResponseDto response = this.getCoffee();
				log.info("{}: coffee name: {}", LocalTime.now(), response.getKorName());
			}
		};
	}

	private CoffeeResponseDto getCoffee() {
		RestTemplate restTemplate = new RestTemplate();
		String uri = "http://localhost:8080/v11/coffees/1";
		ResponseEntity<CoffeeResponseDto> response = restTemplate.getForEntity(uri, CoffeeResponseDto.class);

		return response.getBody();
	}
}
[코드 4-31] Spring MVC 기반의 메인 애플리케이션 Controller에 요청을 전송하는 코드

코드 4-31은 메인 애플리케이션 Controller(SpringMvcMainCoffeeController)의 getCoffee() 핸들러 메서드를 호출하는 클라이언트를 구현한 코드입니다.

 

(1)을 보면, for 문을 이용해서 SpringMvcMainCoffeeController 의 getCoffee() 핸들러 메서드(http://localhost:8080/v11/coffees/1)를 다섯번 호출하는 것을 알 수 있습니다.

 

이 경우 실행 결과가 어떻게 출력되는지 확인해 보겠습니다.

 

⭐ 애플리케이션을 실행할 때, 메인 애플리케이션이 호출하는 대상인 외부 애플리케이션을 먼저 실행 시킨 후에 메인 애플리케이션을 실행해야 한다는 것 기억하세요!

 

Spring에서는 CommandLineRunner나 ApplicationRunner 를 이용하면 애플리케이션이 실행되는 시점에 어떤 처리작업을 할 수 있습니다.

코드 4-31에서는 CommandLineRunner 내부의 코드가 메인 애플리케이션 SpringMvcMainCoffeeController의 getCoffee() 핸들러 메서드에 요청을 전송하는 클라이언트의 역할을 합니다.

즉, 코드 4-31의 코드가 그림 4-17에서 프런트엔드의 역할을 한다고 생각하면 됩니다. ^^

 

Spring MVC 요청 처리 방식 확인을 위한 실행 결과,

2022-07-27 14:17:16.707  INFO 17484 --- [           main] c.c.SpringMvcMainSampleApplication       : # 요청 시작 시간: 14:17:16.707955700

2022-07-27 14:17:16.833  INFO 17484 --- [nio-8080-exec-1] c.c.c.SpringMvcMainCoffeeController      : # call Spring MVC Main Controller: 2022-07-27T14:17:16.833451500  // (1-1) 첫 번째 요청
2022-07-27 14:17:21.984  INFO 17484 --- [           main] c.c.SpringMvcMainSampleApplication       : 14:17:21.984873800: coffee name: 카페라떼 // (1-2) 첫 번째 요청 처리 결과

2022-07-27 14:17:21.990  INFO 17484 --- [nio-8080-exec-2] c.c.c.SpringMvcMainCoffeeController      : # call Spring MVC Main Controller: 2022-07-27T14:17:21.990018700 // (2-1) 두 번째 요청
2022-07-27 14:17:26.999  INFO 17484 --- [           main] c.c.SpringMvcMainSampleApplication       : 14:17:26.999949600: coffee name: 카페라떼 // (2-2) 두 번째 요청 처리 결과

2022-07-27 14:17:27.003  INFO 17484 --- [nio-8080-exec-3] c.c.c.SpringMvcMainCoffeeController      : # call Spring MVC Main Controller: 2022-07-27T14:17:27.003373200
2022-07-27 14:17:32.010  INFO 17484 --- [           main] c.c.SpringMvcMainSampleApplication       : 14:17:32.010208600: coffee name: 카페라떼

2022-07-27 14:17:32.014  INFO 17484 --- [nio-8080-exec-4] c.c.c.SpringMvcMainCoffeeController      : # call Spring MVC Main Controller: 2022-07-27T14:17:32.014291900
2022-07-27 14:17:37.021  INFO 17484 --- [           main] c.c.SpringMvcMainSampleApplication       : 14:17:37.021770: coffee name: 카페라떼

2022-07-27 14:17:37.026  INFO 17484 --- [nio-8080-exec-5] c.c.c.SpringMvcMainCoffeeController      : # call Spring MVC Main Controller: 2022-07-27T14:17:37.026077900
2022-07-27 14:17:42.033  INFO 17484 --- [           main] c.c.SpringMvcMainSampleApplication       : 14:17:42.033187200: coffee name: 카페라떼
[코드 4-32] Spring MVC 기반의 애플리케이션 요청 처리 결과

코드 4-32는 메인 애플리케이션인 SpringMvcMainCoffeeController의 getCoffee() 핸들러 메서드에 요청을 전송한 결과가 로그로 출력된 것입니다.

 

로그를 분석해 봅시다.

 

(1-1)에서 첫 번째 요청이 SpringMvcMainCoffeeController의 getCoffee() 핸들러 메서드에 전달된 시간과 (1-2)에서 첫 번째 요청 처리 결과로 출력된 시간을 보면 5초 정도의 시간이 걸린 것을 확인할 수 있습니다.

 

나머지 요청 처리 시간 역시 5초 정도 걸렸습니다. 이처럼 각 요청마다 5초의 시간이 걸린 이유는 외부 애플리케이션 역할을 하는 SpringMvcOutboundCoffeeController 의 getCoffee() 핸들러 메서드에서 Thread.sleep(5000)를 설정해서 요청 처리 쓰레드에 5초의 지연 시간을 주었기 때문입니다.

 

따라서 5번의 요청에 걸린 시간은 총 25초 정도가 됩니다.

 

⭐ 이 결과를 통해 Spring MVC 기반의 메인 애플리케이션이 외부 애플리케이션 서버와 통신할 때 요청 처리 쓰레드가 Blocking 된다는 것을 알 수 있습니다.

 

Spring MVC 기반 애플리케이션이 Blocking 방식으로 외부 서버와 통신한다는 사실을 확인했으니 이제 Spring WebFlux 기반 애플리케이션의 요청 처리 방식을 확인해 볼까요?



✔ Spring WebFlux의 Non-Blocking 처리 방식

Spring WebFlux 기반 메인 애플리케이션의 Controller(SpringWebFluxMainCoffeeController) 샘플 코드

package com.codestates.coffee;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.time.LocalDateTime;

@Slf4j
@RestController
@RequestMapping("/v11/coffees")
public class SpringWebFluxMainCoffeeController {
    String uri = "http://localhost:5050/v11/coffees/1";

    @ResponseStatus(HttpStatus.OK)
    @GetMapping("/{coffee-id}")
    public Mono<CoffeeResponseDto> getCoffee(@PathVariable("coffee-id") long coffeeId) throws InterruptedException {
        log.info("# call Spring WebFlux Main Controller: {}", LocalDateTime.now());

        // (1)
        return WebClient.create()
                .get()
                .uri(uri)
                .retrieve()
                .bodyToMono(CoffeeResponseDto.class);
    }
}
[코드 4-33] Spring WebFlux 기반의 메인 애플리케이션 Controller

코드 4-33은 Spring WebFlux 기반의 메인 애플리케이션 Controller입니다.

Spring WebFlux 기반의 메인 애플리케이션 역시 클라이언트의 요청을 처리하기 위해 (1)과 같이 외부 애플리케이션에 한번 더 요청을 전송하는 것을 볼 수 있습니다.

 

Spring MVC 기반의 애플리케이션 Controller 코드와 도대체 뭐가 달라진거지? 라고 생각할 수 있을만큼 코드 구조가 거의 동일합니다.

 

그런데 코드를 잘 보면 코드 상에 두가지 차이점이 존재합니다.

첫 번째는 Spring MVC 기반 애플리케이션에서는 외부 애플리케이션과의 통신을 위해 RestTemplate 사용한 반면 Spring WebFlux 기반 애플리케이션에서는 (1)과 같이 WebClient라는 Rest Client를 사용합니다.

RestTemplate이 Blocking 방식의 Rest Client인 반면에 WebClient는 Non-Blocking 방식의 Rest Client라는 사실을 참고하기 바랍니다.

 

이번 유닛의 주 목적은 WebClient의 구체적인 사용법을 익히는 것은 아닙니다.

따라서 WebClient의 사용법은 이번 유닛에서 다루지 않습니다.

WebClient의 사용법이 궁금하다면 아래의 [심화 학습]을 참고하세요.

 

두 번째 차이점은 getCoffee() 핸들러 메서드의 리턴 타입이 ResponseEntity<CoffeeResponseDto> 아니라 Mono<CoffeeResponseDto>라는 것입니다.

 

Mono는 어디서 본적이 있죠? ^^

바로 Reactor의 데이터 타입 중 하나인 그 Mono입니다. ^^

 

Spring WebFlux에서도 ResponseEntity가 사용되기는 합니다.

다만, ResponseEntity<Mono<CoffeeResponseDto>> 와 같은 형태로 사용됩니다.



Spring WebFlux 기반 외부 애플리케이션의 Controller(SpringWebFluxOutboundSampleApplication) 샘플 코드

package com.codestates.coffee;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/v11/coffees")
public class SpringWebFluxOutboundCoffeeController {
    @ResponseStatus(HttpStatus.OK)
    @GetMapping("/{coffee-id}")
    public Mono<CoffeeResponseDto> getCoffee(@PathVariable("coffee-id") long coffeeId) throws InterruptedException {
        CoffeeResponseDto responseDto = new CoffeeResponseDto(coffeeId, "카페라떼", "CafeLattee", 4000);

        // (1)
        Thread.sleep(5000);
        return Mono.just(responseDto);   // (2)
    }
}
[코드 4-34] Spring WebFlux 기반의 외부 애플리케이션 Controller

코드 4-34는

코드 4-33의 메인 애플리케이션 Controller(SpringWebFluxMainCoffeeController)의 getCoffee() 핸들러 메서드에서 WebClient로 전송한 요청을 전달 받는 외부 애플리케이션의 Controller(SpringWebFluxOutboundCoffeeController) 코드입니다.

 

코드 4-34에서도 역시 외부 애플리케이션의 요청 처리 시간이 오래 걸리는 것을 시뮬레이션 하기 위해서 (1)과 같이 요청 처리 쓰레드에 지연 시간을 주었습니다.

 

Reactor 유닛에서 학습했던 익숙한 코드가 (2)에서 보이는군요. ^^

이번 샘플 코드에서 보이는 Reactor 관련된 코드는 이어지는 챕터에서 설명을 합니다.

지금은 Spring WebFlux 애플리케이션의 요청 처리 방식에 집중해주세요.



Spring WebFlux 기반 메인 애플리케이션을 호출하는 클라이언트 샘플 코드

package com.codestates;

import com.codestates.coffee.CoffeeResponseDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.time.LocalTime;

@Slf4j
@SpringBootApplication
public class SpringWebFluxMainSampleApplication {

	public static void main(String[] args) {
		System.setProperty("reactor.netty.ioWorkerCount", "1");
		SpringApplication.run(SpringWebFluxMainSampleApplication.class, args);
	}

	@Bean
	public CommandLineRunner run() {
		return (String... args) -> {
			log.info("# 요청 시작 시간: {}", LocalTime.now());

      // (1)
			for (int i = 1; i <= 5; i++) {
				this.getCoffee()
						.subscribe(
								response -> {
									log.info("{}: coffee name: {}", LocalTime.now(), response.getKorName());
								}
						);
			}
		};
	}

	private Mono<CoffeeResponseDto> getCoffee() {
		String uri = "http://localhost:6060/v11/coffees/1";

		return WebClient.create()
				.get()
				.uri(uri)
				.retrieve()
				.bodyToMono(CoffeeResponseDto.class);
	}
}
[코드 4-35] Spring WebFlux 기반의 메인 애플리케이션 Controller에 요청을 전송하는 클라이언트 코드

코드 4-35는 Spring WebFlux 기반의 메인 애플리케이션 Controller(SpringWebFluxMainCoffeeController)의 getCoffee() 핸들러 메서드를 호출하는 클라이언트를 구현한 코드입니다.

 

여기서도 역시 (1)과 같이 for 문을 이용해서 SpringWebFluxMainCoffeeController 의 getCoffee() 핸들러 메서드(http://localhost:6060/v11/coffees/1)를 다섯번 호출하는 것을 알 수 있습니다.

 

이 번에는 Spring MVC때와 실행 결과가 어떻게 다른지 확인해 봅시다.



Spring WebFlux 요청 처리 방식 확인을 위한 실행 결과

2022-07-27 15:09:20.022  INFO 20160 --- [           main] c.c.SpringWebFluxMainSampleApplication   : # 요청 시작 시간: 15:09:20.021338900
2022-07-27 15:09:20.960  INFO 20160 --- [ctor-http-nio-1] c.c.c.SpringWebFluxMainCoffeeController  : # call Spring WebFlux Main Controller: 2022-07-27T15:09:20.960358400 // (1) 첫 번째 요청 수신
2022-07-27 15:09:20.988  INFO 20160 --- [ctor-http-nio-1] c.c.c.SpringWebFluxMainCoffeeController  : # call Spring WebFlux Main Controller: 2022-07-27T15:09:20.988741200 // (2) 두 번째 요청 수신
2022-07-27 15:09:20.990  INFO 20160 --- [ctor-http-nio-1] c.c.c.SpringWebFluxMainCoffeeController  : # call Spring WebFlux Main Controller: 2022-07-27T15:09:20.990737800 // (3) 세 번째 요청 수신
2022-07-27 15:09:20.992  INFO 20160 --- [ctor-http-nio-1] c.c.c.SpringWebFluxMainCoffeeController  : # call Spring WebFlux Main Controller: 2022-07-27T15:09:20.992845800 // (4) 네 번째 요청 수신
2022-07-27 15:09:20.994  INFO 20160 --- [ctor-http-nio-1] c.c.c.SpringWebFluxMainCoffeeController  : # call Spring WebFlux Main Controller: 2022-07-27T15:09:20.994847    // (5) 다섯 번째 요청 수신
2022-07-27 15:09:26.215  INFO 20160 --- [ctor-http-nio-1] c.c.SpringWebFluxMainSampleApplication   : 15:09:26.215659900: coffee name: 카페라떼
2022-07-27 15:09:26.219  INFO 20160 --- [ctor-http-nio-1] c.c.SpringWebFluxMainSampleApplication   : 15:09:26.219924100: coffee name: 카페라떼
2022-07-27 15:09:26.220  INFO 20160 --- [ctor-http-nio-1] c.c.SpringWebFluxMainSampleApplication   : 15:09:26.220840500: coffee name: 카페라떼
2022-07-27 15:09:26.220  INFO 20160 --- [ctor-http-nio-1] c.c.SpringWebFluxMainSampleApplication   : 15:09:26.220840500: coffee name: 카페라떼
2022-07-27 15:09:26.220  INFO 20160 --- [ctor-http-nio-1] c.c.SpringWebFluxMainSampleApplication   : 15:09:26.220840500: coffee name: 카페라떼
[코드 4-36] Spring WebFlux 기반의 애플리케이션 요청 처리 결과

우리가 앞에서 확인한 Spring MVC 기반에서 다섯 번 요청을 전송했을 때, 전체 요청 처리 시간은 총 25초 정도 걸렸습니다.

 

그런데 코드 4-37을 보면 요청 시작 시간과 요청 결과로 전달 받은 커피 정보를 수신한 시간을 확인해보면 전체 처리 시간이 6 초 정도입니다.

 

왜 이런 결과를 보이는 걸까요?

코드 4-36의 결과를 이해하기 위해서는 (1)부터 (5)까지 메인 애플리케이션의 Controller가 요청을 1차로 수신한 시간을 보면 어느 정도 궁금증이 풀립니다.

 

(1)부터 (5)까지의 요청 수신 시간이 밀리초 단위는 조금 다르지만 초 단위는 동일한것을 확인할 수 있습니다.

 

즉, 이 결과의 의미는 메인 애플리케이션의 Controller에서 외부 애플리케이션의 Controller에 요청을 전송할 때, 외부 애플리케이션의 Controller에서 Thread.sleep(5000)으로 지연 시간을 주었다고 해서 메인 애플리케이션 Controller의 요청 처리 쓰레드가 Blocking 되지 않는 다는 것을 의미합니다.

 

한마디로 거의 동일한 시간에 동시 다발적으로 요청이 들어와도 요청을 일단 Blocking 하지 않고, 외부 애플리케이션에게 요청을 그대로 전달합니다.

 

이렇게 하면 외부 애플리케이션 Controller에서 5초 정도의 지연시간을 주긴했지만 한꺼번에 다섯 개의 요청을 거의 동시에 외부 애플리케이션 Controller에 전송하는 효과를 보이기 때문에 전체 처리 시간 역시 5초를 조금 넘는 6초의 시간이 걸리는 것입니다.

 

이것이 바로 Spring WebFlux에서 지원하는 Non-Blocking 통신의 실체입니다.

1차로 요청을 수신한 애플리케이션에서 외부 애플리케이션에 요청을 추가적으로 전달할 때 1차로 요청을 수신한 애플리케이션의 요청 처리 쓰레드가 Blocking 되지 않는다는 사실, 꼭 기억하세요!

 


 

핵심 포인트

  • Spring WebFlux는 Spring 5부터 지원하는 리액티브 웹 애플리케이션을 위한 웹 프레임워크이다.
  • Spring WebFlux는 Spring MVC 방식의 @Controller, @RestController, @RequestMapping 등과 같은 애너테이션을 동일하게 지원한다.
  • Spring WebFlux는 1차로 요청을 수신한 애플리케이션에서 외부 애플리케이션에 요청을 추가적으로 전달할 때 1차로 요청을 수신한 애플리케이션의 요청 처리 쓰레드가 Blocking 되지 않는다.