전체 목록
SpringHard#075

Filter, Interceptor, AOP의 차이점과 각각의 적합한 사용 사례를 설명해주세요.

#Spring#Filter#Interceptor#AOP

답변 포인트

실행 시점(Servlet 전, Controller 전, 메서드 전)과 접근할 수 있는 정보의 차이를 생각해보세요.

정답 및 해설

빠른 요약

Filter (javax.servlet.Filter): Servlet 컨테이너 레벨. DispatcherServlet 이전에 동작.

Spring 웹 애플리케이션에서는 여러 요청에 걸쳐 공통으로 처리해야 하는 횡단 관심사(Cross-Cutting Concern)를 처리하기 위해 Filter, Interceptor, AOP라는 세 가지 메커니즘을 제공합니다. 이 셋은 동작하는 레이어와 시점이 다르므로, 목적에 맞게 선택하는 것이 중요합니다. 요청 처리 흐름에서의 위치를 기준으로 Filter → Interceptor → AOP 순으로 더 안쪽에서 동작합니다.

전체 처리 흐름

Java
HTTP 
    
    

         Servlet Container               
    
             Filter Chain              
    Filter1  Filter2  Filter3       
    

                   
                   

         Spring MVC Container            
                                         
         DispatcherServlet               
                                        
    
          HandlerInterceptor           
    preHandle  Handler  postHandle   
    
                                        
    
            Controller                 
                     
           AOP Advice                
          @Before                    
          Service.method             
          @After                     
                     
    

Filter (javax.servlet.Filter)

개요

Filter는 Servlet 컨테이너 레벨에서 동작하며, DispatcherServlet이 요청을 받기 전에 실행됩니다. Spring의 컨텍스트와 무관하게 동작하므로, Spring Bean을 직접 주입받기 어렵습니다(단, Spring Boot에서는 DelegatingFilterProxy를 통해 가능).

구현 방법

Java
@Component
public class JwtAuthenticationFilter implements Filter {

    private final JwtTokenProvider jwtTokenProvider;

    public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        String token = extractToken(httpRequest);

        if (token != null && jwtTokenProvider.validateToken(token)) {
            // 인증 성공: 다음 필터 또는 DispatcherServlet으로 전달
            chain.doFilter(request, response);
        } else {
            // 인증 실패: 응답 직접 반환
            httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            httpResponse.getWriter().write("{\"error\": \"Unauthorized\"}");
        }
    }

    private String extractToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

CORS 처리 Filter 예시

Java
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res,
                         FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        HttpServletRequest request = (HttpServletRequest) req;

        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");

        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            chain.doFilter(req, res);
        }
    }
}

적합한 사용 사례

  • 인코딩 설정: CharacterEncodingFilter
  • CORS 처리: Cross-Origin 헤더 설정
  • JWT 토큰 검증: 인증 필터
  • 요청/응답 로깅: 전체 요청 내용 기록
  • XSS 방어: 입력값 이스케이프 처리
  • 멀티파트 요청 처리

Interceptor (HandlerInterceptor)

개요

Interceptor는 Spring MVC 레벨에서 동작하며, DispatcherServlet이 컨트롤러로 요청을 전달하기 전/후에 실행됩니다. Spring의 Bean을 자유롭게 사용할 수 있으며, 3개의 메서드를 통해 세밀한 제어가 가능합니다.

주요 메서드

Java
public interface HandlerInterceptor {

    // 컨트롤러 실행 전 → false 반환 시 요청 중단
    default boolean preHandle(HttpServletRequest request,
                              HttpServletResponse response,
                              Object handler) throws Exception {
        return true;
    }

    // 컨트롤러 실행 후, 뷰 렌더링 전 (예외 발생 시 미호출)
    default void postHandle(HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler,
                            ModelAndView modelAndView) throws Exception {
    }

    // 뷰 렌더링 완료 후 (예외 발생해도 항상 호출)
    default void afterCompletion(HttpServletRequest request,
                                 HttpServletResponse response,
                                 Object handler,
                                 Exception ex) throws Exception {
    }
}

로그인 체크 Interceptor 구현

Java
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {

    private static final Logger log = LoggerFactory.getLogger(LoginCheckInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {

        String requestURI = request.getRequestURI();
        log.info("인터셉터 실행 - URI: {}", requestURI);

        HttpSession session = request.getSession(false);
        if (session == null || session.getAttribute("loginUser") == null) {
            log.warn("미인증 사용자 접근 - URI: {}", requestURI);
            response.sendRedirect("/login?redirectURL=" + requestURI);
            return false; // 컨트롤러 실행 중단
        }

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler, Exception ex) throws Exception {
        if (ex != null) {
            log.error("예외 발생 - URI: {}, 예외: {}", request.getRequestURI(), ex.getMessage());
        }
    }
}

Interceptor 등록

Java
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    private final LoginCheckInterceptor loginCheckInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginCheckInterceptor)
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/login", "/signup", "/css/**", "/js/**");
    }
}

실행 시간 측정 Interceptor

Java
@Component
public class ExecutionTimeInterceptor implements HandlerInterceptor {

    private static final String START_TIME = "startTime";

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) {
        request.setAttribute(START_TIME, System.currentTimeMillis());
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler, Exception ex) {
        long startTime = (Long) request.getAttribute(START_TIME);
        long endTime = System.currentTimeMillis();
        long executionTime = endTime - startTime;

        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            log.info("[{}] {} - {}ms",
                    handlerMethod.getBeanType().getSimpleName(),
                    handlerMethod.getMethod().getName(),
                    executionTime);
        }
    }
}

적합한 사용 사례

  • 로그인 체크: 세션/토큰 기반 인증 확인
  • 권한 검사: 특정 URL에 대한 접근 제어
  • 요청 로깅: URI, 메서드, 파라미터 기록
  • 실행 시간 측정: 컨트롤러 응답 시간
  • 공통 모델 설정: ModelAndView에 공통 데이터 추가

AOP (Aspect Oriented Programming)

개요

AOP는 메서드 레벨에서 동작하며, 비즈니스 로직 안의 횡단 관심사를 분리합니다. 특정 패키지, 클래스, 메서드에 대해 세밀하게 적용 범위를 제어할 수 있습니다. Spring AOP는 프록시 기반으로 동작합니다.

주요 어노테이션

어노테이션설명
@Before메서드 실행 전
@After메서드 실행 후 (성공/실패 무관)
@AfterReturning메서드 정상 반환 후
@AfterThrowing예외 발생 후
@Around메서드 전/후 (가장 강력)

성능 측정 AOP

Java
@Aspect
@Component
public class PerformanceAspect {

    private static final Logger log = LoggerFactory.getLogger(PerformanceAspect.class);

    // Service 패키지의 모든 메서드에 적용
    @Around("execution(* com.example.service..*(..))")
    public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();

        try {
            Object result = joinPoint.proceed(); // 실제 메서드 실행
            return result;
        } finally {
            long endTime = System.currentTimeMillis();
            log.info("[성능] {}.{}() - {}ms",
                    joinPoint.getTarget().getClass().getSimpleName(),
                    joinPoint.getSignature().getName(),
                    endTime - startTime);
        }
    }
}

트랜잭션 로깅 AOP

Java
@Aspect
@Component
public class TransactionLoggingAspect {

    @Before("@annotation(org.springframework.transaction.annotation.Transactional)")
    public void logBeforeTransaction(JoinPoint joinPoint) {
        log.info("트랜잭션 시작: {}.{}",
                joinPoint.getTarget().getClass().getSimpleName(),
                joinPoint.getSignature().getName());
    }

    @AfterThrowing(
        pointcut = "@annotation(org.springframework.transaction.annotation.Transactional)",
        throwing = "ex"
    )
    public void logTransactionException(JoinPoint joinPoint, Exception ex) {
        log.error("트랜잭션 예외: {}.{} - {}",
                joinPoint.getTarget().getClass().getSimpleName(),
                joinPoint.getSignature().getName(),
                ex.getMessage());
    }
}

커스텀 어노테이션 기반 AOP

Java
// 커스텀 어노테이션 정의
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditLog {
    String action() default "";
}

// 어노테이션 사용
@Service
public class UserService {

    @AuditLog(action = "USER_CREATE")
    public UserDto createUser(CreateUserRequest request) {
        // 비즈니스 로직
    }
}

// AOP Aspect
@Aspect
@Component
public class AuditLogAspect {

    @AfterReturning(
        pointcut = "@annotation(auditLog)",
        returning = "result"
    )
    public void logAudit(JoinPoint joinPoint, AuditLog auditLog, Object result) {
        log.info("[감사 로그] 액션: {}, 메서드: {}, 결과: {}",
                auditLog.action(),
                joinPoint.getSignature().getName(),
                result);
    }
}

적합한 사용 사례

  • 트랜잭션 관리: @Transactional (Spring의 기본 AOP 활용)
  • 성능 측정: 비즈니스 로직 실행 시간
  • 감사 로그(Audit Log): 데이터 변경 이력 추적
  • 캐싱: @Cacheable (Spring Cache)
  • 보안: 메서드 레벨 권한 검사 (@PreAuthorize)
  • 재시도 로직: @Retryable (Spring Retry)

세 가지 비교

실행 순서 확인

Java
// 요청 처리 순서:
// Filter.doFilter (전) → Interceptor.preHandle → Controller → Service(AOP)
//                      → Interceptor.postHandle → View
//                      → Interceptor.afterCompletion
// Filter.doFilter (후)

예외 처리 비교

Java
// Filter: try-catch로 직접 처리, @ExceptionHandler 미적용
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
    try {
        chain.doFilter(req, res);
    } catch (Exception e) {
        // 직접 응답 작성 필요
        ((HttpServletResponse) res).setStatus(500);
    }
}

// Interceptor: @ExceptionHandler 적용 가능
// AOP: @ExceptionHandler 적용 가능

요약 표

구분FilterInterceptorAOP
동작 레이어Servlet 컨테이너Spring MVCSpring AOP (프록시)
적용 범위모든 요청/응답Spring MVC 핸들러특정 메서드/클래스
Spring Bean 사용제한적 (DelegatingFilterProxy)가능가능
URL 패턴 제어가능가능포인트컷으로 제어
@ExceptionHandler미적용적용 가능적용 가능
주요 사용 사례인코딩, CORS, JWT 검증로그인 체크, 권한 검사트랜잭션, 성능 측정, 감사 로그
실행 시점DispatcherServlet 이전/이후컨트롤러 이전/이후메서드 이전/이후

관련 질문

같은 카테고리/태그 기준