Spring 웹 애플리케이션에서 공통 횡단 관심사(Cross-Cutting Concerns)를 처리하기 위해 Filter(필터), Interceptor(인터셉터), Spring AOP(Aspect) 3가지 메커니즘을 사용한다.
Web Container 단에서 작동하는 Filter는 CORS, XSS, 전역 인코딩 등 가장 외곽의 HTTP 통신을 담당하며, Spring Container 단에서 작동하는 Interceptor는 Controller 호출 앞뒤에서 인증/인가, 권한 검증 및 뷰 데이터 조작을 처리한다.
두 메커니즘의 실행 흐름, 코드 패턴, 트러블슈팅(ContentCachingRequestWrapper, ThreadLocal 정리) 및 올바른 선택 기준을 통합 제시한다.
[HTTP Client Request]
│
▼
+-----------------------------------------------------------------------------------+
| Web Container (Tomcat / Servlet Container) |
| |
| [Filter 1] ──> [Filter 2 (OncePerRequestFilter)] |
| │ (FilterChain.doFilter) |
| ▼ |
| +-----------------------------------------------------------------------------+ |
| | Spring Container (DispatcherServlet) | |
| | | |
| | [Interceptor - preHandle()] | |
| | │ | |
| | ▼ | |
| | [Spring AOP Proxy (@Before)] | |
| | │ | |
| | ▼ | |
| | [Controller] ──> [Service] ──> [Repository] | |
| | │ | |
| | ▼ | |
| | [Spring AOP Proxy (@After)] | |
| | │ | |
| | ▼ | |
| | [Interceptor - postHandle() & afterCompletion()] | |
| +-----------------------------------------------------------------------------+ |
| │ |
| [Filter - 응답 후처리 구역] <───┘ |
+-----------------------------------------------------------------------------------+
│
▼
[HTTP Client Response]Filter는 자바 서블릿 스펙(jakarta.servlet.Filter)으로, doFilter() 메서드 호출 시 chain.doFilter()를 통해 요청을 다음 단계로 넘긴다.
public class CustomLoggingFilter implements Filter{
@Override
public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain)
throws IOException,ServletException{
HttpServletRequest httpRequest=(HttpServletRequest) request;
System.out.println("[Filter 전단] 요청 URI: "+ httpRequest.getRequestURI());
// 다음 필터 또는 DispatcherServlet으로 요청 전달
chain.doFilter(request, response);
System.out.println("[Filter 후단] 응답 완료 후 후처리");
}
}OncePerRequestFilter와 중복 실행 방지서블릿 환경에서 Forward(RequestDispatcher.forward()) 발생 시 동일한 요청에 대해 일반 Filter가 2번 이상 중복 호출될 수 있다.
Spring이 제공하는 OncePerRequestFilter를 상속하면 하나의 HTTP 요청당 단 1회 실행을 완벽히 보장한다. (JWT 인증 필터 구현 시 필수)
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter{
@Override
protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain filterChain)
throws ServletException,IOException{
String token= request.getHeader("Authorization");
if(token!=null&& token.startsWith("Bearer ")){
// JWT 검증 및 SecurityContext 설정
}
filterChain.doFilter(request, response);// 단 1회 실행 보장
}
}@ControllerAdvice 예외 처리 불능 문제@ExceptionHandler가 필터에서 발생한 예외를 잡지 못한다.try-catch로 예외를 잡아 response.getWriter().write()로 커스텀 JSON 응답을 쓰거나, FilterExceptionHandler를 최전방 필터로 추가 배치해야 한다.InputStream) 재사용 불가 문제request.getInputStream()을 필터에서 읽으면, 스트림 소진으로 인해 @RequestBody에서 데이터를 읽지 못해 HttpRequestMethodNotSupportedException 또는 빈 바디 에러가 발생한다.ContentCachingRequestWrapper를 사용하여 요청을 감싸 캐싱 후 읽어야 한다.HandlerInterceptor 3대 Lifecycle 메서드public class AuthInterceptor implements HandlerInterceptor{
// 1. 컨트롤러 실행 전 호출 (가장 많이 사용)
@Override
public boolean preHandle(HttpServletRequest request,HttpServletResponse response,Object handler) throws Exception {
String token= request.getHeader("X-AUTH-TOKEN");
if(token==null||!token.equals("valid-token")){
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;// false 반환 시 컨트롤러 호출 중단!
}
return true;// true 반환 시 다음 인터셉터 또는 컨트롤러 실행
}
// 2. 컨트롤러 실행 후, View 렌더링 직전에 호출 (예외 발생 시 실행 안 됨)
@Override
public void postHandle(HttpServletRequest request,HttpServletResponse response,Object handler,ModelAndView modelAndView) throws Exception{
System.out.println("[Interceptor] 컨트롤러 완료 후 View 렌더링 전");
}
// 3. 뷰 렌더링 포함 모든 작업 완료 후 호출 (예외 발생 여부 상관없이 반드시 실행)
@Override
public void afterCompletion(HttpServletRequest request,HttpServletResponse response,Object handler,Exception ex) throws Exception{
System.out.println("[Interceptor] 모든 작업 완전 종료");
}
}WebMvcConfigurer)@Configuration
public class WebConfig implements WebMvcConfigurer{
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(newAuthInterceptor())
.order(1)// 실행 순서 (숫자가 작을수록 우선)
.addPathPatterns("/api/**")// 적용할 경로
.excludePathPatterns("/api/v1/login","/api/v1/signup");// 제외할 경로
}
}preHandle() 반환값 (boolean)true를 반환해야 컨트롤러로 진입하며, false를 반환하면 실행이 즉시 중단된다.false 반환 시 클라이언트가 무응답(Hanging) 상태에 빠지지 않도록 반드시 response.setStatus()와 오류 응답 메시지를 직접 작성해야 한다.ThreadLocal 사용 시 데이터 섞임(Pollution) 방지ThreadLocal에 보관한 경우, Tomcat 스레드 풀 재사용으로 인해 다른 사용자가 이전 요청의 인증 정보를 참조하는 대형 정보 유출 사고가 발생할 수 있다.postHandle()은 컨트롤러 예외 시 호출되지 않으므로, 예외 발생 여부와 상관없이 항상 실행되는 afterCompletion()에서 반드시 ThreadLocal.remove()를 호출해야 한다.@Aspect를 통한 비즈니스 계층 공통 처리Spring AOP는 서비스/리포지토리 등 Spring Bean의 메서드 실행 시점을 대상으로 동작한다.
@Around, @Before, @AfterReturning, @AfterThrowing Advice를 통해 메서드의 입력 파라미터, 반환값, 예외를 정밀하게 조작할 수 있다.
@Aspect
@Component
public class PerformanceAspect{
@Around("execution(* com.example.service..*.*(..))")
public Object profile(ProceedingJoinPoint joinPoint) throws Throwable{
long start=System.currentTimeMillis();
try{
return joinPoint.proceed();// 타겟 메서드 실행
}finally{
long finish=System.currentTimeMillis();
System.out.println(joinPoint.getSignature()+" 소요시간: "+(finish- start)+"ms");
}
}
}@Aspect 구현 시 유의사항 (프록시 한계와 트러블슈팅)this.internalMethod())하는 경우, 프록시를 거치지 않으므로 AOP Advice 및 @Transactional이 동작하지 않는다.(해결책: 비즈니스 로직을 별도 Service 클래스로 분리)private 메서드 적용 불가: 프록시 상속/대행 구조상 public 메서드에만 AOP를 적용할 수 있다.Filter를 선택해야 하는 경우:
보안/WAF (XSS 방지, IP 차단)
CORS 전역 헤더 설정
웹 애플리케이션 전체 HTTP 요청/응답 페이로드 로깅 (ContentCachingRequestWrapper)
Spring MVC 외부의 저수준 서블릿 조작이 필요한 경우
Interceptor를 선택해야 하는 경우:
사용자 인증 및 권한 검증 (세션/JWT 토큰 체크)
컨트롤러 단위 실행 시간 측정 (성능 프로파일링)
Controller 및 View에 공통 모델 데이터 주입
Spring의 @ControllerAdvice로 예외를 통합 관리하고자 할 경우
@Aspect (Spring AOP) 사용이 적합한 경우:
서비스/리포지토리 계층의 트랜잭션 관리 (@Transactional)
비즈니스 메서드 세부 실행 시간 측정 및 프로파일링
서비스 메서드 파라미터/반환값 로깅 및 도메인 예외 로깅
댓글
GitHub 계정으로 의견이나 질문을 남길 수 있습니다.