티스토리 뷰

1. 어플리케이션 구동
Spring Boot 애플리케이션이 구동되면, Spring의 ApplicationContext가 초기화되고, 이를 통해 애플리케이션의 빈들이 생성됩니다. Spring WebFlux 애플리케이션의 경우, 주로 AnnotationConfigReactiveWebServerApplicationContext 객체가 사용됩니다. 이 객체는 WebFlux 관련 빈들을 초기화하고 관리합니다.
- 애플리케이션 실행: SpringApplication.run() 호출로 애플리케이션이 시작됩니다.
- ApplicationContext 초기화 및 빈 주입 :
- AnnotationConfigReactiveWebServerApplicationContext 구현체로 어노테이션 기반으로 모든 빈 주입
2. WebFlux 주요 Bean
a. NettyWebServer ( implements WebServer )
- Netty기반으로 만들어진 HttpServer를 띄우고, 요청을 DispatcherHandler 에 넘긴다.
- HTTP요청을 HttpServerRequest로 파싱.
package org.springframework.boot.web.embedded.netty;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import org.springframework.boot.web.embedded.AbstractEmbeddedWebServer;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.WebServerException;
import org.springframework.boot.web.server.WebServerInitializer;
import org.springframework.util.Assert;
import reactor.netty.http.server.HttpServer;
public class NettyWebServer extends AbstractEmbeddedWebServer {
private final HttpServer httpServer;
private final EventLoopGroup eventLoopGroup;
private Channel channel;
public NettyWebServer(HttpServer httpServer) {
Assert.notNull(httpServer, "HttpServer must not be null");
this.httpServer = httpServer;
this.eventLoopGroup = new NioEventLoopGroup();
}
@Override
public void start() throws WebServerException {
try {
this.httpServer
.run((httpServer -> {
// 초기화된 HttpServer로 서버를 시작
this.channel = httpServer.bindNow();
this.channel.onClose()
.doFinally(signalType -> this.stop());
}));
} catch (Exception ex) {
throw new WebServerException("Failed to start the server", ex);
}
}
@Override
public void stop() {
if (this.channel != null) {
this.channel.close();
}
this.eventLoopGroup.shutdownGracefully();
}
@Override
public int getPort() {
return this.channel != null ? ((InetSocketAddress) this.channel.localAddress()).getPort() : -1;
}
@Override
public WebServer initialize(WebServerInitializer initializer) {
// 초기화 과정에서 필요한 작업을 처리
return this;
}
public static class NettyServerBuilder {
private final HttpServer httpServer;
public NettyServerBuilder() {
this.httpServer = HttpServer.create();
}
public NettyWebServer build() {
return new NettyWebServer(this.httpServer);
}
public NettyServerBuilder port(int port) {
this.httpServer.port(port);
return this;
}
public NettyServerBuilder handler(WebHandler handler) {
this.httpServer.handle((request, response) -> handler.handle(request, response));
return this;
}
public NettyServerBuilder logging(LogLevel logLevel) {
this.httpServer.doOnBound((server) -> server.tcpConfiguration(tcp -> tcp.option(ChannelOption.SO_RCVBUF, 1024))
.doOnChannelInit((channel) -> channel.pipeline().addLast(new LoggingHandler(logLevel))));
return this;
}
}
}
b. DispatcherHandler (implements HttpHandler)
- 요청에 맞는 HandlerMapping 탐색 => 요청 처리
- WebMVC의 DispatcherServlet과 유사
package org.springframework.web.reactive;
import java.util.Collections;
import java.util.List;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.lang.Nullable;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.handler.ExceptionHandlingWebHandler;
/**
* Spring WebFlux의 핵심 `WebHandler` 구현체로,
* HTTP 요청을 적절한 핸들러(Controller)로 매핑하고 실행함.
*
* Spring MVC의 `DispatcherServlet`과 같은 역할을 함.
*/
public class DispatcherHandler implements WebHandler, ApplicationContextAware, InitializingBean {
@Nullable
private List<HandlerMapping> handlerMappings;
@Nullable
private List<HandlerAdapter> handlerAdapters;
@Nullable
private List<WebExceptionHandler> exceptionHandlers;
@Nullable
private ApplicationContext applicationContext;
/**
* `ApplicationContextAware` 인터페이스 구현 (Spring 컨텍스트 주입)
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
/**
* `InitializingBean` 구현체로, Bean 생성 후 자동으로 실행됨.
* `HandlerMapping`, `HandlerAdapter`, `WebExceptionHandler`를 가져옴.
*/
@Override
public void afterPropertiesSet() {
if (this.applicationContext == null) {
throw new IllegalStateException("ApplicationContext가 설정되지 않았습니다.");
}
this.handlerMappings = initStrategies(this.applicationContext, HandlerMapping.class);
this.handlerAdapters = initStrategies(this.applicationContext, HandlerAdapter.class);
this.exceptionHandlers = initStrategies(this.applicationContext, WebExceptionHandler.class);
}
/**
* HTTP 요청을 처리하는 핵심 메서드
* @param exchange HTTP 요청 및 응답을 포함하는 ServerWebExchange 객체
*/
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
return getHandler(exchange)
.flatMap(handler -> getHandlerAdapter(handler)
.flatMap(adapter -> adapter.handle(exchange, handler)))
.switchIfEmpty(Mono.defer(() -> handleNoMatch(exchange)))
.onErrorResume(ex -> handleDispatchException(exchange, ex));
}
/**
* `HandlerMapping`을 통해 요청을 처리할 적절한 핸들러 찾기
*/
private Mono<Object> getHandler(ServerWebExchange exchange) {
return Flux.fromIterable(this.handlerMappings != null ? this.handlerMappings : Collections.emptyList())
.concatMap(mapping -> mapping.getHandler(exchange))
.next();
}
/**
* `HandlerAdapter`를 통해 적절한 핸들러 실행
*/
private Mono<HandlerAdapter> getHandlerAdapter(Object handler) {
return Flux.fromIterable(this.handlerAdapters != null ? this.handlerAdapters : Collections.emptyList())
.filter(adapter -> adapter.supports(handler))
.next()
.switchIfEmpty(Mono.error(() -> new IllegalStateException("HandlerAdapter를 찾을 수 없습니다: " + handler)));
}
/**
* 핸들러를 찾을 수 없을 때 (404 Not Found)
*/
private Mono<Void> handleNoMatch(ServerWebExchange exchange) {
exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND);
return exchange.getResponse().setComplete();
}
/**
* 요청 처리 중 예외 발생 시 `WebExceptionHandler`를 통해 처리
*/
private Mono<Void> handleDispatchException(ServerWebExchange exchange, Throwable ex) {
return Flux.fromIterable(this.exceptionHandlers != null ? this.exceptionHandlers : Collections.emptyList())
.concatMap(handler -> handler.handle(exchange, ex))
.next();
}
/**
* ApplicationContext에서 특정 전략 빈 리스트를 가져오는 메서드
*/
private <T> List<T> initStrategies(ApplicationContext context, Class<T> strategyType) {
return context.getBeanProvider(strategyType).orderedStream().toList();
}
}
c. HandlerMapping:
- RouterFunction 기반, 어노테이션 기반으로 된 요청정보 <-> 핸들러정보 가지고있는 객체
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.reactive.handler;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import org.springframework.beans.BeansException;
import org.springframework.util.CollectionUtils;
/**
* Implementation of the {@link org.springframework.web.reactive.HandlerMapping}
* interface to map from URLs to request handler beans. Supports both mapping
* to bean instances and mapping to bean names; the latter is required for
* non-singleton handlers.
*
* <p>The "urlMap" property is suitable for populating the handler map with
* bean instances. Mappings to bean names can be set via the "mappings"
* property, in a form accepted by the {@code java.util.Properties} class,
* as follows:
*
* <pre class="code">
* /welcome.html=ticketController
* /show.html=ticketController</pre>
*
* <p>The syntax is {@code PATH=HANDLER_BEAN_NAME}. If the path doesn't begin
* with a slash, one is prepended.
*
* <p>Supports direct matches, for example, a registered "/test" matches "/test", and
* various Ant-style pattern matches, for example, a registered "/t*" pattern matches
* both "/test" and "/team", "/test/*" matches all paths under "/test",
* "/test/**" matches all paths below "/test". For details, see the
* {@link org.springframework.web.util.pattern.PathPattern} javadoc.
*
* @author Rossen Stoyanchev
* @author Sam Brannen
* @since 5.0
*/
public class SimpleUrlHandlerMapping extends AbstractUrlHandlerMapping {
private final Map<String, Object> urlMap = new LinkedHashMap<>();
/**
* Create a {@code SimpleUrlHandlerMapping} with default settings.
*/
public SimpleUrlHandlerMapping() {
}
/**
* Create a {@code SimpleUrlHandlerMapping} using the supplied URL map.
* @param urlMap map with URL paths as keys and handler beans (or handler
* bean names) as values
* @since 5.2
* @see #setUrlMap(Map)
*/
public SimpleUrlHandlerMapping(Map<String, ?> urlMap) {
setUrlMap(urlMap);
}
/**
* Create a {@code SimpleUrlHandlerMapping} using the supplied URL map and order.
* @param urlMap map with URL paths as keys and handler beans (or handler
* bean names) as values
* @param order the order value for this {@code SimpleUrlHandlerMapping}
* @since 5.2
* @see #setUrlMap(Map)
* @see #setOrder(int)
*/
public SimpleUrlHandlerMapping(Map<String, ?> urlMap, int order) {
setUrlMap(urlMap);
setOrder(order);
}
/**
* Map URL paths to handler bean names.
* This is the typical way of configuring this HandlerMapping.
* <p>Supports direct URL matches and Ant-style pattern matches. For syntax details,
* see the {@link org.springframework.web.util.pattern.PathPattern} javadoc.
* @param mappings properties with URLs as keys and bean names as values
* @see #setUrlMap
*/
public void setMappings(Properties mappings) {
CollectionUtils.mergePropertiesIntoMap(mappings, this.urlMap);
}
/**
* Set a Map with URL paths as keys and handler beans (or handler bean names)
* as values. Convenient for population with bean references.
* <p>Supports direct URL matches and Ant-style pattern matches. For syntax details,
* see the {@link org.springframework.web.util.pattern.PathPattern} javadoc.
* @param urlMap map with URLs as keys and beans as values
* @see #setMappings
*/
public void setUrlMap(Map<String, ?> urlMap) {
this.urlMap.putAll(urlMap);
}
/**
* Allow {@code Map} access to the URL path mappings, with the option to add or
* override specific entries.
* <p>Useful for specifying entries directly, for example via "urlMap[myKey]".
* This is particularly useful for adding or overriding entries in child
* bean definitions.
*/
public Map<String, ?> getUrlMap() {
return this.urlMap;
}
/**
* Calls the {@link #registerHandlers} method in addition to the
* superclass's initialization.
*/
@Override
public void initApplicationContext() throws BeansException {
super.initApplicationContext();
registerHandlers(this.urlMap);
}
/**
* Register all handlers specified in the URL map for the corresponding paths.
* @param urlMap a Map with URL paths as keys and handler beans or bean names as values
* @throws BeansException if a handler couldn't be registered
* @throws IllegalStateException if there is a conflicting handler registered
*/
protected void registerHandlers(Map<String, Object> urlMap) throws BeansException {
if (urlMap.isEmpty()) {
logger.trace("No patterns in " + formatMappingName());
}
else {
for (Map.Entry<String, Object> entry : urlMap.entrySet()) {
String url = entry.getKey();
Object handler = entry.getValue();
// Prepend with slash if not already present.
if (!url.startsWith("/")) {
url = "/" + url;
}
// Remove whitespace from handler bean name.
if (handler instanceof String handlerName) {
handler = handlerName.trim();
}
registerHandler(url, handler);
}
logMappings();
}
}
private void logMappings() {
if (mappingsLogger.isDebugEnabled()) {
mappingsLogger.debug(formatMappingName() + " " + getHandlerMap());
}
else if (logger.isDebugEnabled()) {
logger.debug("Patterns " + getHandlerMap().keySet() + " in " + formatMappingName());
}
}
}
d. HandlerAdapter:
- HandlerMapping를 통해, 넘어온 핸들러 정보를 사용하여 실제작업을 처리 후 결과리턴
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.function.support;
import java.util.List;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.core.Ordered;
import org.springframework.core.log.LogFormatUtils;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.Assert;
import org.springframework.web.context.request.async.AsyncWebRequest;
import org.springframework.web.context.request.async.WebAsyncManager;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.servlet.HandlerAdapter;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.function.HandlerFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
/**
* {@code HandlerAdapter} implementation that supports {@link HandlerFunction}s.
*
* @author Arjen Poutsma
* @since 5.2
*/
public class HandlerFunctionAdapter implements HandlerAdapter, Ordered {
private static final Log logger = LogFactory.getLog(HandlerFunctionAdapter.class);
private int order = Ordered.LOWEST_PRECEDENCE;
private @Nullable Long asyncRequestTimeout;
/**
* Specify the order value for this HandlerAdapter bean.
* <p>The default value is {@code Ordered.LOWEST_PRECEDENCE}, meaning non-ordered.
* @see org.springframework.core.Ordered#getOrder()
*/
public void setOrder(int order) {
this.order = order;
}
@Override
public int getOrder() {
return this.order;
}
/**
* Specify the amount of time, in milliseconds, before concurrent handling
* should time out. In Servlet 3, the timeout begins after the main request
* processing thread has exited and ends when the request is dispatched again
* for further processing of the concurrently produced result.
* <p>If this value is not set, the default timeout of the underlying
* implementation is used.
* <p>A value of 0 or less indicates that the asynchronous operation will never
* time out.
* @param timeout the timeout value in milliseconds
*/
public void setAsyncRequestTimeout(long timeout) {
this.asyncRequestTimeout = timeout;
}
@Override
public boolean supports(Object handler) {
return handler instanceof HandlerFunction;
}
@Override
public @Nullable ModelAndView handle(HttpServletRequest servletRequest,
HttpServletResponse servletResponse,
Object handler) throws Exception {
WebAsyncManager asyncManager = getWebAsyncManager(servletRequest, servletResponse);
servletResponse = getWrappedResponse(asyncManager);
ServerRequest serverRequest = getServerRequest(servletRequest);
ServerResponse serverResponse;
if (asyncManager.hasConcurrentResult()) {
serverResponse = handleAsync(asyncManager);
}
else {
HandlerFunction<?> handlerFunction = (HandlerFunction<?>) handler;
serverResponse = handlerFunction.handle(serverRequest);
}
if (serverResponse != null) {
return serverResponse.writeTo(servletRequest, servletResponse, new ServerRequestContext(serverRequest));
}
else {
return null;
}
}
private WebAsyncManager getWebAsyncManager(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(servletRequest, servletResponse);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(servletRequest);
asyncManager.setAsyncWebRequest(asyncWebRequest);
return asyncManager;
}
/**
* Obtain response wrapped by
* {@link org.springframework.web.context.request.async.StandardServletAsyncWebRequest}
* to enforce lifecycle rules from Servlet spec (section 2.3.3.4)
* in case of async handling.
*/
private static HttpServletResponse getWrappedResponse(WebAsyncManager asyncManager) {
AsyncWebRequest asyncRequest = asyncManager.getAsyncWebRequest();
Assert.notNull(asyncRequest, "No AsyncWebRequest");
HttpServletResponse servletResponse = asyncRequest.getNativeResponse(HttpServletResponse.class);
Assert.notNull(servletResponse, "No HttpServletResponse");
return servletResponse;
}
private ServerRequest getServerRequest(HttpServletRequest servletRequest) {
ServerRequest serverRequest =
(ServerRequest) servletRequest.getAttribute(RouterFunctions.REQUEST_ATTRIBUTE);
Assert.state(serverRequest != null, () -> "Required attribute '" +
RouterFunctions.REQUEST_ATTRIBUTE + "' is missing");
return serverRequest;
}
private @Nullable ServerResponse handleAsync(WebAsyncManager asyncManager) throws Exception {
Object result = asyncManager.getConcurrentResult();
asyncManager.clearConcurrentResult();
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(result, !traceOn);
return "Resume with async result [" + formatted + "]";
});
if (result instanceof ServerResponse response) {
return response;
}
else if (result instanceof Exception exception) {
throw exception;
}
else if (result instanceof Throwable throwable) {
throw new ServletException("Async processing failed", throwable);
}
else if (result == null) {
return null;
}
else {
throw new IllegalArgumentException("Unknown result from WebAsyncManager: [" + result + "]");
}
}
private static class ServerRequestContext implements ServerResponse.Context {
private final ServerRequest serverRequest;
public ServerRequestContext(ServerRequest serverRequest) {
this.serverRequest = serverRequest;
}
@Override
public List<HttpMessageConverter<?>> messageConverters() {
return this.serverRequest.messageConverters();
}
}
}
3. 애플리케이션 구동, HTTP 요청 처리 과정
1) 빈 초기화:
WebFlux 관련 빈들이 어플리케이션 컨텍스트에 등록.
WebServer, HttpHandler, DispatcherHandler, HandlerMapping, HandlerAdapter
- DispatcherHandler는 HTTP 요청을 처리하는 핵심 처리기이며, HandlerMapping 빈들이 매핑을 담당합니다. HandlerAdapter 빈들이 요청 처리 방식을 정의합니다.
2) 요청이 들어왔을 때:
클라이언트가 HTTP 요청을 보내면, **DispatcherHandler**가 해당 요청을 받아서 처리하기 위한 핸들러를 찾습니다. 이 과정은 비동기적으로 이루어집니다.
- DispatcherHandler는 HandlerMapping을 통해 URL과 HTTP 메서드에 맞는 핸들러를 찾습니다.
- HandlerAdapter가 적절한 핸들러 메서드를 실행합니다.
- 실행된 핸들러가 반환한 데이터는 Mono 또는 **Flux**로 감싸져 클라이언트에 비동기적으로 응답됩니다.
3) 핸들러 매핑과 어댑터:
요청이 들어오면 DispatcherHandler는 핸들러 매핑을 사용하여 핸들러를 찾습니다. 찾은 핸들러는 핸들러 어댑터를 통해 실행됩니다.
- HandlerMapping은 URL 경로와 HTTP 메서드(GET, POST 등)를 기반으로 요청을 처리할 핸들러 메서드를 찾습니다.
- HandlerAdapter는 해당 핸들러 메서드를 실행하는 역할을 하며, 이때 비동기 방식으로 결과를 반환합니다.
4) 예외 처리:
요청 처리 중 예외가 발생하면, **ExceptionHandler**가 해당 예외를 처리합니다. ExceptionHandlerExceptionResolver가 등록된 예외 핸들러를 찾아 실행합니다.
4. 핸들러 및 어댑터 초기화
- RequestMappingHandlerMapping: @RequestMapping과 같은 애노테이션을 기반으로 요청 URL과 핸들러를 매핑합니다.
- RequestMappingHandlerAdapter: @RequestMapping을 처리하는 어댑터로, 요청에 맞는 핸들러 메서드를 실행합니다.
- WebFilter와 HandlerInterceptor: 웹 필터와 인터셉터는 요청이 처리되기 전에 특정 작업을 수행합니다. 필터는 요청을 처리하기 전에 실행되고, 인터셉터는 핸들러 메서드 호출 전후로 실행됩니다.
5. 요청 처리 흐름 예시
- HTTP 요청 수신: 클라이언트가 /api/example로 GET 요청을 보냅니다.
- DispatcherHandler: DispatcherHandler가 요청을 받아들이고, HandlerMapping을 사용하여 해당 요청을 처리할 핸들러를 찾습니다.
- 핸들러 매핑: RequestMappingHandlerMapping이 요청 URL(/api/example)과 메서드(GET)에 맞는 핸들러 메서드를 찾습니다.
- 핸들러 어댑터: RequestMappingHandlerAdapter가 해당 핸들러 메서드를 실행하고, 결과를 Mono로 반환합니다.
- 응답 처리: 비동기적으로 실행된 핸들러 메서드가 결과를 클라이언트로 전송합니다.
정리
Spring WebFlux 기반 애플리케이션은 요청 처리와 빈 초기화가 비동기적이고 논블로킹 방식으로 이루어집니다. HTTP 요청이 들어오면 **DispatcherHandler**가 이를 처리하고, **HandlerMapping**과 **HandlerAdapter**를 통해 적절한 핸들러를 찾아 실행합니다. 빈 초기화 과정에서 DispatcherHandler, HandlerMapping, HandlerAdapter 빈들이 등록되고, HTTP 요청은 비동기적으로 처리됩니다.