Spring portfolio
Spring 생태계는 전통적인 동기식 Servlet 기반 스택과, 비동기 리액티브 기반 스택을 동시에 제공하며, Spring Security는 이 두 환경 모두에서 보안을 일관되게 적용할 수 있도록 기본적으로 지원합니다.

Servlet Stack
Servlet Stack은 전통적인 동기 블로킹 기반 웹 아키텍처입니다. Spring MVC는 Servlet API 위에서 동작하며, 하나의 요청이 하나의 스레드를 점유하는 "Thread-per-Request" 모델을 따릅니다.
즉, 클라이언트 요청이 들어오면 서버는 전용 스레드를 할당하고, 해당 요청이 처리 완료될 때까지 스레드는 블로킹 상태로 유지됩니다. 이 구조는 이해하기 쉽고 안정적이지만, 동시 요청이 많아질수록 스레드 자원 고갈 문제가 발생할 수 있습니다.
Servlet Containers
Servlet Stack의 최하단에는 Servlet Container가 위치합니다. 대표적인 구현체로는 Tomcat, Jetty, Undertow 등이 있으며, Spring Boot는 기본적으로 내장 Tomcat을 사용합니다.
Servlet Container는 다음과 같은 역할을 수행합니다.
- HTTP 요청 수신 및 응답 전송
- URL 매핑에 따른 Servlet 호출
- 스레드 풀 관리
- 세션, 쿠키, 커넥션 관리
- Keep-Alive, SSL 처리
기본 구조는 다음과 같습니다.

요청이 들어오면 컨테이너는 스레드 풀에서 스레드를 하나 꺼내 요청을 처리하며, 응답이 완료될 때까지 해당 스레드는 점유 상태로 유지됩니다. 이 구조가 바로 "Thread-per-Request" 모델입니다.
- Connector: HTTP 요청을 HttpServletRequest 객체로 변환
- Filter: 요청 전/후 공통 로직 처리
- init(): 서블릿 초기화
- service(): 요청 처리 엔트리
- destroy(): 자원 해제
Servlet API

서블릿은 Java 웹 애플리케이션의 핵심 구성 요소로, 클라이언트 요청을 직접 처리하는 객체입니다. 서블릿은 Jakarta EE Servlet(과거 Java EE Servlet)에 정의되어 있습니다.
public interface Servlet {
public void init(ServletConfig config)
throws ServletException;
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
public void destroy();
}
일반적인 요청 처리 흐름은 다음과 같이 동작합니다.
- init() : 서블릿 초기화
- service() : 요청 처리 분기
- destroy() : 종료 시점 호출
특히 HttpServlet은 HTTP 프로토콜에 특화된 서블릿 구현체로, 대부분의 웹 애플리케이션에서 사용하는 표준 서블릿 클래스입니다.
public abstract class HttpServlet extends GenericServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {...}
protected void doHead(HttpServletRequest req, HttpServletResponse resp) {...}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {...}
protected void doPut(HttpServletRequest req, HttpServletResponse resp) {...}
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) {...}
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) {...}
protected void doTrace(HttpServletRequest req, HttpServletResponse resp) {...}
...
protected void service(HttpServletRequest req, HttpServletResponse resp) {
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
...
doGet(req, resp);
} else if (method.equals(METHOD_HEAD)) {
...
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req, resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req, resp);
}
}
}
HttpServlet의 핵심 역할은 service() 메서드를 통해 HTTP 메서드에 따라 적절한 처리 메서드로 라우팅하는 것입니다. 내부적으로는 요청의 HTTP Method를 분석한 후 메서드를 호출합니다.
즉, 개발자는 doGet(), doPost() 등의 메서드만 구현하면 되고, 요청 분기 로직은 HttpServlet이 대신 처리합니다.
Spring MVC
Spring MVC는 서블릿 위에서 동작하는 웹 프레임워크로, 요청을 Controller로 라우팅하고 응답을 생성하는 역할을 합니다.
다음은 Spring MVC의 처리 흐름 다이어그램입니다.

protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {...}
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {...}
protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale, HttpServletRequest request) throws Exception {...}
여기서 중요한 점은 HandlerMapping, HandlerAdapter, ViewResolver가 각각 단일 객체가 아니라 여러 구현체를 리스트 형태로 보관하고 있다는 점입니다.
DispatcherServlet은 요청이 들어오면, 내부에 등록된 HandlerMapping 목록을 순회하며 현재 요청을 처리할 수 있는 핸들러를 탐색합니다. 이후 매칭되는 HandlerExecutionChain이 발견되면, 해당 체인에 포함된 Handler를 실행할 수 있는 HandlerAdapter를 찾기 위해 등록된 Adapter 리스트를 다시 순회하고, 실행 가능한 Adapter를 선택하여 처리 과정을 진행합니다.
ViewResolver 역시 동일한 방식으로 동작합니다. 컨트롤러가 반환한 View 이름을 기반으로, 등록된 ViewResolver 리스트를 순차적으로 순회하면서 처리 가능한 Resolver를 찾아 최종 View 객체를 생성합니다.
Spring Security
Servlet Stack에서 Security는 Filter 기반으로 동작합니다.
Spring Security는 Servlet Filter 체인에 포함되어 있으며, 모든 요청은 MVC로 전달되기 전에 Security 필터를 먼저 통과합니다.
다음은 Security 필터의 의 처리 흐름 다이어그램입니다.

Spring Security는 두 단계의 프록시를 거쳐 동작합니다. 먼저 Servlet 컨테이너의 Filter는 Spring의 보안 필터를 알지 못하기 때문에, 이를 연결하기 위해 DelegatingFilterProxy가 사용됩니다. 이 프록시는 실제 보안을 처리하지 않고 Spring ApplicationContext에 등록된 보안 체인으로 요청을 위임하는 역할만 수행합니다.
이후 요청은 FilterChainProxy로 전달되며, 이 프록시는 요청에 맞는 SecurityFilterChain을 선택하고 실제 보안 필터들을 실행하는 진입점 역할을 담당합니다.
Security Filter들 중 실제 인증을 담당하는 Filter는 AbstractAuthenticationProcessingFilter를 상속하고 있는 UsernamePasswordAuthenticationFilter입니다.
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean ... {
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {...}
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {...}
}
인증은 attemptAuthentication()에서 AuthenticationManager를 통해 수행되며, 인증에 성공하면 successfulAuthentication()에서 생성된 인증 정보(Authentication)를 SecurityContext에 저장합니다.
이 SecurityContext는 SecurityContextHolder를 통해 관리되며, 저장 방식은 SecurityContextHolderStrategy에 의해 결정됩니다. 기본적으로는 MODE_THREADLOCAL 전략이 사용되며, 이 경우 SecurityContext는 ThreadLocal에 저장되어 현재 요청을 처리하는 스레드 단위로 바인딩되어 관리됩니다.
Spring Data
Servlet Stack에서의 데이터 계층은 주로 블로킹 기반의 DB 접근 기술을 사용합니다.

이 구조는 다음과 같은 특징을 가집니다.
- DB I/O 수행 시 스레드 블로킹
- 요청당 하나의 Connection 사용
- 트랜잭션은 스레드 기반으로 관리
DB 응답이 지연될수록 요청 스레드는 오랜 시간 점유되며, 이는 서비스 전체의 처리량에 영향을 줍니다.
Spring Data는 관계형 데이터베이스뿐 아니라 MongoDB, Redis, Cassandra 등 다양한 저장소를 지원하며, 각각의 특성에 맞는 데이터 접근 방식을 제공합니다.
C10K 문제
C10K 문제는 "동시에 10,000개의 클라이언트 연결을 효율적으로 처리할 수 있는가"라는 질문에서 출발한 개념입니다. 이는 단순한 네트워크 성능 문제가 아니라, 서버의 아키텍처 설계 방식과 직결되는 문제입니다.
전통적인 Servlet Stack은 요청당 스레드를 하나씩 할당하는 구조를 사용합니다. 즉, 클라이언트 요청이 10,000개가 동시에 들어오면 10,000개의 스레드가 필요하게 됩니다.
이 구조의 문제점은 다음과 같습니다.
- 스레드 생성 비용 증가
- 컨텍스트 스위칭 오버헤드 발생
- 메모리 사용량 급증
- OS 스케줄러 부담 증가
결과적으로 일정 수준을 초과하는 동시 접속 환경에서는 성능 저하와 응답 지연이 발생하게 됩니다. 이것이 바로 Servlet 기반 서버가 C10K 환경에서 한계에 부딪히는 이유입니다.
Reactive Stack은 이러한 문제를 해결하기 위해 등장한 아키텍처입니다. 이벤트 루프 기반의 논블로킹 I/O 모델을 사용하여, 적은 수의 스레드로 수천 개의 연결을 동시에 처리할 수 있도록 설계되었습니다.
Reactive Stack
Reactive Stack은 논블로킹 기반의 비동기 아키텍처로 설계되었습니다. Spring WebFlux는 다수의 요청을 소수의 이벤트 루프 스레드가 처리하는 구조를 채택하며, 이벤트 기반 방식으로 동작합니다.
요청 처리 중 I/O 대기가 발생하면 스레드는 블로킹되지 않고 다른 작업을 처리하며, 응답이 준비되었을 때 다시 이벤트를 통해 처리 흐름이 이어집니다. 이로 인해 적은 스레드로도 대량의 동시 요청을 효율적으로 처리할 수 있습니다.
Netty
Netty는 고성능 비동기 네트워크 애플리케이션 프레임워크로, Reactive Stack에서 가장 하위의 실행 엔진 역할을 담당합니다.

Servlet Stack에서 Tomcat이 요청 처리 환경을 제공하는 것처럼, Reactive Stack에서는 Netty가 네트워크 연결과 I/O 처리를 담당합니다.
Netty의 핵심 특징은 다음과 같습니다.
- 이벤트 루프(Event Loop) 기반 구조
- Non-blocking I/O 처리
- Selector 기반 멀티플렉싱 구조
- 소수의 스레드로 대량 연결 처리
- 파이프라인 기반 데이터 처리 구조
Netty는 요청이 들어올 때마다 별도의 스레드를 생성하지 않습니다. 대신 소수의 이벤트 루프 스레드가 다수의 연결을 감시하며, I/O 이벤트가 발생했을 때 해당 작업을 처리합니다.
Netty의 EventLoop와 Channel에 대한 자세한 내용은 Netty - EventLoop와 Channel에서 다루고 있습니다.
Reactor Netty
Reactor Netty는 Netty 위에서 동작하는 HTTP 전용 Reactive 라이브러리로, Netty의 저수준 API를 Reactor의 Reactive Streams 모델에 맞게 추상화한 계층입니다. Servlet Stack에서 Servlet API가 HTTP 요청과 응답 처리 규약을 제공한다면, Reactive Stack에서는 Reactor Netty가 그에 대응하는 역할을 수행합니다.

Reactor Netty는 자체적으로 Transport 인터페이스를 정의하고 있으며, Netty 기반 네트워크 처리를 이 Transport 추상화 위에서 구성합니다. 이를 통해 특정 네트워크 기술에 직접 종속되지 않는 전송 구조를 제공하고, 상위 계층은 일관된 방식으로 네트워크와 상호작용할 수 있도록 합니다.

Reactor Netty에서 HTTP 서버를 실제로 생성하고 실행하는 핵심 구성 요소는 HttpServer입니다. HttpServer는 Reactor Netty가 제공하는 고수준 서버 API로, 내부적으로는 ServerTransport 추상화를 기반으로 하며 Netty의 채널, 이벤트 루프, 파이프라인을 감싸는 형태로 구성됩니다.
public static void main(String[] args) {
DisposableServer server =
HttpServer.create()
.host("localhost")
.port(8080)
.handle(
(request, response) ->
response.sendString(Mono.just("hello")))
.bindNow();
server.onDispose()
.block();
}
Netty 파이프라인에 등록된 NIO 소켓 채널이 요청을 수신하면, HTTP Decoder가 바이트 스트림을 HttpMessage 객체로 파싱하고, 이렇게 생성된 요청은 HttpServer에서 HttpServerRequest로 래핑되어 handle() 메서드로 전달된 후 처리됩니다.
package reactor.netty.http.server;
public abstract class HttpServer extends ServerTransport<HttpServer, HttpServerConfig> {
public HttpServer() {
}
public static HttpServer create() {
return HttpServerBind.INSTANCE;
}
public final HttpServer handle(
BiFunction<HttpServerRequest, HttpServerResponse, Publisher<Void>> handler) {...}
...
}
여기서 주목할 점은, HttpServer의 handle() 메서드는 원래 Reactor Netty에서 정의한 함수형 인터페이스(BiFunction<HttpServerRequest, HttpServerResponse, Publisher>)를 인자로 받는다는 점입니다. 즉, 순수하게 Reactor Netty만 사용할 경우 개발자가 이 함수형 인터페이스를 직접 구현하여 요청을 처리하도록 설계되어 있습니다.
Spring WebFlux
Spring WebFlux는 리액티브 런타임(Reactor Netty) 위에서 동작하는 비동기 논블로킹 웹 프레임워크입니다.

Spring WebFlux는 HTTP 요청을 처리할 때 특정 서버 구현에 종속되지 않고, 동일한 방식으로 요청을 처리할 수 있는 통합 인터페이스를 제공하고자 합니다. 이를 위해 Reactor Netty의 HttpServer.handle() 메서드가 요구하는 시그니처에 맞춰,
BiFunction<HttpServerRequest, HttpServerResponse, Publisher<Void>>를 구현한 ReactorHttpHandlerAdapter를 제공하며, 이 어댑터 내부에서 생성자 인자로 받은 HttpHandler를 호출합니다.
public class ReactorHttpHandlerAdapter implements BiFunction<HttpServerRequest, HttpServerResponse, Mono<Void>> {
private final HttpHandler httpHandler;
public ReactorHttpHandlerAdapter(HttpHandler httpHandler) {
this.httpHandler = httpHandler;
}
@Override
public Mono<Void> apply(HttpServerRequest reactorRequest, HttpServerResponse reactorResponse) {
return this.httpHandler.handle(request, response);
}
}
HttpHandler
HttpHandler는 다음과 같은 특징을 가지는 함수형 인터페이스입니다.
- ServerHttpRequest와 ServerHttpResponse를 인자로 받음
- 응답 처리가 완료되는 시점을 Mono로 반환
package org.springframework.http.server.reactive;
public interface HttpHandler {
/**
* Handle the given request and write to the response.
* @param request current request
* @param response current response
* @return indicates completion of request handling
*/
Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response);
}
HttpHandler를 구현한 뒤 이를 ReactorHttpHandlerAdapter로 감싸 HttpServer.handle()에 전달하면, Reactor Netty가 해당 핸들러를 통해 HTTP 요청을 정상적으로 처리합니다.
public static void main(String[] args) {
HttpHandler httpHandler = (request, response) ->
response.writeWith(
Mono.just(
response.bufferFactory()
.wrap("hello".getBytes())
)
);
ReactorHttpHandlerAdapter adapter =
new ReactorHttpHandlerAdapter(httpHandler);
HttpServer.create()
.port(8080)
.handle(adapter)
.bindNow()
.onDispose()
.block();
}
WebHandler
Spring은 WebFlux를 설계하면서 HTTP 요청 처리 구조를 기존 서블릿 방식과는 다르게 가져가기로 판단했습니다. 그 이유는 비동기 · 논블로킹 환경에서 HTTP I/O 처리와 애플리케이션 로직이 동일한 계층에 존재할 경우 구조적 복잡성과 확장성 한계가 발생하기 때문입니다.

그래서 Spring은 이 지점에서 두 책임을 분리했습니다.
- HTTP 요청의 수신과 응답 작성을 담당하는 계층 → HttpHandler
- 실제 웹 애플리케이션 로직을 실행하는 계층 → WebHandler
이렇게 두 계층이 분리되었기 때문에 이 둘을 중간 계층이 필요합니다.
이를 담당하는 것이 바로 HttpWebHandlerAdapter입니다.
public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHandler {
public HttpWebHandlerAdapter(WebHandler delegate) {
super(delegate);
}
@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
...
ServerWebExchange exchange = createExchange(request, response);
return getDelegate().handle(exchange);
}
}
public class WebHandlerDecorator implements WebHandler {
private final WebHandler delegate;
public WebHandlerDecorator(WebHandler delegate) {
this.delegate = delegate;
}
public WebHandler getDelegate() {
return this.delegate;
}
}
public interface WebHandler {
/**
* Handle the web server exchange.
* @param exchange the current server exchange
* @return {@code Mono<Void>} to indicate when request handling is complete
*/
Mono<Void> handle(ServerWebExchange exchange);
}
이 두 계층을 연결하는 HttpWebHandlerAdapter는 WebHandlerDecorator를 상속하고 HttpHandler를 구현한 클래스입니다.
이 클래스는 HTTP 계층과 WebFlux 내부 처리 계층을 연결하는 핵심 어댑터로, 재정의된 handle() 메서드 내부에서 ServerHttpRequest와 ServerHttpResponse를 묶어 ServerWebExchange를 생성한 뒤, 이를 webHandler(DispatcherHandler)에 전달하여 본격적인 웹 처리 흐름을 시작합니다.
다음은 Spring WebFlux의 처리 흐름 다이어그램입니다.

Spring MVC와 비교했을 때의 공통점과 차이점은 다음과 같습니다.
공통점
- 중앙 디스패처를 기반으로 요청 처리가 이루어집니다.
- Spring MVC: DispatcherServlet
- Spring WebFlux: DispatcherHandler
- 핸들러 탐색을 위해 HandlerMapping을 리스트 형태로 관리합니다.
- 실제 핸들러 호출을 위해 HandlerAdapter를 리스트 형태로 관리합니다.
차이점
- Spring MVC는 ViewResolver를 통해 뷰 기반 응답을 처리하는데,
Spring WebFlux는 HandlerResultHandler를 통해 컨트롤러의 반환 결과를 리액티브 방식으로 처리합니다. - Spring MVC는 HandlerInterceptor를 사용하여 요청 전·후 처리를 수행하는데,
Spring WebFlux는 WebFilter를 사용하여 요청과 응답 흐름을 리액티브 체인 기반으로 제어합니다.
Spring Security Reactive
Filter 기반의 Servlet Spring Security와 비슷하게 Reactive Spring Security는 WebFilter 기반으로 동작합니다.
그러나 Servlet 환경에서는 Servlet 컨테이너가 Spring의 보안 체인을 직접 알지 못하기 때문에, 이를 연결하기 위한 DelegatingFilterProxy가 먼저 요청을 받아 Spring Security 영역으로 위임하고, 이후 FilterChainProxy가 요청에 맞는 SecurityFilterChain을 선택하여 실제 보안 처리를 수행했습니다.
반면 Reactive 환경에서는 WebFilterChain이 처음부터 Spring 컨테이너 내부에서 구성되므로 별도의 연결용 프록시가 필요하지 않으며, WebFilterChainProxy가 곧바로 보안 진입점 역할을 수행하여 SecurityWebFilterChain을 선택하고 실행합니다.

Servlet Security Filter 중 실제로 인증을 수행하던 UsernamePasswordAuthenticationFilter와 동일한 역할을 Reactive 환경에서는 AuthenticationWebFilter가 담당합니다.
public class AuthenticationWebFilter implements WebFilter {
private Mono<Void> authenticate(ServerWebExchange exchange, WebFilterChain chain, Authentication token) {...}
protected Mono<Void> onAuthenticationSuccess(Authentication authentication, WebFilterExchange webFilterExchange) {...}
}
AuthenticationWebFilter는 authenticate()를 통해 인증을 수행하고, 인증에 성공하면 onAuthenticationSuccess() 단계에서 인증 정보(Authentication)를 SecurityContext에 저장합니다.
Servlet Spring Security에서는 이 SecurityContext가 SecurityContextHolderStrategy를 통해 ThreadLocal 기반으로 관리되었다면, Reactive Spring Security에서는 ThreadLocal 대신 Reactor Context를 사용하며 이 컨텍스트는 Mono/Flux 체인과 함께 전달됩니다. 따라서 요청 처리 과정에서 스레드가 변경되더라도 동일한 리액티브 흐름 내에서는 ReactiveSecurityContextHolder를 통해 일관된 인증 정보를 조회할 수 있습니다.
Spring Data Reactive
Reactive Stack에서는 Spring Data R2DBC와 같은 논블로킹 기반 데이터 접근 기술을 사용합니다.

R2DBC는 JDBC와 달리 스레드를 점유하지 않고 비동기적으로 데이터 스트림을 처리하며, 결과를 Mono와 Flux 형태로 전달합니다. 이 과정에서 스레드는 특정 요청에 고정되지 않고 이벤트 루프를 통해 재사용되며, DB 응답 대기 시간 동안에도 다른 요청을 처리할 수 있습니다. 커넥션 역시 요청 단위로 장시간 점유되지 않고, 리액티브 시퀀스 흐름에 따라 필요한 순간에만 사용됩니다. 트랜잭션 또한 ThreadLocal 대신 Reactor Context 기반으로 관리되며, 리액티브 체인 전체에 걸쳐 논블로킹 방식으로 트랜잭션 범위가 유지됩니다.
또한 R2DBC는 ORM을 공식 지원하지 않으므로 JPA 기반의 객체 그래프 매핑 방식은 사용할 수 없습니다.
Spring Data는 관계형 데이터베이스뿐 아니라 MongoDB, Redis, Cassandra 등 다양한 저장소를 지원하며, 각각의 특성에 맞는 데이터 접근 방식을 제공합니다.
'Spring > Webflux' 카테고리의 다른 글
| Reactor operators (0) | 2025.12.08 |
|---|---|
| Netty - ByteBuf (2) | 2025.12.03 |
| Netty Server (0) | 2025.12.03 |
| Netty - EventLoop와 Channel (0) | 2025.12.02 |
| Reactive Programing (0) | 2025.11.25 |