함수형 앤드포인트

김기현·2025년 8월 3일

Spring WebFlux

목록 보기
13/28

스프링 WebFlux는 웹 애플리케이션을 구축하는 두 가지 주요 모델을 제공하는데, 애노테이션 기반 프로그래밍 모델과 함수형 엔드포인트 프로그래밍 모델이다.
함수형 엔드포인트는 스프링 MVC의 애노테이션 기반 컨트롤러와 달리, 웹 요청을 처리하는 로직을 함수형 프로그래밍 스타일로 정의하는 방식이다.
즉, HandlerFunctionRouterFunction이라는 두 가지 핵심 인터페이스를 사용하여 요청 라우팅과 처리를 직접 구성한다.


핵심 구성 요소

HandlerFunction<T>

@FunctionalInterface
public interface HandlerFunction<T extends ServerResponse> {
    T handle(ServerRequest request) throws Exception;
}
  • 이름에서 알 수 있듯이 요청을 핸들링(처리)하는 함수이다.
  • 애노테이션 기반 컨트롤러의 @RequestMapping메소드와 유사한 역할을 한다.
  • ServerRequest객체를 입력으로 받고, Mono<ServerResponse>를 반환한다.
    • ServerRequest는 HTTP 요청에 대한 모든 정보(헤더, 바디, 쿼리 파라미터 등)를 담고 있다.
    • ServerResponse는 클라이언트에게 보낼 HTTP 응답을 나타낸다.
  • 주로 람다 표현식으로 구현된다.
public class MyHandlers {
    public Mono<ServerResponse> hello(ServerRequest request) {
        return ServerResponse.ok()
                .bodyValue("Hello");
    }

    public Mono<ServerResponse> goodbye(ServerRequest request) {
        String name = request.queryParam("name").orElse("Guest");
        
        return ServerResponse.ok()
                .bodyValue("Goodbye " + name);
    }
}

RouterFunction<T>

@FunctionalInterface
public interface RouterFunction<T extends ServerResponse> {
    Optional<HandlerFunction<T>> route(ServerRequest request);

    default RouterFunction<T> and(RouterFunction<T> other) {
        return new RouterFunctions.SameComposedRouterFunction(this, other);
    }

    default RouterFunction<?> andOther(RouterFunction<?> other) {
        return new RouterFunctions.DifferentComposedRouterFunction(this, other);
    }

    default RouterFunction<T> andRoute(RequestPredicate predicate, HandlerFunction<T> handlerFunction) {
        return this.and(RouterFunctions.route(predicate, handlerFunction));
    }

    default RouterFunction<T> andNest(RequestPredicate predicate, RouterFunction<T> routerFunction) {
        return this.and(RouterFunctions.nest(predicate, routerFunction));
    }

    default <S extends ServerResponse> RouterFunction<S> filter(HandlerFilterFunction<T, S> filterFunction) {
        return new RouterFunctions.FilteredRouterFunction(this, filterFunction);
    }

    default void accept(RouterFunctions.Visitor visitor) {
        visitor.unknown(this);
    }

    default RouterFunction<T> withAttribute(String name, Object value) {
        Assert.hasLength(name, "Name must not be empty");
        Assert.notNull(value, "Value must not be null");
        Map<String, Object> attributes = new LinkedHashMap();
        attributes.put(name, value);
        return new RouterFunctions.AttributesRouterFunction(this, attributes);
    }

    default RouterFunction<T> withAttributes(Consumer<Map<String, Object>> attributesConsumer) {
        Assert.notNull(attributesConsumer, "AttributesConsumer must not be null");
        Map<String, Object> attributes = new LinkedHashMap();
        attributesConsumer.accept(attributes);
        return new RouterFunctions.AttributesRouterFunction(this, attributes);
    }
}
  • 들어오는 요청을 어떤 HandlerFunction으로 라우팅(매핑)할지 결정하는 함수이다.
  • 애노테이션 기반 컨트롤러의 @RequestMapping 애노테이션과 유사한 역할을 하지만 더 유연하고 프로그래밍 방식으로 라우팅 규칙을 정의할 수 있다.
  • ServerRequest객체를 입력받고, Mono<HandlerFunction<t>>를 반환한다.
    • 요청에 매핑되는 핸들러가 없으면 Mono.empty()를 반환한다.
  • 주로 RouterFunctions.route()팩토리 메소드를 사용하여 정의한다.
@Configuration
public class MyRoutingConfiguration {

    @Bean
    public RouterFunction<ServerResponse> myRoutes(MyHandlers myHandlers) {
        return route(GET("/hello"), myHandlers::hello) // GET /hello 요청을 myHandlers.hello 메서드에 매핑
                .andRoute(GET("/goodbye").and(queryParam("name", value -> !value.isEmpty())), myHandlers::goodbye) // 값이 비어있지 않은지 검증한다.
                .andRoute(POST("/echo"), request -> request.bodyToMono(String.class) // POST /echo 요청
                        .flatMap(body -> ServerResponse.ok().bodyValue("Echo: " + body)));
    }
}

함수형 앤드포인트의 장점

1. 높은 유연성 및 재사용성

  • 라우팅 규칙을 코드(함수)로 직접 정의하므로, 조건부 라우팅, 동적 라우팅 등 복잡하고 유연한 라우팅 로직을 쉽게 구현할 수 있다.
  • HandlerFunction은 순수 함수에 가깝게 작성될 수 있으므로 단위 테스트가 용이하고 재사용성이 높다.

2. 명확한 관심사 분리

  • RouterFunction은 요청을 어떤 핸들러롤 보낼지 결정하는 라우팅 역할에만 집중한다.
  • HandlerFunction은 실제 요청을 처리하고 응답을 생성하는 역할에만 집중한다.
  • 이러한 분리는 코드의 가독성을 높이고 유지보수를 용이하게 한다.

3. 애노테이션 오버헤드 없음

  • 컴파일 타임에 애노테이션 프로세싱이 필요 없어 이론적으로는 약간 더 가볍다.
  • 하지만 대부분의 애플리케이션에서는 성능 차이가 없거나 미미하다.

4. 함수형 프로그래밍 패러다임에 대한 일관성

  • 스프링 WebFlux의 핵심인 Reacotr 라이브러리의 MonoFlux가 함수형/리액티브 패더다임을 따르듯이 함수형 앤드포인트도 이와 일관된 프로그래밍 스타일을 제공한다.
  • 이는 전체 코드베이스의 응집력을 높일 수 있다.

5. 테스트 용이성

  • HandlerFunction은 일반적인 Java 메소드처럼 테스트할 수 있다.
  • Mock 객체나 스프링 컨텍스트 로딩 없이도 테스트가 가능하여 테스트 속도가 빠르다.

언제 함수형 앤드포인트를 사용하는 게 좋을까

  • 마이크로서비스: 가볍고 유연한 API를 빠르게 구축해야 하는 마이크로서비스에 적합하다.
  • API Gateway: 복잡한 라우팅 규칙이나 동적인 라우팅이 필요한 API 게이트웨이에 활용될 수 있다.
  • 순수 함수형 프로그래밍 스타일 선호: 팀이 함수형 프로그래밍에 익숙하고, 애플리케이션 전체에 걸쳐 일관된 함수형 스타일을 적용하고 싶을 때.
  • 성능/최적화: 매우 미세한 수준의 성능 최적화가 필요하고, 애노테이션 오버헤드조차 없애고자 할 때.
  • 간단한 유틸리티 API: 비즈니스 로직이 매우 단순하고 라우팅 규칙이 직관적인 경우.
profile
백엔드 개발자를 목표로 공부하는 대학생

0개의 댓글