Skip to content

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 값을 설정해야 한다.

java
@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에 도달하기 전에 처리해야 한다. 특히 인증 필터에서는 예외 상황에 대한 적절한 응답 처리가 필요하다.

java
@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

java
public interface HandlerMapping {
    HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}

요청 URL을 적절한 핸들러(컨트롤러 메서드)에 매핑한다. @RequestMapping 어노테이션 정보를 기반으로 매핑을 수행한다.

HandlerAdapter

java
public interface HandlerAdapter {
    boolean supports(Object handler);
    ModelAndView handle(HttpServletRequest request, HttpServletResponse response,
                       Object handler) throws Exception;
}

다양한 타입의 핸들러를 일관된 방식으로 실행할 수 있게 해주는 어댑터 패턴 구현체다.

요청 처리 흐름

DispatcherServletdoDispatch() 메서드를 통해 요청을 처리한다. 먼저 등록된 HandlerMapping 구현체들을 순회하면서 현재 요청을 처리할 수 있는 핸들러를 검색한다. Spring Boot 환경에서는 주로 RequestMappingHandlerMapping@RequestMapping 어노테이션 기반의 컨트롤러 메서드를 매핑한다.

핸들러가 발견되면 해당 핸들러를 실행할 수 있는 HandlerAdapter를 검색한다. RequestMappingHandlerAdapter가 어노테이션 기반 컨트롤러의 실행을 담당하며, 메서드 파라미터 바인딩, 유효성 검사, 반환값 처리 등의 복잡한 작업을 수행한다.

실제 핸들러 실행 전후로 HandlerInterceptor가 동작한다. preHandle()에서 요청 전처리를 수행하고, 핸들러 실행 후 postHandle()에서 후처리를 담당한다. 응답 완료 후에는 리소스 정리를 위한 afterCompletion()이 실행되며, 이는 예외 발생 여부와 관계없이 항상 호출된다.

예외가 발생하면 HandlerExceptionResolver가 동작하여 적절한 오류 응답을 생성한다. @ControllerAdvice로 등록된 전역 예외 처리기가 이 단계에서 작동한다.

CORS 처리 방식과 설정

WebMvcConfigurer를 통한 전역 CORS 설정

Spring MVC에서 CORS 설정은 주로 WebMvcConfigurer 인터페이스를 구현하여 처리한다.

java
@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 어노테이션을 사용한다.

java
@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 요청을 보내는데, 이 요청이 필터에서 차단되면 실제 요청은 전혀 실행되지 않는다.

필터 체인 최적화 전략

경로별 필터 적용

모든 요청에 인증 필터를 적용하는 대신, 필요한 경로에만 선별적으로 적용할 수 있다.

java
@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 호출 시 불필요한 처리 과정을 생략할 수 있다.

성능 모니터링

필터 체인의 성능을 모니터링하기 위해 간단한 로깅 필터를 구현할 수 있다.

java
@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는 전략 패턴과 템플릿 메서드 패턴을 효과적으로 활용하여 확장 가능하고 유연한 구조를 제공한다. 이런 아키텍처의 이해를 바탕으로 더 효율적이고 안정적인 웹 애플리케이션을 개발할 수 있다.

Released under the MIT License.