일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |
- Data Structure
- mongoDB
- 운영체제
- design pattern
- 알고리즘
- 네트워크
- 디자인 패턴
- 컴퓨터구조
- IT
- Galera Cluster
- OS
- 백준
- 자바
- Spring
- JavaScript
- 자료구조
- Java
- c언어
- react
- MySQL
- C
- JPA
- spring webflux
- Proxy
- Kafka
- redis
- 파이썬
- MSA
- Heap
- Algorithm
- Today
- Total
시냅스
구현하며 이해하는 Spring WebFlux 본문
Spring WebFlux 의 작동방식을 이해하기 위해 단계별로 진행하는 포스팅입니다.
이 글에서는 Spring WebFlux 를 구현하며 알아봅니다.
이전 글 : https://liltdevs.tistory.com/210
https://liltdevs.tistory.com/189
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 를 해내는 구조입니다.
이제 실제로 구현하며 살펴보겠습니다.
구현
- 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
'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 |