Loading...
Spring Framework Reference Documentation 7.0.2의 Functional Endpoints의 한국어 번역본입니다.
아래의 경우에 피드백에서 신고해주신다면 반영하겠습니다.
감사합니다 :)
See equivalent in the Servlet stack
Spring WebFlux는 함수들이 request를 route하고 처리하며, contract가 immutability를 위해 설계된 경량의 함수형 프로그래밍 모델인 WebFlux.fn을 포함합니다. 이는 어노테이션 기반 프로그래밍 모델에 대한 대안이지만, 그 외에는 동일한 Reactive Core 기반 위에서 동작합니다.
See equivalent in the Servlet stack
WebFlux.fn에서 HTTP request는 HandlerFunction으로 처리되며, 이는 ServerRequest를 받고 지연된 ServerResponse(즉, Mono<ServerResponse>)를 반환하는 함수입니다.
request와 response 객체 모두 HTTP request와 response에 대한 JDK 8 친화적인 접근을 제공하는 immutable contract를 가집니다.
HandlerFunction은 어노테이션 기반 프로그래밍 모델에서 @RequestMapping 메서드의 body에 해당합니다.
들어오는 request는 RouterFunction을 사용하여 handler function으로 route되며, 이는 ServerRequest를 받고 지연된 HandlerFunction(즉, Mono<HandlerFunction>)을 반환하는 함수입니다.
router function이 일치하면 handler function이 반환되고, 그렇지 않으면 비어 있는 Mono가 반환됩니다.
RouterFunction은 @RequestMapping 어노테이션에 해당하지만, router function이 data뿐 아니라 behavior도 제공한다는 점이 큰 차이입니다.
RouterFunctions.route()는 다음 예제에서 보듯이 router 생성을 용이하게 하는 router builder를 제공합니다:
1import static org.springframework.http.MediaType.APPLICATION_JSON; 2import static org.springframework.web.reactive.function.server.RequestPredicates.*; 3import static org.springframework.web.reactive.function.server.RouterFunctions.route; 4 5PersonRepository repository = ... 6PersonHandler handler = new PersonHandler(repository); 7 8RouterFunction<ServerResponse> route = route() // (1) 9 .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) 10 .GET("/person", accept(APPLICATION_JSON), handler::listPeople) 11 .POST("/person", handler::createPerson) 12 .build(); 13 14public class PersonHandler { 15 16 // ... 17 18 public Mono<ServerResponse> listPeople(ServerRequest request) { 19 // ... 20 } 21 22 public Mono<ServerResponse> createPerson(ServerRequest request) { 23 // ... 24 } 25 26 public Mono<ServerResponse> getPerson(ServerRequest request) { 27 // ... 28 } 29}
| 1 | route()를 사용하여 router를 생성합니다. |
1val repository: PersonRepository = ... 2val handler = PersonHandler(repository) 3 4val route = coRouter { // (1) 5 accept(APPLICATION_JSON).nest { 6 GET("/person/{id}", handler::getPerson) 7 GET("/person", handler::listPeople) 8 } 9 POST("/person", handler::createPerson) 10} 11 12class PersonHandler(private val repository: PersonRepository) { 13 14 // ... 15 16 suspend fun listPeople(request: ServerRequest): ServerResponse { 17 // ... 18 } 19 20 suspend fun createPerson(request: ServerRequest): ServerResponse { 21 // ... 22 } 23 24 suspend fun getPerson(request: ServerRequest): ServerResponse { 25 // ... 26 } 27}
| 1 | Coroutines router DSL을 사용하여 router를 생성합니다. router { }를 통한 Reactive 대안도 사용할 수 있습니다. |
RouterFunction을 실행하는 한 가지 방법은 이를 HttpHandler로 변환하고, 다음의 내장된 server adapters 중 하나를 통해 설치하는 것입니다:
RouterFunctions.toHttpHandler(RouterFunction)RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)대부분의 애플리케이션은 WebFlux Java 설정을 통해 실행할 수 있습니다. Running a Server를 참고하세요.
See equivalent in the Servlet stack
ServerRequest와 ServerResponse는 HTTP request와 response에 대한 JDK 8 친화적인 접근을 제공하는 immutable 인터페이스입니다.
request와 response 모두 body 스트림에 대해 Reactive Streams back pressure를 제공합니다.
request body는 Reactor Flux 또는 Mono로 표현됩니다.
response body는 Flux와 Mono를 포함한 어떤 Reactive Streams Publisher로도 표현될 수 있습니다.
이에 대한 자세한 내용은 Reactive Libraries를 참고하세요.
ServerRequest는 HTTP 메서드, URI, 헤더, 쿼리 파라미터에 대한 접근을 제공하며, body에 대한 접근은 body 메서드를 통해 제공됩니다.
다음 예제는 request body를 Mono<String>으로 추출합니다:
1Mono<String> string = request.bodyToMono(String.class);
1val string = request.awaitBody<String>()
다음 예제는 body를 Flux<Person>(또는 Kotlin에서는 Flow<Person>)으로 추출하며,
Person 객체는 JSON이나 XML과 같은 어떤 serialized form으로부터 decode됩니다:
1Flux<Person> people = request.bodyToFlux(Person.class);
1val people = request.bodyToFlow<Person>()
앞의 예제들은 보다 일반적인 ServerRequest.body(BodyExtractor)를 사용하는 shortcut이며,
이는 BodyExtractor 함수형 전략 인터페이스를 받습니다.
유틸리티 클래스인 BodyExtractors는 여러 인스턴스에 대한 접근을 제공합니다.
예를 들어, 앞의 예제들은 다음과 같이도 작성할 수 있습니다:
1Mono<String> string = request.body(BodyExtractors.toMono(String.class)); 2Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));
1val string = request.body(BodyExtractors.toMono(String::class.java)).awaitSingle() 2val people = request.body(BodyExtractors.toFlux(Person::class.java)).asFlow()
다음 예제는 form data에 접근하는 방법을 보여줍니다:
1Mono<MultiValueMap<String, String>> map = request.formData();
1val map = request.awaitFormData()
다음 예제는 multipart data에 map 형태로 접근하는 방법을 보여줍니다:
1Mono<MultiValueMap<String, Part>> map = request.multipartData();
1val map = request.awaitMultipartData()
다음 예제는 multipart data에 streaming 방식으로, 한 번에 하나씩 접근하는 방법을 보여줍니다:
1Flux<PartEvent> allPartEvents = request.bodyToFlux(PartEvent.class); 2allPartEvents.windowUntil(PartEvent::isLast) 3 .concatMap(p -> p.switchOnFirst((signal, partEvents) -> { 4 if (signal.hasValue()) { 5 PartEvent event = signal.get(); 6 if (event instanceof FormPartEvent formEvent) { 7 String value = formEvent.value(); 8 // handle form field 9 } 10 else if (event instanceof FilePartEvent fileEvent) { 11 String filename = fileEvent.filename(); 12 Flux<DataBuffer> contents = partEvents.map(PartEvent::content); 13 // handle file upload 14 } 15 else { 16 return Mono.error(new RuntimeException("Unexpected event: " + event)); 17 } 18 } 19 else { 20 return partEvents; // either complete or error signal 21 } 22 }));
1val allPartsEvents = request.bodyToFlux<PartEvent>() 2allPartsEvents.windowUntil(PartEvent::isLast) 3 .concatMap { 4 it.switchOnFirst { signal, partEvents -> 5 if (signal.hasValue()) { 6 val event = signal.get() 7 if (event is FormPartEvent) { 8 val value: String = event.value() 9 // handle form field 10 } else if (event is FilePartEvent) { 11 val filename: String = event.filename() 12 val contents: Flux<DataBuffer> = partEvents.map(PartEvent::content) 13 // handle file upload 14 } else { 15 return@switchOnFirst Mono.error(RuntimeException("Unexpected event: $event")) 16 } 17 } else { 18 return@switchOnFirst partEvents // either complete or error signal 19 } 20 } 21 }
PartEvent객체의 body contents는 메모리 누수를 피하기 위해 완전히 소비되거나, 전달되거나, 해제되어야 합니다.
다음은 DataBinder를 통해 request 파라미터, URI 변수, 헤더를 bind하는 방법과,
DataBinder를 커스터마이즈하는 방법을 보여줍니다:
1Pet pet = request.bind(Pet.class, dataBinder -> dataBinder.setAllowedFields("name"));
1val pet = request.bind(Pet::class.java) { dataBinder -> dataBinder.setAllowedFields("name") }
ServerResponse는 HTTP response에 대한 접근을 제공하며, immutable이므로 build 메서드를 사용해 이를 생성할 수 있습니다.
builder를 사용하여 response 상태를 설정하고, response 헤더를 추가하거나 body를 제공할 수 있습니다.
다음 예제는 JSON content를 가진 200 (OK) response를 생성합니다:
1Mono<Person> person = ... 2ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);
1val person: Person = ... 2ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(person)
다음 예제는 Location 헤더와 body가 없는 201 (CREATED) response를 생성하는 방법을 보여줍니다:
1URI location = ... 2ServerResponse.created(location).build();
1val location: URI = ... 2ServerResponse.created(location).build()
사용되는 codec에 따라, body가 serialize되거나 deserialize되는 방식을 커스터마이즈하기 위해 hint 파라미터를 전달할 수 있습니다. 예를 들어, Jackson JSON view를 지정하려면 다음과 같습니다:
1ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
1ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
다음 예제에서 보듯이 handler function을 람다로 작성할 수 있습니다:
1HandlerFunction<ServerResponse> helloWorld = 2 request -> ServerResponse.ok().bodyValue("Hello World");
1val helloWorld = HandlerFunction<ServerResponse> { ServerResponse.ok().bodyValue("Hello World") }
이는 편리하지만, 애플리케이션에서는 여러 function이 필요하고, 여러 개의 inline 람다는 복잡해질 수 있습니다.
따라서 관련된 handler function들을 handler 클래스로 함께 그룹화하는 것이 유용하며, 이는 어노테이션 기반 애플리케이션에서 @Controller와 유사한 역할을 합니다.
예를 들어, 다음 클래스는 리액티브 Person 리포지토리를 노출합니다:
1import static org.springframework.http.MediaType.APPLICATION_JSON; 2import static org.springframework.web.reactive.function.server.ServerResponse.ok; 3 4public class PersonHandler { 5 6 private final PersonRepository repository; 7 8 public PersonHandler(PersonRepository repository) { 9 this.repository = repository; 10 } 11 12 public Mono<ServerResponse> listPeople(ServerRequest request) { // (1) 13 Flux<Person> people = repository.allPeople(); 14 return ok().contentType(APPLICATION_JSON).body(people, Person.class); 15 } 16 17 public Mono<ServerResponse> createPerson(ServerRequest request) { // (2) 18 Mono<Person> person = request.bodyToMono(Person.class); 19 return ok().build(repository.savePerson(person)); 20 } 21 22 public Mono<ServerResponse> getPerson(ServerRequest request) { // (3) 23 int personId = Integer.valueOf(request.pathVariable("id")); 24 return repository.getPerson(personId) 25 .flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person)) 26 .switchIfEmpty(ServerResponse.notFound().build()); 27 } 28}
| 1 | listPeople은 리포지토리에서 찾은 모든 Person 객체를 JSON으로 반환하는 handler function입니다. |
| 2 | createPerson은 request body에 포함된 새로운 Person을 저장하는 handler function입니다.<br>PersonRepository.savePerson(Person)이 Mono<Void>를 반환한다는 점에 유의하세요. 이는 request에서 person이 읽혀지고 저장되었을 때 completion signal을 emit하는 비어 있는 Mono입니다. 따라서<br>build(Publisher<Void>) 메서드를 사용하여 그 completion signal이 수신될 때(즉,<br>Person이 저장되었을 때) response를 전송합니다. |
| 3 | getPerson은 id path 변수로 식별되는 단일 person을 반환하는 handler function입니다.<br>해당 Person을 리포지토리에서 조회하고, 발견되면 JSON response를 생성합니다.<br>발견되지 않으면 switchIfEmpty(Mono<T>)를 사용하여 404 Not Found response를 반환합니다. |
1class PersonHandler(private val repository: PersonRepository) { 2 3 suspend fun listPeople(request: ServerRequest): ServerResponse { // (1) 4 val people: Flow<Person> = repository.allPeople() 5 return ok().contentType(APPLICATION_JSON).bodyAndAwait(people) 6 } 7 8 suspend fun createPerson(request: ServerRequest): ServerResponse { // (2) 9 val person = request.awaitBody<Person>() 10 repository.savePerson(person) 11 return ok().buildAndAwait() 12 } 13 14 suspend fun getPerson(request: ServerRequest): ServerResponse { // (3) 15 val personId = request.pathVariable("id").toInt() 16 return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).bodyValueAndAwait(it) } 17 ?: ServerResponse.notFound().buildAndAwait() 18 19 } 20}
| 1 | listPeople은 리포지토리에서 찾은 모든 Person 객체를 JSON으로 반환하는 handler function입니다. |
| 2 | createPerson은 request body에 포함된 새로운 Person을 저장하는 handler function입니다.<br>PersonRepository.savePerson(Person)은 반환 타입이 없는 suspending function입니다. |
| 3 | getPerson은 id path 변수로 식별되는 단일 person을 반환하는 handler function입니다.<br>해당 Person을 리포지토리에서 조회하고, 발견되면 JSON response를 생성합니다.<br>발견되지 않으면 404 Not Found response를 반환합니다. |
함수형 엔드포인트는 Spring의 validation facilities를 사용하여
request body에 validation을 적용할 수 있습니다.
예를 들어, Person에 대한 커스텀 Spring
Validator 구현이 있다고 가정해 보겠습니다:
1public class PersonHandler { 2 3 private final Validator validator = new PersonValidator(); // (1) 4 5 // ... 6 7 public Mono<ServerResponse> createPerson(ServerRequest request) { 8 Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate); // (2) 9 return ok().build(repository.savePerson(person)); 10 } 11 12 private void validate(Person person) { 13 Errors errors = new BeanPropertyBindingResult(person, "person"); 14 validator.validate(person, errors); 15 if (errors.hasErrors()) { 16 throw new ServerWebInputException(errors.toString()); // (3) 17 } 18 } 19}
| 1 | Validator 인스턴스를 생성합니다. |
| 2 | validation을 적용합니다. |
| 3 | 400 response를 위한 예외를 발생시킵니다. |
1class PersonHandler(private val repository: PersonRepository) { 2 3 private val validator = PersonValidator() // (1) 4 5 // ... 6 7 suspend fun createPerson(request: ServerRequest): ServerResponse { 8 val person = request.awaitBody<Person>() 9 validate(person) // (2) 10 repository.savePerson(person) 11 return ok().buildAndAwait() 12 } 13 14 private fun validate(person: Person) { 15 val errors: Errors = BeanPropertyBindingResult(person, "person") 16 validator.validate(person, errors) 17 if (errors.hasErrors()) { 18 throw ServerWebInputException(errors.toString()) // (3) 19 } 20 } 21}
| 1 | Validator 인스턴스를 생성합니다. |
| 2 | validation을 적용합니다. |
| 3 | 400 response를 위한 예외를 발생시킵니다. |
Handler는 또한 LocalValidatorFactoryBean을 기반으로 하는 global Validator 인스턴스를 생성하고 주입함으로써
표준 bean validation API(JSR-303)를 사용할 수 있습니다.
Spring Validation을 참고하세요.
RouterFunctionSee equivalent in the Servlet stack
Router function은 request를 해당하는 HandlerFunction으로 route하는 데 사용됩니다.
일반적으로 router function을 직접 작성하지 않고, RouterFunctions 유틸리티 클래스의 메서드를 사용하여 이를 생성합니다.
매개변수 없는 RouterFunctions.route()는 router function을 생성하기 위한 fluent builder를 제공하며,
RouterFunctions.route(RequestPredicate, HandlerFunction)는 router를 직접 생성하는 방법을 제공합니다.
일반적으로 route() builder를 사용하는 것이 권장되며, 이는 발견하기 어려운 static import 없이
일반적인 매핑 시나리오에 대한 편리한 shortcut을 제공합니다.
예를 들어, router function builder는 GET request에 대한 매핑을 생성하기 위해
GET(String, HandlerFunction) 메서드를 제공하며, POST에 대해서는 POST(String, HandlerFunction)을 제공합니다.
HTTP 메서드 기반 매핑 외에도, route builder는 request에 대한 매핑 시 추가적인 predicate를 도입하는 방법을 제공합니다.
각 HTTP 메서드에 대해 RequestPredicate를 파라미터로 받는 오버로드된 변형이 있으며,
이를 통해 추가적인 제약 조건을 표현할 수 있습니다.
자신만의 RequestPredicate를 작성할 수 있지만, RequestPredicates 유틸리티 클래스는
HTTP 메서드, request 경로, 헤더, API version 등에 기반하여 매칭하기 위한 공통적인 필요를 위한
built-in 옵션을 제공합니다.
다음 예제는 Accept 헤더 request predicate를 사용합니다:
1RouterFunction<ServerResponse> route = RouterFunctions.route() 2 .GET("/hello-world", accept(MediaType.TEXT_PLAIN), 3 request -> ServerResponse.ok().bodyValue("Hello World")).build();
1val route = coRouter { 2 GET("/hello-world", accept(TEXT_PLAIN)) { 3 ServerResponse.ok().bodyValueAndAwait("Hello World") 4 } 5}
여러 request predicate를 함께 compose할 수 있으며, 다음과 같이 사용할 수 있습니다:
RequestPredicate.and(RequestPredicate) — 둘 다 일치해야 합니다.RequestPredicate.or(RequestPredicate) — 둘 중 하나가 일치하면 됩니다.RequestPredicates의 많은 predicate는 compose되어 있습니다.
예를 들어, RequestPredicates.GET(String)은
RequestPredicates.method(HttpMethod)와 RequestPredicates.path(String)으로부터 compose됩니다.
위의 예제에서도 두 개의 request predicate를 사용하고 있으며,
builder는 내부적으로 RequestPredicates.GET을 사용하고, 이를 accept predicate와 compose합니다.
Router function은 순서대로 평가됩니다. 첫 번째 route가 일치하지 않으면, 두 번째가 평가되는 식입니다. 따라서 보다 구체적인 route를 일반적인 것보다 먼저 선언하는 것이 좋습니다. 이는 나중에 설명할 Spring bean으로 router function을 등록할 때도 중요합니다. 이 동작은 "가장 구체적인" controller 메서드가 자동으로 선택되는 어노테이션 기반 프로그래밍 모델과는 다르다는 점에 유의하세요.
router function builder를 사용할 때, 정의된 모든 route는 하나의 RouterFunction으로 compose되어
build()에서 반환됩니다.
여러 router function을 함께 compose하는 다른 방법도 있습니다:
RouterFunctions.route() builder의 add(RouterFunction)RouterFunction.and(RouterFunction)RouterFunction.andRoute(RequestPredicate, HandlerFunction) — 중첩된 RouterFunctions.route()와 함께<br>RouterFunction.and()를 사용하는 shortcut입니다.다음 예제는 네 개의 route를 compose하는 방법을 보여줍니다:
1import static org.springframework.http.MediaType.APPLICATION_JSON; 2import static org.springframework.web.reactive.function.server.RequestPredicates.*; 3 4PersonRepository repository = ... 5PersonHandler handler = new PersonHandler(repository); 6 7RouterFunction<ServerResponse> otherRoute = ... 8 9RouterFunction<ServerResponse> route = route() 10 .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) // (1) 11 .GET("/person", accept(APPLICATION_JSON), handler::listPeople) // (2) 12 .POST("/person", handler::createPerson) // (3) 13 .add(otherRoute) // (4) 14 .build();
| 1 | JSON과 일치하는 Accept 헤더를 가진 GET /person/{id}는<br>PersonHandler.getPerson으로 route됩니다. |
| 2 | JSON과 일치하는 Accept 헤더를 가진 GET /person은<br>PersonHandler.listPeople로 route됩니다. |
| 3 | 추가 predicate가 없는 POST /person은<br>PersonHandler.createPerson에 매핑되며, |
| 4 | otherRoute는 다른 곳에서 생성된 router function이며, 생성된 route에 추가됩니다. |
1import org.springframework.http.MediaType.APPLICATION_JSON 2 3val repository: PersonRepository = ... 4val handler = PersonHandler(repository) 5 6val otherRoute: RouterFunction<ServerResponse> = coRouter { } 7 8val route = coRouter { 9 GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) // (1) 10 GET("/person", accept(APPLICATION_JSON), handler::listPeople) // (2) 11 POST("/person", handler::createPerson) // (3) 12}.and(otherRoute) // (4)
| 1 | JSON과 일치하는 Accept 헤더를 가진 GET /person/{id}는<br>PersonHandler.getPerson으로 route됩니다. |
| 2 | JSON과 일치하는 Accept 헤더를 가진 GET /person은<br>PersonHandler.listPeople로 route됩니다. |
| 3 | 추가 predicate가 없는 POST /person은<br>PersonHandler.createPerson에 매핑되며, |
| 4 | otherRoute는 다른 곳에서 생성된 router function이며, 생성된 route에 추가됩니다. |
router function 그룹이 공통 predicate(예를 들어 공통 path)를 가지는 것은 일반적입니다.
위의 예제에서 공통 predicate는 /person과 일치하는 path predicate이며, 세 개의 route에서 사용됩니다.
어노테이션을 사용할 때는 /person에 매핑되는 type-level @RequestMapping 어노테이션을 사용하여
이 중복을 제거할 수 있습니다.
WebFlux.fn에서는 router function builder의 path 메서드를 통해 path predicate를 공유할 수 있습니다.
예를 들어, 위 예제의 마지막 몇 줄은 nested route를 사용하여 다음과 같이 개선할 수 있습니다:
1RouterFunction<ServerResponse> route = route() 2 .path("/person", builder -> builder // (1) 3 .GET("/{id}", accept(APPLICATION_JSON), handler::getPerson) 4 .GET(accept(APPLICATION_JSON), handler::listPeople) 5 .POST(handler::createPerson)) 6 .build();
| 1 | path의 두 번째 파라미터는 router builder를 받는 consumer라는 점에 유의하세요. |
1val route = coRouter { // (1) 2 "/person".nest { 3 GET("/{id}", accept(APPLICATION_JSON), handler::getPerson) 4 GET(accept(APPLICATION_JSON), handler::listPeople) 5 POST(handler::createPerson) 6 } 7}
| 1 | Coroutines router DSL을 사용하여 router를 생성합니다. router { }를 통한 Reactive 대안도 사용할 수 있습니다. |
path 기반 nesting이 가장 일반적이지만, builder의 nest 메서드를 사용하여 어떤 종류의 predicate에도 nesting할 수 있습니다.
위 예제에는 여전히 공통 Accept 헤더 predicate 형태의 중복이 남아 있습니다.
nest 메서드를 accept와 함께 사용하여 다음과 같이 더 개선할 수 있습니다:
1RouterFunction<ServerResponse> route = route() 2 .path("/person", b1 -> b1 3 .nest(accept(APPLICATION_JSON), b2 -> b2 4 .GET("/{id}", handler::getPerson) 5 .GET(handler::listPeople)) 6 .POST(handler::createPerson)) 7 .build();
1val route = coRouter { 2 "/person".nest { 3 accept(APPLICATION_JSON).nest { 4 GET("/{id}", handler::getPerson) 5 GET(handler::listPeople) 6 POST(handler::createPerson) 7 } 8 } 9}
Router function은 API version에 의한 매칭을 지원합니다.
먼저
WebFlux Config에서 API versioning을 활성화한 다음,
다음과 같이 version predicate를 사용할 수 있습니다:
1RouterFunction<ServerResponse> route = RouterFunctions.route() 2 .GET("/hello-world", version("1.2"), 3 request -> ServerResponse.ok().body("Hello World")).build();
1val route = coRouter { 2 GET("/hello-world", version("1.2")) { 3 ServerResponse.ok().bodyValueAndAwait("Hello World") 4 } 5}
version predicate는 다음과 같을 수 있습니다:
기반 인프라 및 API Versioning 지원에 대한 자세한 내용은 API Versioning을 참고하세요.
WebFlux.fn은 리소스를 제공하기 위한 built-in 지원을 제공합니다.
아래에 설명된 기능 외에도,<br>
RouterFunctions#resource(java.util.function.Function)덕분에 훨씬 더 유연한 리소스 처리 기능을 구현할 수 있습니다.
지정된 predicate와 일치하는 request를 리소스로 redirect하는 것이 가능합니다. 이는 예를 들어 Single Page Application에서 redirect를 처리하는 데 유용할 수 있습니다.
1ClassPathResource index = new ClassPathResource("static/index.html"); 2RequestPredicate spaPredicate = path("/api/**").or(path("/error")).negate(); 3RouterFunction<ServerResponse> redirectToIndex = route() 4 .resource(spaPredicate, index) 5 .build();
1val redirectToIndex = router { 2 val index = ClassPathResource("static/index.html") 3 val spaPredicate = !(path("/api/**") or path("/error")) 4 resource(spaPredicate, index) 5}
주어진 pattern과 일치하는 request를 주어진 root 위치를 기준으로 하는 리소스로 route하는 것도 가능합니다.
1Resource location = new FileUrlResource("public-resources/"); 2RouterFunction<ServerResponse> resources = RouterFunctions.resources("/resources/**", location);
1val location = FileUrlResource("public-resources/") 2val resources = router { resources("/resources/**", location) }
See equivalent in the Servlet stack
router function을 HTTP 서버에서 어떻게 실행할 수 있을까요?
간단한 방법은 router function을 다음 중 하나를 사용하여 HttpHandler로 변환하는 것입니다:
RouterFunctions.toHttpHandler(RouterFunction)RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)그런 다음 반환된 HttpHandler를 사용하여
서버별 지침을 위한 HttpHandler에 따라 여러 서버 어댑터와 함께 사용할 수 있습니다.
Spring Boot에서도 사용되는 보다 일반적인 옵션은
DispatcherHandler 기반 설정을 통해
WebFlux Config에서 실행하는 것이며,
이는 request를 처리하는 데 필요한 컴포넌트를 선언하기 위해 Spring 설정을 사용합니다.
함수형 엔드포인트를 지원하기 위해 WebFlux Java 설정은 다음 인프라 컴포넌트를 선언합니다:
RouterFunctionMapping: Spring 설정에서 하나 이상의 RouterFunction<?> bean을 감지하고,<br>정렬한 후,<br>RouterFunction.andOther를 통해 이를 결합하고, request를 최종적으로 compose된 RouterFunction으로 route합니다.HandlerFunctionAdapter: DispatcherHandler가 request에 매핑된 HandlerFunction을 호출할 수 있도록 하는 간단한 어댑터입니다.ServerResponseResultHandler: HandlerFunction 호출의 결과를 처리하기 위해<br>ServerResponse의 writeTo 메서드를 호출합니다.위의 컴포넌트들은 함수형 엔드포인트가 DispatcherHandler request 처리 lifecycle 안에 들어가도록 해 주며,
선언된 것이 있다면 어노테이션 기반 controller와 함께(잠재적으로) 나란히(side by side) 실행될 수도 있습니다.
이는 Spring Boot WebFlux starter에서 함수형 엔드포인트가 활성화되는 방식이기도 합니다.
다음 예제는 WebFlux Java 설정을 보여줍니다(실행 방법은 DispatcherHandler를 참고하세요):
1@Configuration 2@EnableWebFlux 3public class WebConfig implements WebFluxConfigurer { 4 5 @Bean 6 public RouterFunction<?> routerFunctionA() { 7 // ... 8 } 9 10 @Bean 11 public RouterFunction<?> routerFunctionB() { 12 // ... 13 } 14 15 // ... 16 17 @Override 18 public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { 19 // configure message conversion... 20 } 21 22 @Override 23 public void addCorsMappings(CorsRegistry registry) { 24 // configure CORS... 25 } 26 27 @Override 28 public void configureViewResolvers(ViewResolverRegistry registry) { 29 // configure view resolution for HTML rendering... 30 } 31}
1@Configuration 2@EnableWebFlux 3class WebConfig : WebFluxConfigurer { 4 5 @Bean 6 fun routerFunctionA(): RouterFunction<*> { 7 // ... 8 } 9 10 @Bean 11 fun routerFunctionB(): RouterFunction<*> { 12 // ... 13 } 14 15 // ... 16 17 override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) { 18 // configure message conversion... 19 } 20 21 override fun addCorsMappings(registry: CorsRegistry) { 22 // configure CORS... 23 } 24 25 override fun configureViewResolvers(registry: ViewResolverRegistry) { 26 // configure view resolution for HTML rendering... 27 } 28}
See equivalent in the Servlet stack
routing function builder의 before, after, filter 메서드를 사용하여 handler function을 필터링할 수 있습니다.
어노테이션을 사용할 때는 @ControllerAdvice, ServletFilter 또는 둘 다를 사용하여 유사한 기능을 구현할 수 있습니다.
filter는 builder에 의해 생성된 모든 route에 적용됩니다.
이는 nested route에 정의된 filter가 "top-level" route에는 적용되지 않는다는 의미입니다.
예를 들어, 다음 예제를 살펴보세요:
1RouterFunction<ServerResponse> route = route() 2 .path("/person", b1 -> b1 3 .nest(accept(APPLICATION_JSON), b2 -> b2 4 .GET("/{id}", handler::getPerson) 5 .GET(handler::listPeople) 6 .before(request -> ServerRequest.from(request) // (1) 7 .header("X-RequestHeader", "Value") 8 .build())) 9 .POST(handler::createPerson)) 10 .after((request, response) -> logResponse(response)) // (2) 11 .build();
| 1 | 커스텀 request 헤더를 추가하는 before filter는 두 개의 GET route에만 적용됩니다. |
| 2 | response를 로깅하는 after filter는 nested route를 포함한 모든 route에 적용됩니다. |
1val route = router { 2 "/person".nest { 3 GET("/{id}", handler::getPerson) 4 GET("", handler::listPeople) 5 before { // (1) 6 ServerRequest.from(it) 7 .header("X-RequestHeader", "Value").build() 8 } 9 POST(handler::createPerson) 10 after { _, response -> // (2) 11 logResponse(response) 12 } 13 } 14}
| 1 | 커스텀 request 헤더를 추가하는 before filter는 두 개의 GET route에만 적용됩니다. |
| 2 | response를 로깅하는 after filter는 nested route를 포함한 모든 route에 적용됩니다. |
router builder의 filter 메서드는 HandlerFilterFunction을 받으며,
이는 ServerRequest와 HandlerFunction을 받고 ServerResponse를 반환하는 함수입니다.
handler function 파라미터는 체인에서 다음 element를 나타냅니다.
이는 일반적으로 route된 handler이지만, 여러 filter가 적용된 경우 다른 filter일 수도 있습니다.
이제 특정 path가 허용되는지 여부를 결정할 수 있는 SecurityManager가 있다고 가정하고,
간단한 security filter를 route에 추가할 수 있습니다.
다음 예제는 이를 수행하는 방법을 보여줍니다:
1SecurityManager securityManager = ... 2 3RouterFunction<ServerResponse> route = route() 4 .path("/person", b1 -> b1 5 .nest(accept(APPLICATION_JSON), b2 -> b2 6 .GET("/{id}", handler::getPerson) 7 .GET(handler::listPeople)) 8 .POST(handler::createPerson)) 9 .filter((request, next) -> { 10 if (securityManager.allowAccessTo(request.path())) { 11 return next.handle(request); 12 } 13 else { 14 return ServerResponse.status(UNAUTHORIZED).build(); 15 } 16 }) 17 .build();
1val securityManager: SecurityManager = ... 2 3val route = router { 4 ("/person" and accept(APPLICATION_JSON)).nest { 5 GET("/{id}", handler::getPerson) 6 GET("", handler::listPeople) 7 POST(handler::createPerson) 8 filter { request, next -> 9 if (securityManager.allowAccessTo(request.path())) { 10 next(request) 11 } 12 else { 13 status(UNAUTHORIZED).build() 14 } 15 } 16 } 17}
위 예제는 next.handle(ServerRequest) 호출이 선택 사항임을 보여줍니다.
access가 허용될 때만 handler function이 실행되도록 합니다.
router function builder의 filter 메서드를 사용하는 것 외에도,
RouterFunction.filter(HandlerFilterFunction)을 통해 기존 router function에 filter를 적용할 수 있습니다.
함수형 엔드포인트에 대한 CORS 지원은 전용<br>
CorsWebFilter를 통해 제공됩니다.
Controller Advice
URI Links