RounterFunction

2024. 10. 13. 20:51Spring Framework/Web on Servlet Stack

Spring Web MVC의 RouterFunction은 HTTP 요청을 적절한 HandlerFunction에 라우팅하는 데 사용되는 함수형 프로그래밍 방식의 요소입니다. 이 모델은 애너테이션 기반 방식의 대안으로, 함수형 프로그래밍 스타일을 지원하여 라우팅과 요청 처리를 더 유연하게 할 수 있습니다. WebMvc.fn에서 사용되며, Spring WebFlux의 Reactive Stack과 유사한 구조를 가지고 있습니다.

RouterFunction의 개념과 사용법

RouterFunction이란?

  • RouterFunction은 HTTP 요청을 적절한 HandlerFunction으로 라우팅하는 함수입니다.
  • @RequestMapping을 사용한 애너테이션 기반 라우팅과 동일한 역할을 하지만, 함수형 스타일로 요청을 라우팅합니다.
  • 요청을 라우팅할 때는 RequestPredicate을 사용하여 경로, HTTP 메서드, 헤더 등 다양한 조건을 적용할 수 있습니다.

RouterFunctions.route() 메서드는 라우터 함수 빌더를 제공하며, 이를 통해 여러 가지 라우팅 규칙을 간결하게 정의할 수 있습니다.

RouterFunction을 정의하는 방법

1. 기본적인 RouterFunction 정의

RouterFunction<ServerResponse> route = RouterFunctions.route()
    .GET("/hello-world", accept(MediaType.TEXT_PLAIN),
        request -> ServerResponse.ok().body("Hello World"))
    .build();
  • GET("/hello-world"): HTTP GET 요청이 /hello-world 경로로 들어오면 매칭됩니다.
  • accept(MediaType.TEXT_PLAIN): Accept 헤더가 text/plain인 경우에만 매칭됩니다.
  • HandlerFunction은 요청을 처리하고 응답을 리턴하는 역할을 합니다. 여기서는 간단히 "Hello World"라는 문자열을 응답으로 보내고 있습니다.

2. RequestPredicate 사용

RequestPredicate는 요청의 경로, HTTP 메서드, 헤더 등의 조건을 정의하는데 사용됩니다. RequestPredicates 유틸리티 클래스를 통해 자주 사용되는 다양한 조건을 미리 제공받을 수 있습니다.

  • RequestPredicate.and(RequestPredicate): 두 가지 조건이 모두 일치해야 요청이 매칭됩니다.
  • RequestPredicate.or(RequestPredicate): 두 가지 조건 중 하나만 일치해도 요청이 매칭됩니다.
RouterFunction<ServerResponse> route = RouterFunctions.route()
    .GET("/hello-world", accept(MediaType.TEXT_PLAIN).and(method(HttpMethod.GET)),
        request -> ServerResponse.ok().body("Hello World"))
    .build();

위 코드는 GET 요청과 text/plain 헤더를 가진 요청만 매칭합니다.

3. 라우터 함수 빌더와 추가적인 요청 조건

RouterFunctions.route()를 사용한 라우터 함수 빌더는 다양한 HTTP 메서드에 대한 간편한 매핑을 제공합니다.

  • GET(String path, HandlerFunction handler): GET 요청을 특정 경로에 매핑합니다.
  • POST(String path, HandlerFunction handler): POST 요청을 특정 경로에 매핑합니다.

RouterFunction의 조합

RouterFunction은 서로 조합할 수 있습니다. 여러 라우터 함수를 하나로 합쳐서 복잡한 라우팅 규칙을 만들 수 있습니다. 이를 위해 RouterFunction.and(), RouterFunction.andRoute() 등의 메서드를 사용할 수 있습니다.

4. 라우터 함수 조합

RouterFunction<ServerResponse> otherRoute = ...;

RouterFunction<ServerResponse> route = RouterFunctions.route()
    .GET("/person/{id}", accept(MediaType.APPLICATION_JSON), handler::getPerson)
    .GET("/person", accept(MediaType.APPLICATION_JSON), handler::listPeople)
    .POST("/person", handler::createPerson)
    .add(otherRoute)  // 다른 RouterFunction 추가
    .build();
  • add(otherRoute): 다른 라우터 함수를 추가하여 라우팅 규칙을 확장할 수 있습니다.
  • GET, POST 메서드를 통해 각 경로에 요청을 라우팅하는 규칙을 정의합니다.

라우팅 순서와 우선순위

RouterFunction은 정의된 순서대로 평가됩니다. 가장 먼저 정의된 라우터가 먼저 매칭을 시도하고, 매칭되지 않으면 다음 라우터가 평가됩니다. 따라서 더 구체적인 라우팅 규칙을 먼저 정의하고, 일반적인 규칙을 나중에 정의하는 것이 중요합니다.

예를 들어:

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

위에서 /person/{id} 경로가 /person 경로보다 먼저 평가됩니다. 이는 /person 경로가 더 일반적인 규칙이기 때문에, 특정 ID를 가진 /person/{id}가 먼저 평가되는 것이 바람직하기 때문입니다.

라우트 중첩 (Nested Routes)

라우트 중첩은 공통되는 경로나 조건을 묶어서 라우터의 중복을 줄이는 데 유용합니다. 중첩된 경로를 정의하여 코드의 간결성을 높일 수 있습니다. 이는 path() 메서드를 사용하여 구현할 수 있습니다.

5. 중첩된 경로 정의

RouterFunction<ServerResponse> route = RouterFunctions.route()
    .path("/person", builder -> builder
        .GET("/{id}", accept(MediaType.APPLICATION_JSON), handler::getPerson)
        .GET(accept(MediaType.APPLICATION_JSON), handler::listPeople)
        .POST(handler::createPerson))
    .build();
  • path("/person"): /person 경로를 공유하는 모든 요청에 대해 하위 경로(/person/{id}, /person)를 정의할 수 있습니다.
  • 중복된 경로를 묶어서 코드의 반복을 줄이고, 구조적으로 더 명확한 라우팅을 구현할 수 있습니다.

중첩된 라우트와 복합 조건

nest() 메서드를 사용하여 특정 조건(예: Accept 헤더)이 동일한 경로들을 묶을 수 있습니다. 이는 중첩된 경로에서 자주 사용하는 헤더나 조건을 함께 사용할 때 유용합니다.

6. nest() 메서드를 사용한 복합 조건

RouterFunction<ServerResponse> route = RouterFunctions.route()
    .path("/person", b1 -> b1
        .nest(accept(MediaType.APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET(handler::listPeople))
        .POST(handler::createPerson))
    .build();
  • nest(accept(MediaType.APPLICATION_JSON)): 공통된 Accept 헤더가 application/json인 경로들을 묶어서 정의합니다.
  • 이를 통해 조건을 한 번만 선언하고, 하위의 여러 경로에 적용할 수 있습니다.

RouterFunction을 Spring Bean으로 등록하기

RouterFunction은 일반적으로 Spring Bean으로 등록됩니다. 이렇게 하면 Spring이 자동으로 라우터를 감지하고 설정할 수 있습니다. 이를 위해 @Configuration 클래스를 사용하여 RouterFunction을 Bean으로 등록할 수 있습니다.

7. RouterFunction을 Spring Bean으로 등록

@Configuration
public class RouterConfig {

    private final PersonHandler handler;

    public RouterConfig(PersonHandler handler) {
        this.handler = handler;
    }

    @Bean
    public RouterFunction<ServerResponse> personRoutes() {
        return RouterFunctions.route()
            .GET("/person/{id}", accept(MediaType.APPLICATION_JSON), handler::getPerson)
            .GET("/person", accept(MediaType.APPLICATION_JSON), handler::listPeople)
            .POST("/person", handler::createPerson)
            .build();
    }
}
  • @Configuration 클래스에서 라우터 함수 정의를 Spring Bean으로 등록합니다.
  • @Bean을 통해 정의된 라우터 함수는 Spring이 자동으로 인식하고, DispatcherServlet에 의해 사용됩니다.

결론

  • RouterFunction은 함수형 스타일로 HTTP 요청을 라우팅하는 기능을 제공합니다.
  • 다양한 RequestPredicate를 사용해 HTTP 메서드, 경로, 헤더 등에 대한 조건을 적용할 수 있습니다.
  • 여러 라우터 함수를 조합하거나 중첩하여 더 복잡한 라우팅을 간결하게 구현할 수 있습니다.
  • 라우터 함수는 Spring Bean으로 등록되어 DispatcherServlet에서 자동으로 감지되고 동작합니다.

이 방식은 기존의 애너테이션 기반 라우팅보다 더 유연하고, 함수형 프로그래밍 스타일을 선호하는 개발자들에게는 직관적이고 효율적인 대안이 될 수 있습니다.