전체 목록
SpringHard#75

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

#Spring#Filter#Interceptor#AOP
힌트

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

정답 및 해설

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

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

전체 처리 흐름

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를 통해 가능).

구현 방법

@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 예시

@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개의 메서드를 통해 세밀한 제어가 가능합니다.

주요 메서드

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 구현

@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 등록

@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

@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

@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

@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

// 커스텀 어노테이션 정의
@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)

세 가지 비교

실행 순서 확인

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

예외 처리 비교

// 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 이전/이후컨트롤러 이전/이후메서드 이전/이후