| 일 | 월 | 화 | 수 | 목 | 금 | 토 | 
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 | 
| 9 | 10 | 11 | 12 | 13 | 14 | 15 | 
| 16 | 17 | 18 | 19 | 20 | 21 | 22 | 
| 23 | 24 | 25 | 26 | 27 | 28 | 29 | 
| 30 | 
- c언어
 - Galera Cluster
 - MySQL
 - redis
 - 네트워크
 - Data Structure
 - JPA
 - Proxy
 - 알고리즘
 - 자료구조
 - C
 - Java
 - react
 - 자바
 - Heap
 - IT
 - 운영체제
 - 컴퓨터구조
 - Algorithm
 - OS
 - JavaScript
 - 백준
 - Kafka
 - Spring
 - spring webflux
 - 디자인 패턴
 - 파이썬
 - mongoDB
 - design pattern
 - MSA
 
- Today
 
- Total
 
시냅스
구현하며 이해하는 Spring WebFlux 본문
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 요청 흐름

- 요청이 들어오면 Netty 등의 서버 엔진을 거쳐 HttpHandler 가 들어오는 요청을 전달 받음
- HttpHandler는 Netty 이외의 다양한 서버 엔진에서 지원하는 서버 API 를 추상화
 - ServerHttpRequest, ServerHttpResponse 를 포함하는 ServerWebExchange 를 생성한 후 WebFilterChain 을 통해 전달
 
 - ServerWebExchange 는 WebFilterChain 에서 전처리 과정을 거친 후 WebHandler 인터페이스의 구현체인 DispatcherHandler 에게 전달
 - DispatcherServlet 과 유사한 DispatcherHandler 는 HandlerMapping List 를 원본 Flux 의 소스로 전달 받음
 - ServerWebExchange 를 처리할 핸들러를 조회
 - 조회한 핸들러의 호출은 HandlerAdapter 에게 위임
 - HandlerAdapter 는 ServerWebExchange 를 처리할 핸들러를 호출
 - Controller 또는 HandlerFunction 형태의 핸들러에서 요청을 처리한 후 응답 데이터를 리턴
 - 핸들러로부터 리턴 받은 응답 데이터를 처리할 HandlerResultHandler 를 조회
 - 조회한 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
'Java, Spring' 카테고리의 다른 글
| Spring 으로 구현하는 선착순 쿠폰 발급 시스템 (+ Redis, Kafka) (5) | 2024.07.20 | 
|---|---|
| Spring WebFlux 에서 ProxySQL 을 사용할 때 문제점 (1) | 2024.02.07 | 
| Spring WebFlux 이해하기 - Reactor (1) | 2024.02.03 | 
| Spring WebFlux 이해하기 - Reactive Streams (0) | 2024.02.03 | 
| JVM, Spring 에서의 시스템 변수와 환경변수 이해 (0) | 2023.11.28 |