거북이 developer

Spring Webflux - HandlerFunction 본문

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을 발생시킨다.

'Spring Framework' 카테고리의 다른 글

Spring Webflux - Server Configuration  (0) 2022.03.14
Spring Webflux - WebClient  (0) 2022.03.14
Spring Webflux - Filter  (0) 2022.03.14
Spring Webflux - Request Mapping  (0) 2022.03.14
Spring Webflux - Overview  (0) 2022.03.14
Comments