시냅스

구현하며 이해하는 Spring WebFlux 본문

Java, Spring

구현하며 이해하는 Spring WebFlux

ted k 2024. 2. 4. 18:46

 

Spring WebFlux 의 작동방식을 이해하기 위해 단계별로 진행하는 포스팅입니다.
이 글에서는 Spring WebFlux 를 구현하며 알아봅니다.

 

 

이전 글 : https://liltdevs.tistory.com/210

 

Spring WebFlux 이해하기 - Reactor

Spring WebFlux 의 작동방식을 이해하기 위해 단계별로 진행하는 포스팅입니다. 이 글에서는 Reactor 에 대해 설명합니다. 이전 글 : https://liltdevs.tistory.com/209 Spring WebFlux 이해하기 - Reactive Streams Spring We

liltdevs.tistory.com

 

https://liltdevs.tistory.com/189

 

구현하며 이해하는 Spring MVC

Spring MVC Spring MVC 는 웹 애플리케이션 개발을 쉽게하기 위해 지원하는 모듈입니다. Model-View-Controller 패턴을 기반으로 FrontController 패턴을 구현한 DispatcherServlet 을 활용하며 매번 Servlet 을 생성하거

liltdevs.tistory.com

 

 

 

Spring WebFlux

 

Spring MVC 는 웹개발을 편리하게 하기위해 지원하는 모듈입니다.

Thread-Per-Request 모델을 따르며, 모든 IO 에 대해 Blocking 되는 특징을 갖고 있습니다.

이러한 특징은 기민한 MSA 환경에서는 분리하게 작용할 수도 있습니다.

 

따라서 Spring 진영에서는 Spring 5 부터 Event Loop Model 을 따르는 Spring WebFlux 를 지원하기 시작했습니다.

Reactive Programming 을 구현한 Reactor 와 Non-Blocking IO 를 지원하는 Http Server 인 Netty,

RDB에 비동기적인 접근을 가능하게 하는 R2DBC 가 주요 스택으로 사용됩니다.

 

 

Spring WebFlux 요청 흐름

 

 

  1. 요청이 들어오면 Netty 등의 서버 엔진을 거쳐 HttpHandler 가 들어오는 요청을 전달 받음
    1. HttpHandler는 Netty 이외의 다양한 서버 엔진에서 지원하는 서버 API 를 추상화
    2. ServerHttpRequest, ServerHttpResponse 를 포함하는 ServerWebExchange 를 생성한 후 WebFilterChain 을 통해 전달
  2. ServerWebExchange 는 WebFilterChain 에서 전처리 과정을 거친 후 WebHandler 인터페이스의 구현체인 DispatcherHandler 에게 전달
  3. DispatcherServlet 과 유사한 DispatcherHandler 는 HandlerMapping List 를 원본 Flux 의 소스로 전달 받음
  4. ServerWebExchange 를 처리할 핸들러를 조회
  5. 조회한 핸들러의 호출은 HandlerAdapter 에게 위임
  6. HandlerAdapter 는 ServerWebExchange 를 처리할 핸들러를 호출
  7. Controller 또는 HandlerFunction 형태의 핸들러에서 요청을 처리한 후 응답 데이터를 리턴
  8. 핸들러로부터 리턴 받은 응답 데이터를 처리할 HandlerResultHandler 를 조회
  9. 조회한 HandlerResultHandler 를 통해 response 로 리턴

 

구성 요소

  • HttpHandler
    • Request 와 Response 를 처리하기 위해 추상화된 단 하나의 메서드만을 가짐
    • HttpWebHandlerAdapter 는 HttpHandler 의 구현체
    • handle 메서드의 파라미터로 전달받은 ServerHttpRequest / ServerHttpResponse 로 ServerWebExchange 를 생성한 후 WebHandler(DispatcherHandler) 호출
  • WebFilter
    • Servlet Filter 처럼 핸들러(annotated, Functional)가 요청을 처리하기 전에 전처리 작업을 할 수 있도록 도와줌
  • HandlerFilterFunction
    • 함수형 기반의 요청 핸들러에 적용할 수 있는 Filter
  • DispatcherHandler
    • WebHandler 인터페이스의 구현체, DispatcherServlet 과 유사하다
    • DispatcherHandler 자체가 Spring Bean 으로 등록되며, Application Context 에서 HandlerMapping, HandlerAdapter, HandlerResultHandler 를 조회
  • HandlerMapping
    • MVC 와 마찬가지로 request 와 handler object 에 대한 매핑을 정의하는 interface
  • HandlerAdapter
    • MVC 와 마찬가지로 Handler object 를 호출하여 실행하고 그 결과를 Mono<HandlerResult> 로 받는다

 

꽤 긴 이야기인 것 같았지만 결국 Spring MVC 와 비슷합니다.

Netty 가 parsing 후 request 를 가져오면, {req, res} 쌍을 ServerWebExchange 로 만들어서 filter 에서 한 번 거르고,

이후 DispatcherHandler 로 들어와 처리할 수 있는 controller 가 있는지 확인하여

adapter 가 실행하여 response 를 해내는 구조입니다.

 

이제 실제로 구현하며 살펴보겠습니다.

 

구현

 

GitHub - taesukang-dev/my-own-spring-webflux: spring webflux 가 구현하는 기능을 이해하기 위해 간단하게 구현

spring webflux 가 구현하는 기능을 이해하기 위해 간단하게 구현하였습니다. Contribute to taesukang-dev/my-own-spring-webflux development by creating an account on GitHub.

github.com

  • DispatcherHandler
  • HandlerMapping
    • AbstractHandlerMethodMapping (RequestMappingHandlerMapping)
    • RouterFunctionMapping

기존 Spring MVC 와 다른 점은 Spring WebFlux 는 Functional Endpoint 를 지원하며

RouterFunction 이 routing 하는 HandlerFunction 을 실행해야 한다는 것입니다.

 

Functional Endpoint 는 기존 @RequestMapping 애노테이션 대신 함수로서 라우팅과 요청을 처리하는 방식을 사용합니다.

HTTP 요청에 대해 HandlerFunction 을 통해 핸들링하는데, 마치 Servlet 의 service(req, res) 와 비슷한 역할을 합니다.

RouterFunction 이 해당하는 URI 에 대한 정의된 routing 규칙을 확인하여 req 를 넘겨주면

HandlerFunction 은 로직을 실행 후 Mono<ServerResponse> 로 응답합니다.

 

  • Functional Endpoint
    • @RequestMapping 애노테이션을 사용하는 URL 경로를 처리하는 대신 라우팅과 요청 처리를 함수로 정의
  • RouterFunction
    • 라우팅 규칙을 정의하고 각 요청에 대한 HandlerFunction 을 반환
  • HandlerFunction
    • RouterFunction 에 의해 반환되는 인터페이스로 실제 요청을 처리하는 로직을 담당
    • ServerRequest 를 인자로 받아 Mono<ServerResponse> 를 반환

실제 구현은 아래에서 코드에서 살펴보도록 하겠습니다.

 

 

HandlerMapping

public interface MyHandlerMapping {
    Mono<?> getHandler(ServerRequest request);
}

 

HandlerMaping 은 getHandler 라는 함수를 가지고 있는 interface 입니다.

Annotated Controller 와 Functional Endpoint 를 함께 지원하기 위해 추상화하여 제공합니다.

 

 

DispatcherHandler

@Slf4j
@Configuration
public class MyDispatcherHandler {

    // MyHandlerMapping 객체들을 저장
    private List<MyHandlerMapping> handlerMappings;

    // DispatcherHandler 의 initStrategies에 대응
    public MyDispatcherHandler(ApplicationContext applicationContext) {
        // applicationContext 에서 MyHandlerMapping 타입의 빈들을 찾아서 handlerMappings 에 저장
        Map<String, MyHandlerMapping> handlerMappingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, MyHandlerMapping.class, true, false);
        this.handlerMappings = new ArrayList<MyHandlerMapping>(handlerMappingBeans.values());
    }

    // request 가 들어오면 handlerMappings 에서 handler 를 찾아서 실행
    public Mono<Void> handle(ServerRequest request) {
        if (this.handlerMappings == null) {
            throw new IllegalStateException("MyDispatcherHandler is not initialized");
        } else {
            return Flux.fromIterable(this.handlerMappings) // handlerMappings 을 data stream 으로 변환
                    .concatMap(mapping -> mapping.getHandler(request)) // handlerMappings 에서 handler 를 찾아서 실행
                    .next() // 첫번째 요소만 가져옴
                    .switchIfEmpty(Mono.error(new IllegalStateException("No matching handler")))
                    .flatMap(handler -> handleRequestWith(request, handler)); // handler 를 실행
        }
    }

    // HandlerFunctionAdapter, RequestMappingHandlerAdapter 에 대응
    // Spring WebFlux 는 handler 를 실행하는 주체로 adapter 를 사용한다, adapter 에게 핸들러를 parameter 로 넘기는 방식
    // adapter 는 handler 를 실행하고 결과를 리턴한다, 예제에서는 handleRequestWith 에서 바로 실행
    private Mono<Void> handleRequestWith(ServerRequest request, Object handler) {
        return ((HandlerFunction<?>) handler).handle(request)
                .doOnNext(response -> {
                    Object body = ((EntityResponse) response).entity();
                    log.info("Response body: {}", body);
                })
                .then();
    }
}

 

FrontController 의 성격을 띄는 DispatcherHandler 입니다.

실제로는 req 에 대한 handler 를 찾으면 HandlerAdapter 가 실행하고

HandlerResultHandler 가 처리결과를 Netty 를 통해 client 에 응답을 반환합니다.

예제에서는 handleRequestWith 라는 함수에서 실행하여 응답을 log 로 출력하였습니다.

 

 

RouterFunctionMapping

@Configuration
public class MyRouterFunctionMapping implements MyHandlerMapping{
    private RouterFunction<?> routerFunction;

    public MyRouterFunctionMapping(ApplicationContext applicationContext) {
        // applicationContext 에서 RouterFunction 타입의 빈들을 찾아서 routerFunction 에 저장
        routerFunction = applicationContext.getBeanProvider(RouterFunction.class)
                            .orderedStream()
                            .toList()
                            .stream().reduce(RouterFunction::andOther)
                            .orElse(null);
    }

    @Override
    public Mono<HandlerFunction<?>> getHandler(ServerRequest request) {
        // routerFunction 에서 request 에 맞는 handler 를 찾아서 실행할 수 있는 HandlerFunction 으로 return
        // routerFunction 은 내부적으로 request 에 맞는 handler 를 찾아서 HandlerFunction 으로 return
        // DefaultRouterFunction.route 에서 찾아서 return
        Mono<? extends HandlerFunction<?>> route = routerFunction.route(request);
        return route.flatMap(Mono::just);
    }
}

 

HandlerMapping 을 implement 하는 RouterFunctionMapping 입니다.

이름에서 유추해볼 수 있듯 Functional Endpoint 에 대해 handler 를 찾아서 반환하는 역할을 하게 됩니다.

앞서 본 DispatcherHandler 에서 getHandler 에서 반환된 handler(HandlerFunction) 을 실행하게 됩니다.

 

 

RequestMappingHandlerMapping

// RequestMappingHandlerMapping -> RequestMappingInfoHandlerMapping -> AbstractHandlerMethodMapping -> AbstractHandlerMapping
// AbstractHandlerMethodMapping, RequestMappingHandlerMapping -> @RequestMapping 을 관리
@Configuration
public class MyRequestMappingHandlerMapping implements MyHandlerMapping{

    // mappingRegistry 에 handler 를 저장
    private Map<Object, Set<Method>> mappingRegistry = new HashMap<>();

    // AbstractHandlerMethodMapping.initHandlerMethods 에 대응
    // applicationContext 에서 @Controller 를 찾아서 mappingRegistry 에 저장
    public MyRequestMappingHandlerMapping(ApplicationContext applicationContext) {
        String[] beanNames = applicationContext.getBeanNamesForType(Object.class);
        int length = beanNames.length;

        for (int i = 0; i < length; i++) {
            String beanName = beanNames[i];
            if (!beanName.startsWith("scopedTarget.")) {
                Class<?> beanType = null;
                try {
                    beanType = applicationContext.getType(beanName);
                } catch (Throwable t) {
                    // ignore
                }

                if (beanType != null && isHandler(beanType)) {
                    // AbstractHandlerMethodMapping, detectHandlerMethods 애 댜웅
                    Class<?> userClass = ClassUtils.getUserClass(beanType);
                    Set<Method> methods = MethodIntrospector.selectMethods(userClass, (ReflectionUtils.MethodFilter) (method) -> {
                        // RequestMappingHandlerMapping, getMappingforMethod 에 대응
                        // 다른 annotation 또한 mapping 이 필요하면 GetMapping -> requestMapping 으로 변경
                        return AnnotatedElementUtils.hasAnnotation(method, GetMapping.class);
                    });

                    // AbstractHandlerMethodMapping, registerHandlerMethod 에 대응
                    Object bean = applicationContext.getBean(beanName);
                    mappingRegistry.put(bean, methods);
                }
            }
        }
    }

    @Override
    public Mono<Object> getHandler(ServerRequest request) {
        // AbstractHandlerMethodMapping, getHandlerInternal 에 대응
        // {lookupHandlerMethod, createHandlerMethod}
        for (Map.Entry<Object, Set<Method>> entry : mappingRegistry.entrySet()) {
            Object bean = entry.getKey();
            Set<Method> methods = entry.getValue();
            for (Method method : methods) {
                if (matches(request, method)) {
                    // HandlerFunction을 생성하고 Mono 로 reutnr, MyDispatcherHandler.handle 에서 lazy evaluation
                    // handle 메소드는 ServerRequest 를 받아 invokeMethod 를 실행
                    return Mono.just((HandlerFunction<ServerResponse>) request1 -> invokeMethod(bean, method, request1));
                }
            }
        }
        return Mono.empty();
    }

	// 아래로 util 성 함수들
    private boolean isHandler(Class<?> beanType) {
        return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
    }

    private boolean matches(ServerRequest request, Method method) {
        GetMapping getMapping = method.getAnnotation(GetMapping.class);
        if (getMapping != null) {
            String path = getMapping.value()[0];
            return request.path().equals(path);
        }
        return false;
    }

    private Mono<ServerResponse> invokeMethod(Object bean, Method method, ServerRequest request) {
        try {
            Object result = method.invoke(bean);
            if (result instanceof Mono) {
                return ((Mono) result).flatMap(res -> {
                    if (res instanceof ServerResponse) {
                        return Mono.just((ServerResponse) res);
                    } else {
                        return ServerResponse.ok().bodyValue(res);
                    }
                });
            } else {
                return ServerResponse.ok().bodyValue(result);
            }
        } catch (IllegalAccessException | InvocationTargetException e) {
            return Mono.error(e);
        }
    }

}

 

RequestMappingHandlerMapping 은 AbstractHandlerMethodMapping 을 상속받아 구현되며

Annotated Controller ( @Controller ) 를 대응하기 위해 만들어진 class 입니다.

application context 에서 handler 에 해당하는 class 들을 가져와서 mappingRegistry 에 저장하여

요청이 올 때마다 mappingRegistry 에서 찾아 HandlerFunction 으로 반환하여 실행할 수 있게 합니다.

위와 같은 절차 덕분에 Spring WebFlux 에서도 Annotated Controller 를 사용할 수 있습니다.

 

예제에서는 간소화를 위해 MappingRegistry 객체를 따로 만들지 않았고, GetMapping 에 대해서만 mapping 하였습니다.

 

 

테스트

@SpringBootApplication
public class MyOwnSpringWebFluxApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyOwnSpringWebFluxApplication.class, args);
    }
}

@RestController
class TestRestController {

    @GetMapping("/test2")
    public Mono<ServerResponse> test() {
        return ServerResponse
                .ok()
                .bodyValue("Hello, RestController!");
    }
}

@Configuration
class TestRouter {
    @Bean
    public RouterFunction<?> route() {
        return RouterFunctions.route()
                .GET("/test", request -> ServerResponse.ok().bodyValue("Hello, Router!"))
                .build();
    }
}

 

RouterFunction 과 Controller 로 생성하였습니다.

 

@SpringBootTest
class MyDispatcherHandlerTest {

    @Autowired
    ApplicationContext applicationContext;

    @Test
    void functional() {
        ServerRequest request = ServerRequest.create(MockServerWebExchange
                                                        .from(MockServerHttpRequest.get("/test").build())
                                                        , HandlerStrategies.withDefaults().messageReaders());

        Mono<Void> result = new MyDispatcherHandler(applicationContext)
                .handle(request);

        StepVerifier.create(result)
                .expectSubscription()
                .verifyComplete();
    }

    @Test
    void annotated() {
        ServerRequest request = ServerRequest.create(MockServerWebExchange
                                                        .from(MockServerHttpRequest.get("/test2").build())
                                                        , HandlerStrategies.withDefaults().messageReaders());

        Mono<Void> result = new MyDispatcherHandler(applicationContext)
                .handle(request);

        StepVerifier.create(result)
                .expectSubscription()
                .verifyComplete();
    }

}

 

Netty 가 없기 때문에 (정확히는 Spring 에 있겠지만 제가 구현한 것이 아니기 때문에)

직접 DispatcherHandler 를 생성해서 호출하도록 하겠습니다.

 

위의 코드가 엄밀한 테스트는 아니지만,

DispatcherHandler 에서 handleRequestWith 로 response 를 log 로 출력하고 있기 때문에

complete 이 되는지, 그리고 console log 가 찍히는지 확인해보겠습니다.

 

 

정상적으로 실행되는 것을 확인하였습니다!

 

Spring WebFlux 의 개괄적인 동작방식을 코드를 통하여 알아보았습니다.

Spring MVC 와 비슷하면서도, Functional Endpoint 를 통해 추가적인 라우팅을 지원한다는 것을 코드로 확인하였습니다.

다음 글은 Network IO에 Non-Blocking 을 지원하는 Netty 를 알아보도록 하겠습니다.

 

위의 글에 추가적인 피드백이 필요하다면 댓글로 알려주세요!

 

끝!

 

참고

https://docs.spring.io/spring-framework/reference/web/webflux.html

 

Spring WebFlux :: Spring Framework

The original web framework included in the Spring Framework, Spring Web MVC, was purpose-built for the Servlet API and Servlet containers. The reactive-stack web framework, Spring WebFlux, was added later in version 5.0. It is fully non-blocking, supports

docs.spring.io

 

Comments