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. 기능적 웹 프레임워크
기능적 웹 프레임워크는 기능을 사용하여 요청을 라우팅하고 처리하는 새로운 프로그래밍 모델을 도입합니다.
어노테이션 매핑을 사용하는 어노테이션 기반 모델과 달리 여기서는 HandlerFunction 및 RouterFunction 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
이전 사용방법(예제)에서는 @RestController 및 WebClient 를 사용하여 간단한 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 를 사용한 테스트에 대한 자세한 내용은 WebClient 및 WebTestClient 작업 에 대한 사용방법(예제)를 참조하십시오 .
7. 요약
이 예제에서는 Spring 5의 새로운 기능적 웹 프레임워크를 소개하고 RouterFunction 과 HandlerFunction이라는 두 가지 핵심 인터페이스를 살펴보았습니다. 또한 요청을 처리하고 응답을 보내는 다양한 경로를 만드는 방법도 배웠습니다.
또한 기능적 엔드포인트 모델을 사용하여 Spring 5 WebFlux 사용방법(예제)에 소개된 EmployeeManagement 애플리케이션을 다시 만들었습니다.
항상 그렇듯이 전체 소스 코드는 Github 에서 찾을 수 있습니다 .