코딩 하는 가든

Spring Webflux의 Functional Endpoints 사용법. (with RouterFunction) 본문

Spring (boot)

Spring Webflux의 Functional Endpoints 사용법. (with RouterFunction)

가든리 2021. 5. 16. 23:32

Spring Webflux에서는 엔드포인트를 매핑 하는 방식으로 기존에 사용하던 어노테이션 방식(@Controller, @RestController)이외에도 Router를 이용한 함수형 방식을 지원 합니다.

이번 글 에서는 웹플럭스에서 Functional Endpoints를 어떻게 사용하는지 알아봅니다.

 

당연한 이야기지만 우선 웹플럭스 의존성이 필요 합니다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
}

 

Functional Endpoint를 사용 하기 위해서는 함수형 인터페이스인 RouterFunction의 구현체를 만들어서 스프링 빈으로 등록 시켜야 합니다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;

import static org.springframework.web.reactive.function.server.RequestPredicates.*;

@Configuration
public class RouterConfig {

    @Bean
    public RouterFunction<ServerResponse> routerExample(PostHandler postHandler) {
        return RouterFunctions.route()                   //1
                .GET("/post/{id}", request -> postHandler.getById(request)) //2
                .POST("/post", postHandler::create)      //3
                .POST("/post/json", accept(MediaType.APPLICATION_JSON), postHandler::createFromJson) //4
                .build(); //5
    }

}

// 1 : RouterFunctions 의 route() 메소드는 RouterFunctionBuilder를 반환 합니다.

        빌더 패턴을 이용하여 RouterFunction을 완성 시킬 수 있습니다.

 

// 2 : GET 메소드로 http GET 메소드가 들어왔을 때를 정의 해 줍니다.

         uri pattern과 함수형 인터페이스를 인자로 받을 수 있습니다.

 

// 3 : POST 메소드 정의. 함수형 인터페이스를 postHandler::create 처럼 축약하여 사용 한 형태.

// 4 : uri pattern과 함수형 인터페이스와 더불어 accept, 혹은 content-type을 명시적으로 써줄 수 있습니다.

// 5: build() 메소드로 빌더 패턴을 완성 시켜 구체 클래스를 만들어 냅니다.

 

 

다음으로 Handler의 구현을 보겠습니다.

@Component
public class PostHandler {

    /**
     * 이번 프로젝트의 주제는 router의 이해이므로
     * 로직은 최대한 간단하게 해서 결과만 받아 볼 수 있도록 구현
     * */

    // path variable 추출
    public Mono<ServerResponse> getById(ServerRequest request) {
        Long id = Long.parseLong(request.pathVariable("id"));
        Post post = new Post(id, "garden", "hello");
        return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(post);
    }

    // x-www-form-urlencoded 추출
    public Mono<ServerResponse> create(ServerRequest request) {
        Mono<MultiValueMap<String, String>> formData = request.formData();

        return formData.flatMap(data -> {
            Map<String, String> dataMap = data.toSingleValueMap();
            String title = dataMap.getOrDefault("title", null);
            String content = dataMap.getOrDefault("content", null);

            Post newPost = new Post(1L, title, content);
            return ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(newPost);
        });
    }

    // json data 추출
    public Mono<ServerResponse> createFromJson(ServerRequest request) {
        Mono<Post> PostMono = request.bodyToMono(Post.class);
        return PostMono.flatMap(post ->
                ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(post));
    }

}

 

Post.java

@AllArgsConstructor
@Setter
@Getter
public class Post {
    private Long id;
    private String title;
    private String content;
}

 

각 메소드는 router에서 넘겨준 것 처럼 ServerRequest 객체를 넘겨 받습니다.

ServerRequest는 Request에 대한 모든 정보를 담고 있으며 클라에서 넘겨준 데이터를 추출 하여 사용 하면 됩니다.

 

pathVariable() 메소드를 통해 "post/{id}" 처럼 들어온 요청의 pathVariable을 추출 할 수 있습니다.

request.pathVariable("id")

formData() 메소드를 통해 x-www-form-urlencoded 형식의 데이터를 추출 할 수 있습니다.

formData는 Mono<Map> 을 반환 합니다.

(multipartData() 를 통해 multipart form-data 도 추출 가능)

Mono<MultiValueMap<String, String>> formData = request.formData();

 

bodyToMono() 메소드를 통해 json 데이터를 Dto로 매핑 시킬 수 있습니다.

Mono<Post> PostMono = request.bodyToMono(Post.class);

 

어노테이션 방식을 이용 하면 @RequestMapping 외에도 @Controller나 @RestController에 공통으로 들어가는 path를 써주는 형태가 있는데 RouterFunctionsBuilder의 path 메소드를 통해 공통 path를 지정 할 수 있습니다.

    @Bean
    public RouterFunction<ServerResponse> nestedRouter(PostHandler postHandler) {
        return RouterFunctions.route()
                .path("/post", builder -> builder
                    .GET("/{id}", postHandler::getById)
                    .POST("", postHandler::create)
                    .POST("/json", postHandler::createFromJson)
                ).build();
    }

 

nest Method를 통해 공통으로 accept를 적용 시킬 수 도 있습니다.

    @Bean
    public RouterFunction<ServerResponse> nestedRouter2(PostHandler postHandler) {
        return RouterFunctions.route()
                .path("/post", builder -> builder
                        .nest(accept(MediaType.APPLICATION_JSON), builder2 -> builder2
                            .POST("/json", postHandler::createFromJson)
//                            .POST("", postHandler::xxx) 이 뒤로 붙는 RequestPredicate는 모두 APPLICATION_JSON이 accept
//                            .PUT("", postHandler::xxx)
                        )
                ).build();
    }

 

글에서 작성된 코드는 아래에서 확인 가능 합니다.

https://github.com/97e57e/BLOG/tree/master/Spring/router-master

 

97e57e/BLOG

블로그 예제 코드 모음. Contribute to 97e57e/BLOG development by creating an account on GitHub.

github.com