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