Spring MVC 요청 처리 구조와 필터 체인 분석
이 글은 Connectrip 개발 당시 사용자 인증과 CORS 설정을 구현하면서 학습한 Spring MVC의 요청 처리 구조를 정리한다. DispatcherServlet
의 동작 방식, Spring Boot 기본 필터 체인의 실행 순서, 그리고 CORS 처리 방식의 차이점을 실제 구현 경험을 바탕으로 다룬다.
Spring Boot 기본 필터 체인
필터 실행 순서와 역할
Spring Boot는 웹 애플리케이션 실행 시 여러 기본 필터를 자동으로 등록한다. 이들의 실행 순서와 역할을 이해하는 것은 커스텀 필터를 올바른 위치에 배치하기 위해 중요하다.
Spring Boot는 OrderedCharacterEncodingFilter
를 가장 먼저 실행하여 요청과 응답의 문자 인코딩을 UTF-8로 설정한다. 그 다음 OrderedFormContentFilter
가 실행되어 HTTP PUT, PATCH, DELETE 요청에서 application/x-www-form-urlencoded
형태의 form data를 파싱하여 ServletRequest.getParameter()
메서드로 접근할 수 있게 변환한다. 마지막으로 OrderedRequestContextFilter
가 -105 순서로 실행되어 RequestContextHolder
를 설정한다. 이 필터는 내부적으로 ThreadLocal
을 사용하여 현재 스레드에 요청 컨텍스트를 저장하며, 이를 통해 컨트롤러나 서비스 레이어 어디서든 RequestContextHolder.currentRequestAttributes()
를 호출하여 현재 HTTP 요청과 응답 객체에 접근할 수 있다. 단, 비동기 처리나 별도 스레드에서는 컨텍스트가 전파되지 않으므로 주의가 필요하다.
커스텀 필터 배치 전략
인증 필터를 구현할 때는 RequestContextFilter
이후에 실행되도록 order 값을 설정해야 한다.
@Component
@Order(-50) // RequestContextFilter(-105) 이후에 실행
public class JwtAuthenticationFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// CORS Preflight 요청은 인증하지 않음
if ("OPTIONS".equals(httpRequest.getMethod())) {
chain.doFilter(request, response);
return;
}
// JWT 토큰 검증 로직
String token = extractToken(httpRequest);
if (token != null && isValidToken(token)) {
chain.doFilter(request, response);
} else {
((HttpServletResponse) response).setStatus(401);
}
}
}
OPTIONS
메서드는 브라우저가 실제 요청 전에 보내는 CORS Preflight 요청이므로 인증에서 제외해야 하며, 필터 순서는 기본 필터들과의 의존 관계를 고려하여 설정하는 것이 중요하다.
필터 체인에서의 예외 처리
필터에서 발생하는 예외는 DispatcherServlet
에 도달하기 전에 처리해야 한다. 특히 인증 필터에서는 예외 상황에 대한 적절한 응답 처리가 필요하다.
@Component
@Order(-50)
public class JwtAuthenticationFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
if ("OPTIONS".equals(httpRequest.getMethod())) {
chain.doFilter(request, response);
return;
}
try {
String token = extractToken(httpRequest);
if (token != null && isValidToken(token)) {
// SecurityContext 설정 등 추가 처리
chain.doFilter(request, response);
} else {
handleAuthenticationFailure(httpResponse, "Invalid or missing token");
}
} catch (Exception e) {
handleAuthenticationFailure(httpResponse, "Authentication processing failed");
}
}
private void handleAuthenticationFailure(HttpServletResponse response, String message) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
try {
response.getWriter().write("{\"error\":\"" + message + "\"}");
} catch (IOException ignored) {
// 로깅 처리
}
}
}
필터에서 예외가 발생하면 후속 필터와 DispatcherServlet
에 도달하지 않으므로, Spring의 전역 예외 처리기(@ControllerAdvice
)가 동작하지 않는다. 따라서 필터 레벨에서 직접 예외를 처리하고 적절한 HTTP 응답을 생성해야 한다.
DispatcherServlet 요청 처리 과정
핵심 구성 요소
DispatcherServlet
은 Front Controller 패턴을 구현하여 모든 HTTP 요청을 중앙에서 처리한다. 주요 구성 요소들의 역할은 아래와 같다.
HandlerMapping
public interface HandlerMapping {
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}
요청 URL을 적절한 핸들러(컨트롤러 메서드)에 매핑한다. @RequestMapping
어노테이션 정보를 기반으로 매핑을 수행한다.
HandlerAdapter
public interface HandlerAdapter {
boolean supports(Object handler);
ModelAndView handle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception;
}
다양한 타입의 핸들러를 일관된 방식으로 실행할 수 있게 해주는 어댑터 패턴 구현체다.
요청 처리 흐름
DispatcherServlet
은 doDispatch()
메서드를 통해 요청을 처리한다. 먼저 등록된 HandlerMapping
구현체들을 순회하면서 현재 요청을 처리할 수 있는 핸들러를 검색한다. Spring Boot 환경에서는 주로 RequestMappingHandlerMapping
이 @RequestMapping
어노테이션 기반의 컨트롤러 메서드를 매핑한다.
핸들러가 발견되면 해당 핸들러를 실행할 수 있는 HandlerAdapter
를 검색한다. RequestMappingHandlerAdapter
가 어노테이션 기반 컨트롤러의 실행을 담당하며, 메서드 파라미터 바인딩, 유효성 검사, 반환값 처리 등의 복잡한 작업을 수행한다.
실제 핸들러 실행 전후로 HandlerInterceptor
가 동작한다. preHandle()
에서 요청 전처리를 수행하고, 핸들러 실행 후 postHandle()
에서 후처리를 담당한다. 응답 완료 후에는 리소스 정리를 위한 afterCompletion()
이 실행되며, 이는 예외 발생 여부와 관계없이 항상 호출된다.
예외가 발생하면 HandlerExceptionResolver
가 동작하여 적절한 오류 응답을 생성한다. @ControllerAdvice
로 등록된 전역 예외 처리기가 이 단계에서 작동한다.
CORS 처리 방식과 설정
WebMvcConfigurer를 통한 전역 CORS 설정
Spring MVC에서 CORS 설정은 주로 WebMvcConfigurer
인터페이스를 구현하여 처리한다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("http://localhost:*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true);
}
}
메서드 레벨 CORS 설정
특정 컨트롤러나 메서드에만 CORS를 적용할 때는 @CrossOrigin
어노테이션을 사용한다.
@RestController
public class ApiController {
@GetMapping("/public")
public ResponseEntity<String> publicApi() {
return ResponseEntity.ok("public");
}
@GetMapping("/special")
@CrossOrigin(origins = "https://special-client.com")
public ResponseEntity<String> specialApi() {
return ResponseEntity.ok("special");
}
}
CORS 처리 시점의 이해
WebMvcConfigurer
와 @CrossOrigin
방식은 CorsFilter
를 사용하지 않고 DispatcherServlet
내부의 DefaultCorsProcessor
에서 처리된다는 점이 중요하다.
CorsFilter 방식에서는 요청이 서블릿 필터 체인의 초기 단계에서 CORS 처리되므로, 후속 필터에서 요청을 차단하더라도 이미 CORS 헤더가 설정된다. 반면 MVC 방식에서는 모든 필터를 통과한 후 DispatcherServlet
에서 HandlerMapping
단계에서 CORS 구성을 확인하고 DefaultCorsProcessor
가 처리한다.
이는 실제 개발에서 중요한 차이를 만든다. 인증 필터에서 OPTIONS
요청을 차단하면 MVC 방식에서는 CORS 헤더 없이 401 응답이 반환되어 브라우저에서 CORS 에러로 인식하지만, CorsFilter 방식에서는 적절한 CORS 헤더와 함께 401 응답을 받을 수 있다.
Preflight 요청 처리도 마찬가지로 영향을 받는다. 브라우저는 특정 조건(커스텀 헤더, 특정 Content-Type 등)에서 실제 요청 전에 OPTIONS
메서드로 Preflight 요청을 보내는데, 이 요청이 필터에서 차단되면 실제 요청은 전혀 실행되지 않는다.
필터 체인 최적화 전략
경로별 필터 적용
모든 요청에 인증 필터를 적용하는 대신, 필요한 경로에만 선별적으로 적용할 수 있다.
@Bean
public FilterRegistrationBean<JwtAuthenticationFilter> authFilter() {
FilterRegistrationBean<JwtAuthenticationFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new JwtAuthenticationFilter());
registration.setOrder(-50);
registration.addUrlPatterns("/api/private/*", "/api/admin/*");
return registration;
}
이를 통해 정적 리소스나 공개 API 호출 시 불필요한 처리 과정을 생략할 수 있다.
성능 모니터링
필터 체인의 성능을 모니터링하기 위해 간단한 로깅 필터를 구현할 수 있다.
@Component
@Order(Ordered.LOWEST_PRECEDENCE) // 가장 낮은 우선순위로 마지막에 실행
public class PerformanceLoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
long startTime = System.currentTimeMillis();
try {
chain.doFilter(request, response);
} finally {
long duration = System.currentTimeMillis() - startTime;
if (duration > 1000) { // 1초 이상 소요된 요청 로깅
HttpServletRequest req = (HttpServletRequest) request;
log.warn("Slow request: {} {} took {}ms",
req.getMethod(), req.getRequestURI(), duration);
}
}
}
}
정리
Spring MVC의 요청 처리 구조를 이해하면 Spring Boot 기본 필터들의 실행 순서를 고려한 올바른 커스텀 필터 배치가 가능하고, CORS 처리 방식의 차이점을 파악하여 적절한 방법을 선택할 수 있다. 또한 필터 적용 범위를 최적화하여 불필요한 처리 과정을 제거함으로써 성능을 향상시킬 수 있으며, 요청 처리 흐름을 정확히 이해함으로써 문제 발생 시 빠른 원인 파악이 가능해진다.
Spring MVC는 전략 패턴과 템플릿 메서드 패턴을 효과적으로 활용하여 확장 가능하고 유연한 구조를 제공한다. 이런 아키텍처의 이해를 바탕으로 더 효율적이고 안정적인 웹 애플리케이션을 개발할 수 있다.