1. 소개

Spring WebFlux는 반응 원리를 사용하여 구축된 새로운 기능적 웹 프레임워크입니다.

이 사용방법(예제)에서는 실제로 작업하는 방법을 배웁니다.

Spring 5 WebFlux 에 대한 기존 사용방법(예제)를 기반으로 합니다. 이 사용방법(예제)에서는 어노테이션 기반 구성 요소를 사용하여 간단한 반응형 REST 애플리케이션을 만들었습니다. 여기서는 기능적 프레임워크를 대신 사용합니다.

2. 메이븐 의존성

이전 기사에서 정의한 것과 동일한 spring-boot-starter-webflux 의존성이 필요합니다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    <version>2.6.4</version>
</dependency>

3. 기능적 웹 프레임워크

기능적 웹 프레임워크는 기능을 사용하여 요청을 라우팅하고 처리하는 새로운 프로그래밍 모델을 도입합니다.

어노테이션 매핑을 사용하는 어노테이션 기반 모델과 달리 여기서는 HandlerFunctionRouterFunction s 를 사용 합니다.

마찬가지로 어노테이션이 달린 컨트롤러에서와 마찬가지로 기능 엔드포인트 접근 방식은 동일한 반응 스택에 구축됩니다.

3.1. 핸들러 함수

HandlerFunction라우팅된 요청에 대한 응답을 생성하는 함수를 나타냅니다.

@FunctionalInterface
public interface HandlerFunction<T extends ServerResponse> {
    Mono<T> handle(ServerRequest request);
}

이 인터페이스는 기본적으로 서블릿과 매우 유사하게 작동 하는 Function<Request, Response<T>> 입니다.

표준 Servlet#service(ServletRequest req, ServletResponse res) 와 비교하여 HandlerFunction 은 응답을 입력 매개변수로 사용하지 않습니다.

3.2. 라우터 기능

RouterFunction@RequestMapping 어노테이션의 대안으로 사용됩니다. 이를 사용하여 요청을 처리기 함수로 라우팅할 수 있습니다.

@FunctionalInterface
public interface RouterFunction<T extends ServerResponse> {
    Mono<HandlerFunction<T>> route(ServerRequest request);
    // ...
}

  일반적으로 완전한 라우터 함수를 작성하는 대신 도우미 함수 RouterFunctions.route() 를 가져와 경로를 만들 수 있습니다.

RequestPredicate 를 적용하여 요청을 라우팅할 수 있습니다 . 조건자가 일치하면 두 번째 인수인 처리기 함수가 반환됩니다.

public static <T extends ServerResponse> RouterFunction<T> route(
  RequestPredicate predicate,
  HandlerFunction<T> handlerFunction)

route() 메서드는 RouterFunction 을 반환 하기 때문에 이를 연결하여 강력하고 복잡한 라우팅 체계를 구축할 수 있습니다.

4. Functional Web을 이용한 Reactive REST Application

이전 사용방법(예제)에서는 @RestControllerWebClient 를 사용하여 간단한 EmployeeManagement REST 애플리케이션을 만들었습니다.

이제 라우터 및 핸들러 기능을 사용하여 동일한 논리를 구현해 보겠습니다.

먼저 Employee반응 스트림을 게시하고 소비하기 위해 RouterFunction 을 사용하여 경로를 만들어야 합니다 .

경로는 Spring 빈으로 등록되며 모든 구성 클래스 내에서 생성될 수 있습니다.

4.1. 단일 리소스

단일 Employee 리소스 를 게시하는 RouterFunction 을 사용하여 첫 번째 경로를 만들어 보겠습니다 .

@Bean
RouterFunction<ServerResponse> getEmployeeByIdRoute() {
  return route(GET("/employees/{id}"), 
    req -> ok().body(
      employeeRepository().findEmployeeById(req.pathVariable("id")), Employee.class));
}

첫 번째 인수는 요청 술어입니다. 여기에서 정적으로 가져온 RequestPredicates.GET 메서드를 어떻게 사용했는지 확인 하십시오. 두 번째 매개변수는 조건자가 적용되는 경우 사용될 핸들러 함수를 정의합니다.

즉, 위의 예제는  /employees/{id} 에 대한 모든 GET 요청 을  EmployeeRepository#findEmployeeById(String id) 메서드로 라우팅합니다.

4.2. 컬렉션 리소스

다음으로 컬렉션 리소스를 게시하기 위해 다른 경로를 추가해 보겠습니다.

@Bean
RouterFunction<ServerResponse> getAllEmployeesRoute() {
  return route(GET("/employees"), 
    req -> ok().body(
      employeeRepository().findAllEmployees(), Employee.class));
}

4.3. 단일 리소스 업데이트

마지막으로 Employee 리소스를 업데이트하기 위한 경로를 추가해 보겠습니다.

@Bean
RouterFunction<ServerResponse> updateEmployeeRoute() {
  return route(POST("/employees/update"), 
    req -> req.body(toMono(Employee.class))
      .doOnNext(employeeRepository()::updateEmployee)
      .then(ok().build()));
}

5. 루트 구성

단일 라우터 기능에서 경로를 함께 구성할 수도 있습니다.

위에서 만든 경로를 결합하는 방법을 살펴보겠습니다.

@Bean
RouterFunction<ServerResponse> composedRoutes() {
  return 
    route(GET("/employees"), 
      req -> ok().body(
        employeeRepository().findAllEmployees(), Employee.class))
        
    .and(route(GET("/employees/{id}"), 
      req -> ok().body(
        employeeRepository().findEmployeeById(req.pathVariable("id")), Employee.class)))
        
    .and(route(POST("/employees/update"), 
      req -> req.body(toMono(Employee.class))
        .doOnNext(employeeRepository()::updateEmployee)
        .then(ok().build())));
}

여기에서는 RouterFunction.and() 를 사용 하여 경로를 결합했습니다.

마지막으로 라우터와 핸들러를 사용하여 EmployeeManagement 애플리케이션에 필요한 완전한 REST API를 구현했습니다 .

응용 프로그램을 실행하려면 별도의 경로를 사용하거나 위에서 생성한 단일 구성 경로를 사용할 수 있습니다.

6. 테스트 경로

WebTestClient 를 사용 하여 경로를 테스트할 수 있습니다.

이렇게 하려면 먼저 bindToRouterFunction 메서드를 사용하여 경로를 바인딩한 다음 테스트 클라이언트 인스턴스를 빌드해야 합니다.

getEmployeeByIdRoute 를 테스트해 보겠습니다 .

@Test
void givenEmployeeId_whenGetEmployeeById_thenCorrectEmployee() {
    WebTestClient client = WebTestClient
      .bindToRouterFunction(config.getEmployeeByIdRoute())
      .build();

    Employee employee = new Employee("1", "Employee 1");

    given(employeeRepository.findEmployeeById("1")).willReturn(Mono.just(employee));

    client.get()
      .uri("/employees/1")
      .exchange()
      .expectStatus()
      .isOk()
      .expectBody(Employee.class)
      .isEqualTo(employee);
}

그리고 유사하게 getAllEmployeesRoute :

@Test
void whenGetAllEmployees_thenCorrectEmployees() {
    WebTestClient client = WebTestClient
      .bindToRouterFunction(config.getAllEmployeesRoute())
      .build();

    List<Employee> employees = Arrays.asList(
      new Employee("1", "Employee 1"),
      new Employee("2", "Employee 2"));

    Flux<Employee> employeeFlux = Flux.fromIterable(employees);
    given(employeeRepository.findAllEmployees()).willReturn(employeeFlux);

    client.get()
      .uri("/employees")
      .exchange()
      .expectStatus()
      .isOk()
      .expectBodyList(Employee.class)
      .isEqualTo(employees);
}

EmployeeRepository를 통해 Employee 인스턴스 가 업데이트 되었음을 ​​어설션하여 updateEmployeeRoute 를 테스트할 수도 있습니다 .

@Test
void whenUpdateEmployee_thenEmployeeUpdated() {
    WebTestClient client = WebTestClient
      .bindToRouterFunction(config.updateEmployeeRoute())
      .build();

    Employee employee = new Employee("1", "Employee 1 Updated");

    client.post()
      .uri("/employees/update")
      .body(Mono.just(employee), Employee.class)
      .exchange()
      .expectStatus()
      .isOk();

    verify(employeeRepository).updateEmployee(employee);
}

WebTestClient 를 사용한 테스트에 대한 자세한 내용은 WebClientWebTestClient 작업 에 대한 사용방법(예제)를 참조하십시오 .

7. 요약

이 예제에서는 Spring 5의 새로운 기능적 웹 프레임워크를 소개하고 RouterFunctionHandlerFunction이라는 두 가지 핵심 인터페이스를 살펴보았습니다. 또한 요청을 처리하고 응답을 보내는 다양한 경로를 만드는 방법도 배웠습니다.

또한 기능적 엔드포인트 모델을 사용하여 Spring 5 WebFlux 사용방법(예제)에 소개된 EmployeeManagement 애플리케이션을 다시 만들었습니다.

항상 그렇듯이 전체 소스 코드는 Github 에서 찾을 수 있습니다 .

Generic footer banner