Functional Endpoints

Dev.Hammy·2024년 4월 6일
0

Spring Web MVC에는 요청을 라우팅하고 처리하는 데 함수가 사용되며 불변성을 위해 설계된 계약인 경량 기능 프로그래밍 모델인 WebMvc.fn이 포함되어 있습니다. 이는 주석 기반 프로그래밍 모델의 대안이지만 동일한 DispatcherServlet에서 실행됩니다.

Overview

반응형 스택에서 이에 상응하는 내용 보기

WebMvc.fn에서 HTTP 요청은 ServerRequest를 가져와 ServerResponse를 반환하는 함수인 HandlerFunction을 통해 처리됩니다. 요청과 응답 객체 모두 HTTP 요청과 응답에 대한 JDK 8 친화적인 액세스를 제공하는 불변 계약을 갖고 있습니다. HandlerFunction은 annotation 기반 프로그래밍 모델의 @RequestMapping 메소드 본문과 동일합니다.

들어오는 요청은 RouterFunction이 있는 핸들러 함수로 라우팅됩니다. 이 함수는 ServerRequest를 가져와 선택적 HandlerFunction(예: Optional<HandlerFunction>)을 반환합니다. 라우터 함수가 일치하면 핸들러 함수가 반환됩니다. 그렇지 않으면 비어 있는 Optional입니다. RouterFunction@RequestMapping annotation과 동일하지만 라우터 기능이 데이터뿐만 아니라 동작도 제공한다는 주요 차이점이 있습니다.

RouterFunctions.route()는 다음 예제와 같이 라우터 생성을 용이하게 하는 라우터 빌더를 제공합니다.

PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);

RouterFunction<ServerResponse> route = route() // (1)
	.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
	.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
	.POST("/person", handler::createPerson)
	.build();


public class PersonHandler {

	// ...

	public ServerResponse listPeople(ServerRequest request) {
		// ...
	}

	public ServerResponse createPerson(ServerRequest request) {
		// ...
	}

	public ServerResponse getPerson(ServerRequest request) {
		// ...
	}
}

(1) route()를 사용하여 라우터를 생성합니다.

예를 들어 @Configuration 클래스에 노출하여 RouterFunction을 빈으로 등록하면 서버 실행에 설명된 대로 서블릿에 의해 자동 감지됩니다.

HandlerFunction

반응형 스택에서 이에 상응하는 내용 보기

ServerRequestServerResponse는 헤더, 본문, 메서드 및 상태 코드를 포함하여 HTTP 요청 및 응답에 대한 JDK 8 친화적인 액세스를 제공하는 불변 인터페이스입니다.

ServerRequest

ServerRequest는 HTTP 메서드, URI, 헤더 및 쿼리 매개 변수에 대한 액세스를 제공하는 반면, 본문에 대한 액세스는 body 메서드를 통해 제공됩니다. 다음 예에서는 요청 본문을 String로 추출합니다.

String string = request.body(String.class);

다음 예에서는 본문을 List<Person>으로 추출합니다. 여기서 Person 객체는 JSON 또는 XML과 같은 직렬화된 형식에서 디코딩됩니다.

List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});

다음 예에서는 매개변수에 액세스하는 방법을 보여줍니다.

MultiValueMap<String, String> params = request.params();

ServerResponse

ServerResponse는 HTTP 응답에 대한 액세스를 제공하며, 변경이 불가능하므로 build 메서드를 사용하여 생성할 수 있습니다. 빌더를 사용하여 응답 상태를 설정하거나, 응답 헤더를 추가하거나, 본문을 제공할 수 있습니다. 다음 예에서는 JSON 콘텐츠로 200(OK) 응답을 생성합니다.

Person person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);

다음 예에서는 본문 없이 Location 헤더를 사용하여 201(CREATED) 응답을 작성하는 방법을 보여줍니다.

URI location = ...
ServerResponse.created(location).build();

CompletableFuture, Publisher 또는 ReactiveAdapterRegistry에서 지원하는 기타 유형의 비동기 결과를 본문으로 사용할 수도 있습니다. 예를 들어:

Mono<Person> person = webClient.get().retrieve().bodyToMono(Person.class);
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);

본문뿐만 아니라 상태 또는 헤더도 비동기 유형을 기반으로 하는 경우 CompletableFuture<ServerResponse>, Publisher<ServerResponse> 또는 ReactiveAdapterRegistry에서 지원하는 기타 비동기 유형을 허용하는 ServerResponse의 static async 메서드를 사용할 수 있습니다. . 예를 들어:

Mono<ServerResponse> asyncResponse = webClient.get().retrieve().bodyToMono(Person.class)
  .map(p -> ServerResponse.ok().header("Name", p.name()).body(p));
ServerResponse.async(asyncResponse);

서버 전송 이벤트ServerResponse의 정적 sse 메서드를 통해 제공될 수 있습니다. 해당 메소드에서 제공하는 빌더를 사용하면 문자열 또는 기타 객체를 JSON으로 보낼 수 있습니다. 예를 들어:

public RouterFunction<ServerResponse> sse() {
	return route(GET("/sse"), request -> ServerResponse.sse(sseBuilder -> {
				// Save the sseBuilder object somewhere..
			}));
}

// In some other thread, sending a String
sseBuilder.send("Hello world");

// Or an object, which will be transformed into JSON
Person person = ...
sseBuilder.send(person);

// Customize the event by using the other methods
sseBuilder.id("42")
		.event("sse event")
		.data(person);

// and done at some point
sseBuilder.complete();

Handler Classes

다음 예제와 같이 핸들러 함수를 람다로 작성할 수 있습니다.

HandlerFunction<ServerResponse> helloWorld =
  request -> ServerResponse.ok().body("Hello World");

이는 편리하지만 애플리케이션에는 여러 함수가 필요하고 여러 인라인 람다를 사용하면 지저분해질 수 있습니다. 따라서 관련 핸들러 함수를 주석 기반 애플리케이션에서 @Controller와 유사한 역할을 하는 핸들러 클래스로 그룹화하는 것이 유용합니다. 예를 들어 다음 클래스는 반응형 Person repository를 노출합니다.

public class PersonHandler {

	private final PersonRepository repository;

	public PersonHandler(PersonRepository repository) {
		this.repository = repository;
	}

	public ServerResponse listPeople(ServerRequest request) {  // (1)
		List<Person> people = repository.allPeople();
		return ok().contentType(APPLICATION_JSON).body(people);
	}

	public ServerResponse createPerson(ServerRequest request) throws Exception { // (2)
		Person person = request.body(Person.class);
		repository.savePerson(person);
		return ok().build();
	}

	public ServerResponse getPerson(ServerRequest request) { // (3)
		int personId = Integer.parseInt(request.pathVariable("id"));
		Person person = repository.getPerson(personId);
		if (person != null) {
			return ok().contentType(APPLICATION_JSON).body(person);
		}
		else {
			return ServerResponse.notFound().build();
		}
	}

}

(1) listPeople은 repository에 있는 모든 Person 객체를 JSON으로 반환하는 핸들러 함수입니다.
(2) createPerson은 요청 본문에 포함된 새 Person을 저장하는 핸들러 함수입니다.
(3) getPersonid 경로 변수로 식별되는 단일 사람을 반환하는 핸들러 함수입니다. 저장소에서 해당 Person을 검색하고, 발견되면 JSON 응답을 생성합니다. 찾을 수 없으면 404 Not Found 응답을 반환합니다.

Validation

functional 엔드포인트는 Spring의 검증 기능을 사용하여 요청 본문에 검증을 적용할 수 있습니다. 예를 들어 Person에 대한 사용자 정의 Spring Validator 구현이 있다고 가정해 보겠습니다.

public class PersonHandler {

	private final Validator validator = new PersonValidator();  // (1)

	// ...

	public ServerResponse createPerson(ServerRequest request) {
		Person person = request.body(Person.class);
		validate(person);  // (2)
		repository.savePerson(person);
		return ok().build();
	}

	private void validate(Person person) {
		Errors errors = new BeanPropertyBindingResult(person, "person");
		validator.validate(person, errors);
		if (errors.hasErrors()) {
			throw new ServerWebInputException(errors.toString());  // (3)
		}
	}
}

(1) Validator 인스턴스를 만듭니다.
(2) 유효성 검사를 적용합니다.
(3) 400 응답에 대한 예외를 발생시킵니다.

핸들러는 LocalValidatorFactoryBean을 기반으로 전역 Validator 인스턴스를 생성하고 주입하여 표준 Bean 유효성 검사 API(JSR-303)를 사용할 수도 있습니다. 스프링 유효성 검사를 참조하세요.

RouterFunction

반응형 스택에서 이에 상응하는 내용 보기

라우터 기능은 요청을 해당 HandlerFunction으로 라우팅하는 데 사용됩니다. 일반적으로 라우터 함수를 직접 작성하지 않고 RouterFunctions 유틸리티 클래스의 메서드를 사용하여 만듭니다. RouterFunctions.route()(매개변수 없음)는 라우터 기능을 생성하기 위한 유연한 빌더를 제공하는 반면, RouterFunctions.route(RequestPredicate, HandlerFunction)는 라우터를 생성하는 직접적인 방법을 제공합니다.

일반적으로, 찾기 어려운 정적 가져오기를 요구하지 않고 일반적인 매핑 시나리오에 대한 편리한 지름길을 제공하는 route() 빌더를 사용하는 것이 좋습니다. 예를 들어, 라우터 기능 빌더는 GET 요청에 대한 매핑을 생성하기 위해 GET(String, HandlerFunction) 메서드를 제공합니다. POST의 경우 POST(String, HandlerFunction)입니다.

HTTP 메서드 기반 매핑 외에도 경로 빌더는 요청에 매핑할 때 추가 조건자를 도입하는 방법을 제공합니다. 각 HTTP 메서드에는 RequestPredicate를 매개변수로 사용하는 오버로드된 변형이 있으며 이를 통해 추가 제약 조건을 표현할 수 있습니다.

Predicates

자신만의 RequestPredicate를 작성할 수 있지만 RequestPredicates 유틸리티 클래스는 요청 경로, HTTP 메서드, 콘텐츠 유형 등에 따라 일반적으로 사용되는 구현을 제공합니다. 다음 예에서는 요청 조건자를 사용하여 Accept 헤더를 기반으로 제약 조건을 만듭니다.

RouterFunction<ServerResponse> route = RouterFunctions.route()
	.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
		request -> ServerResponse.ok().body("Hello World")).build();

다음을 사용하여 여러 요청 predicate를 함께 구성할 수 있습니다.

  • RequestPredicate.and(RequestPredicate) — 둘 다 일치해야 합니다.

  • RequestPredicate.or(RequestPredicate) — 둘 중 하나가 일치할 수 있습니다.

RequestPredicates의 많은 predicates가 구성됩니다. 예를 들어 RequestPredicates.GET(String)RequestPredicates.method(HttpMethod)RequestPredicates.path(String)로 구성됩니다. 위에 표시된 예에서는 빌더가 내부적으로 RequestPredicates.GET을 사용하고 이를 accept predicate로 구성하므로 두 개의 요청 조건자를 사용합니다.

Routes

라우터 기능은 순서대로 평가됩니다. 첫 번째 경로가 일치하지 않으면 두 번째 경로가 평가되는 식입니다. 따라서 일반 경로보다 구체적인 경로를 선언하는 것이 합리적입니다. 이는 나중에 설명하겠지만 라우터 기능을 Spring 빈으로 등록할 때에도 중요합니다. 이 동작은 "가장 구체적인" 컨트롤러 메서드가 자동으로 선택되는 주석 기반 프로그래밍 모델과 다릅니다.

라우터 함수 빌더를 사용할 때 정의된 모든 경로는 build()에서 반환되는 하나의 RouterFunction으로 구성됩니다. 여러 라우터 기능을 함께 구성하는 다른 방법도 있습니다.

  • RouterFunctions.route() 빌더에 add(RouterFunction)

  • RouterFunction.and(RouterFunction)

  • RouterFunction.andRoute(RequestPredicate, HandlerFunction)  - 중첩된 RouterFunctions.route()가 있는 RouterFunction.and()의 단축키입니다.

다음 예에서는 네 가지 경로의 구성을 보여줍니다.

PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);

RouterFunction<ServerResponse> otherRoute = ...

RouterFunction<ServerResponse> route = route()
	.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) // (1)
	.GET("/person", accept(APPLICATION_JSON), handler::listPeople)  // (2)
	.POST("/person", handler::createPerson)  // (3)
	.add(otherRoute)   // (4)
	.build();

(1) JSON과 일치하는 Accept 헤더가 있는 GET /person/{id}PersonHandler.getPerson으로 라우팅됩니다.
(2) JSON과 일치하는 Accept 헤더가 있는 GET /personPersonHandler.listPeople로 라우팅됩니다.
(3) 추가 조건자가 없는 POST /personPersonHandler.createPerson에 매핑됩니다.
(4) otherRoute는 다른 곳에서 생성되어 구축된 경로에 추가되는 라우터 기능입니다.

Nested Routes

라우터 기능 그룹이 공유 경로와 같은 공유 predicate를 갖는 것이 일반적입니다. 위의 예에서 공유 predicate는 세 개의 경로에서 사용되는 /person과 일치하는 경로 predicate가 됩니다. annotation을 사용할 때 /person에 매핑되는 유형 수준 @RequestMapping annotation을 사용하여 이 중복을 제거합니다. WebMvc.fn에서 경로 조건자는 라우터 기능 빌더의 path 메서드를 통해 공유될 수 있습니다. 예를 들어, 위 예제의 마지막 몇 줄은 중첩된 경로를 사용하여 다음과 같은 방식으로 개선될 수 있습니다.

RouterFunction<ServerResponse> route = route()
	.path("/person", builder -> builder // (1)
		.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
		.GET(accept(APPLICATION_JSON), handler::listPeople)
		.POST(handler::createPerson))
	.build();

(1) path의 두 번째 매개변수는 라우터 빌더를 사용하는 소비자입니다.

경로 기반 중첩이 가장 일반적이지만 빌더에서 nest 메소드를 사용하여 모든 종류의 조건자에 중첩할 수 있습니다. 위 내용에는 여전히 공유 Accept-header 조건자의 형태로 일부 중복이 포함되어 있습니다. accept와 함께 nest 메소드를 사용하면 더욱 개선할 수 있습니다.

RouterFunction<ServerResponse> route = route()
	.path("/person", b1 -> b1
		.nest(accept(APPLICATION_JSON), b2 -> b2
			.GET("/{id}", handler::getPerson)
			.GET(handler::listPeople))
		.POST(handler::createPerson))
	.build();

Serving Resources

WebMvc.fn은 리소스 제공을 위한 기본 지원을 제공합니다.

[Note]
아래 설명된 기능 외에도 RouterFunctions#resource(java.util.function.Function) 덕분에 더욱 유연한 리소스 처리를 구현할 수 있습니다.

Redirecting to a resource

지정된 predicate와 일치하는 요청을 리소스로 리디렉션할 수 있습니다. 예를 들어 단일 페이지 애플리케이션에서 리디렉션을 처리하는 데 유용할 수 있습니다.

   ClassPathResource index = new ClassPathResource("static/index.html");
List<String> extensions = Arrays.asList("js", "css", "ico", "png", "jpg", "gif");
RequestPredicate spaPredicate = path("/api/**").or(path("/error")).or(pathExtension(extensions::contains)).negate();
RouterFunction<ServerResponse> redirectToIndex = route()
	.resource(spaPredicate, index)
	.build();

Serving resources from a root location

주어진 패턴과 일치하는 요청을 주어진 루트 위치를 기준으로 한 리소스로 라우팅하는 것도 가능합니다.

Resource location = new FileSystemResource("public-resources/");
RouterFunction<ServerResponse> resources = RouterFunctions.resources("/resources/**", location);

Running a Server

반응형 스택에서 이에 상응하는 내용 보기

일반적으로 Spring 구성을 사용하여 요청을 처리하는 데 필요한 구성 요소를 선언하는 MVC 구성을 통해 DispatcherHandler 기반 설정에서 라우터 기능을 실행합니다. MVC Java 구성은 기능적 끝점을 지원하기 위해 다음 인프라 구성 요소를 선언합니다.

  • RouterFunctionMapping: Spring 구성에서 하나 이상의 RouterFunction<?> Bean을 감지하고 순서를 지정하며 RouterFunction.andOther를 통해 결합하고 결과적으로 구성된 RouterFunction으로 요청을 라우팅합니다.

  • HandlerFunctionAdapter: DispatcherHandler가 요청에 매핑된 HandlerFunction을 호출할 수 있게 해주는 간단한 어댑터입니다.

앞선 구성 요소를 사용하면 기능적 끝점이 DispatcherServlet 요청 처리 수명 주기에 맞도록 할 수 있으며 선언된 경우 주석이 달린 컨트롤러와 함께 (잠재적으로) 나란히 실행될 수도 있습니다. 이는 또한 Spring Boot 웹 스타터에서 기능적 엔드포인트를 활성화하는 방법이기도 합니다.

다음 예는 WebFlux Java 구성을 보여줍니다.

@Configuration
@EnableMvc
public class WebConfig implements WebMvcConfigurer {

	@Bean
	public RouterFunction<?> routerFunctionA() {
		// ...
	}

	@Bean
	public RouterFunction<?> routerFunctionB() {
		// ...
	}

	// ...

	@Override
	public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
		// configure message conversion...
	}

	@Override
	public void addCorsMappings(CorsRegistry registry) {
		// configure CORS...
	}

	@Override
	public void configureViewResolvers(ViewResolverRegistry registry) {
		// configure view resolution for HTML rendering...
	}
}

Filtering Handler Functions

반응형 스택에서 이에 상응하는 내용 보기

라우팅 함수 빌더에서 before, after 또는 filter 메서드를 사용하여 핸들러 함수를 필터링할 수 있습니다. annotation을 사용하면 @ControllerAdvice, ServletFilter 또는 둘 다를 사용하여 유사한 기능을 얻을 수 있습니다. 필터는 빌더가 구축한 모든 경로에 적용됩니다. 이는 중첩된 경로에 정의된 필터가 "최상위" 경로에 적용되지 않음을 의미합니다. 예를 들어 다음 예를 고려해보세요.

RouterFunction<ServerResponse> route = route()
	.path("/person", b1 -> b1
		.nest(accept(APPLICATION_JSON), b2 -> b2
			.GET("/{id}", handler::getPerson)
			.GET(handler::listPeople)
			.before(request -> ServerRequest.from(request)  // (1)
				.header("X-RequestHeader", "Value")
				.build()))
		.POST(handler::createPerson))
	.after((request, response) -> logResponse(response))  // (2)
	.build();

(1) 사용자 정의 요청 헤더를 추가하는 before 필터는 두 개의 GET 경로에만 적용됩니다.
(2) 응답을 기록하는 after 필터는 중첩된 경로를 포함한 모든 경로에 적용됩니다.

라우터 빌더의 filter 메소드는 HandlerFilterFunction을 사용합니다. 이 함수는 ServerRequestHandlerFunction을 사용하고 ServerResponse를 반환합니다. 핸들러 함수 매개변수는 체인의 다음 요소를 나타냅니다. 이는 일반적으로 라우팅되는 핸들러이지만 여러 필터가 적용되는 경우 다른 필터가 될 수도 있습니다.

이제 특정 경로가 허용되는지 여부를 결정할 수 있는 SecurityManager가 있다고 가정하고 경로에 간단한 보안 필터를 추가할 수 있습니다. 다음 예에서는 그 방법을 보여줍니다.

SecurityManager securityManager = ...

RouterFunction<ServerResponse> route = route()
	.path("/person", b1 -> b1
		.nest(accept(APPLICATION_JSON), b2 -> b2
			.GET("/{id}", handler::getPerson)
			.GET(handler::listPeople))
		.POST(handler::createPerson))
	.filter((request, next) -> {
		if (securityManager.allowAccessTo(request.path())) {
			return next.handle(request);
		}
		else {
			return ServerResponse.status(UNAUTHORIZED).build();
		}
	})
	.build();

앞의 예에서는 next.handle(ServerRequest) 호출이 선택 사항임을 보여줍니다. 접근이 허용된 경우에만 핸들러 기능이 실행되도록 합니다.

라우터 함수 빌더에서 filter 메소드를 사용하는 것 외에도 RouterFunction.filter(HandlerFilterFunction)를 통해 기존 라우터 함수에 필터를 적용하는 것이 가능합니다.

[Note]
기능적 엔드포인트에 대한 CORS 지원은 전용 CorsFilter를 통해 제공됩니다.

0개의 댓글