Spring Framework
Spring Webflux - HandlerFunction
흥부가귀막혀
2022. 3. 14. 12:50
HandlerFunction
- ServerRequest 를 받아 ServerResponse 를 리턴하는, MVC 의 Controller 와 유사한 로직을 구현하는 함수이다.
- ServerRequest와 ServerResponse는 자바 8 방식으로 HTTP 요청과 응답에 접근할 수 있는 불변(immutable) 인터페이스다.
- 요청, 응답 body 모두 리액티브 스트림 back pressure로 처리한다. request body는 리액터 Flux나 Mono로 표현한다.
- response body는 Flux와 Mono를 포함한 어떤 리액티브 스트림 Publisher든 상관없다. 자세한 정보는 Reactive Libraries 를 참고.
ServerRequest
- ServerRequest로 HTTP 메소드, URI, 헤더, 쿼리 파라미터에 접근할 수 있으며, body를 추출할 수 있는 메소드를 제공한다.
- query 파라미터 조회.
Optional<String> param = serverRequest.queryParam("param");
- path variable 파라미터 조회.
String id = serverRequest.pathVariable("id");
- RequestBody 를 Mono<String> 으로 추출.
Mono<String> string = request.bodyToMono(String.class);
- RequestBody 를 Flux<Person>으로 추출한다. Person 객체는 JSON이나 XML 같은 직렬화된 데이터로 디코딩한다.
Flux<Person> people = request.bodyToFlux(Person.class);
- bodyToMono, bodyToFlux 는 함수형 인터페이스 BodyExtractor를 받는 ServerRequest.body(BodyExtractor) 메소드의 축약 버전이다. BodyExtractors 유틸리티 클래스에 있는 인터페이스를 활용하여 위의 예제를 다음과 같이 사용할 수 있다.
Mono<String> string = request.body(BodyExtractors.toMono(String.class));
Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));
- 이밖에도 다음과 같은 목적에 맞게 request body 를 추출할 수 있는 API 가 제공된다.
form data 추출
Mono<MultiValueMap<String, String> map = request.formData();
multipart data 추출
Mono<MultiValueMap<String, Part> map = request.multipartData();
multiparts 를 streaming 으로 하나씩 추출할 때
Flux<Part> parts = request.body(BodyExtractors.toParts());
ServerResponse
- HTTP 응답은 ServerResponse로 접근할 수 있으며, 이 인터페이스는 불변이기 때문에(immutable) build 메소드로 생성한다. 빌더로 헤더를 추가하거나, 상태 코드, body를 설정할 수 있다.
- JSON 컨텐츠로 200 (OK) 응답
Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);
- body 없이 Location 헤더로만 201 (CREATED) 응답
URI location = ...
ServerResponse.created(location).build();
- hint 파라미터를 넘기면 사용하는 코덱에 따라 body 직렬화/역직렬화 방식을 커스텀할 수 있다. 아래 예는 Jackson JSON view를 지정하는 예제이다.
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
Handler Classes
- HandlerFunction 은 FunctionalInterface 로 정의되어있기 때문에 lamda 형태로 정의가 가능하다.
HandlerFunction<ServerResponse> helloWorld = request -> ServerResponse.ok().bodyValue("Hello World");
- 다만 HandlerFunction 로직이 복잡하거나 많을 경우 lamda 로 정의하는데 부담이 생기기 때문에 일반 class 에 함수를 정의하고 해당 함수를 handler function 으로 묶을 수 있다.
router
@Configuration
public class RouterConfig {
@Bean
public RouterFunction<?> personRouter(PersonRepository repository) {
PersonHandler handler = new PersonHandler(repository);
return RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.build();
}
}
handler class
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
public class PersonHandler {
private final PersonRepository repository;
public PersonHandler(PersonRepository repository) {
this.repository = repository;
}
public Mono<ServerResponse> listPeople(ServerRequest request) { // (1)
Flux<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people, Person.class);
}
public Mono<ServerResponse> createPerson(ServerRequest request) { // (2)
Mono<Person> person = request.bodyToMono(Person.class);
return ok().build(repository.savePerson(person));
}
public Mono<ServerResponse> getPerson(ServerRequest request) { // (3)
int personId = Integer.valueOf(request.pathVariable("id"));
return repository.getPerson(personId)
.flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
.switchIfEmpty(ServerResponse.notFound().build());
}
}
(1) listPeople은 레포지토리에 있는 모든 Person 객체를 JSON으로 반환하는 핸들러 펑션이다.
(2) createPerson은 request body에 있는 Person을 저장하는 핸들러 펑션이다. PersonRepository.savePerson(Person)은 Mono<Void>를 리턴한다는 점에 주의해라. 비어 있는 Mono는 요청 데이터를 읽어 저장하고 나면 완료됐다는 신호를 보낸다. 따라서 이 신호를 받았을 때(즉, Person이 저장됐을 때) 응답을 보내기 위해 build(Publisher<Void>)를 사용한다.
(3) getPerson은 path variable에 있는 id로 식별한 person 객체 하나를 리턴하는 핸들러 펑션이다. 레포지토리에서 Person을 찾으면 JSON 응답을 만든다. 찾지 못했다면 switchIfEmpty(Mono<T>)를 실행해 404 Not Found로 응답한다.
Validation
- 함수형 엔드포인트는 스프링 validation facilities를 사용해서 request body를 검증할 수 있다.
- 다음 예제는 커스텀 스프링 Validator 구현체로 person을 검증한다
public class PersonHandler {
private final Validator validator = new PersonValidator(); // (1)
// ...
public Mono<ServerResponse> createPerson(ServerRequest request) {
Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate); // (2)
return ok().build(repository.savePerson(person));
}
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으로 응답하는 exception을 발생시킨다.