공부하기/Spring

[Spring] Interceptor에 대해 알아보자

다섯자두 2025. 2. 26. 14:38

Spring Interceptor란?

Spring MVC에서 제공하는 기능으로, 특정 요청이 컨트롤러에 도달하기 전이나 응답이 클라이언트에게 반환되기 전에 추가적인 처리를 수행할 수 있도록 도와준다.

이를 통해 인증, 로깅, 공통 처리 로직등을 분리해서 유지보수성을 높일 수 있다.

구현하는 법

spring-web 5.3 이하 버전에서는

  • `HandlerInterceptorAdaptor` 클래스를 상속
  • `HandlerInterceptor` 인터페이스를 구현

함으로써 인터셉터를 생성할 수 있었다.

그러나 5.4 버전 이후부터는 `HandlerInterceptorAdaptor` 클래스가 deprecated 처리되어 `HandlerInterceptor` 인터페이스를 구현하는 방식으로 인터셉터를 만들어야 한다.

☑️  Spring Interceptor 구현 방법
  - `HandlerInterceptor` 인터페이스를 구현한다.

HandlerInterceptor 인터페이스

세 개의 메인 메서드가 있다.

1. preHandle()

실제 handler를 실행하기 전에 호출된다.

@Override
public boolean preHandle(
  HttpServletRequest request,
  HttpServletResponse response, 
  Object handler) throws Exception {
    // your code
    return true;
}
  • 반환값이 `false` : 요청이 중단되며, 이후 인터셉터나 컨트롤러가 실행되지 않는다.
  • 주로 인증/권한 체크를 수행한다.

2. postHandler()

실제 handler가 실행된 후 호출된다.

@Override
public void postHandle(
  HttpServletRequest request, 
  HttpServletResponse response,
  Object handler, 
  ModelAndView modelAndView) throws Exception {
    // your code
}
  • 응답 헤더 추가, 요청 처리 시간 측정 및 로깅 등 주로 수행

3. afterCompletion()

모든 요청 작업이 끝난 후 호출된다.

@Override
public void afterCompletion(
  HttpServletRequest request, 
  HttpServletResponse response,
  Object handler, Exception ex) {
    // your code
}
  • DB 커넥션 해제 등 리소스 정리, 요청 처리 완료 로그 기록, 예외 발생 여부 확인 및 추가 로깅 등 주로 수행

postHandler와 afterCompletion 호출의 타이밍 차이는?

``postHandler``

  • 컨트롤러의 handler가 실행된 직후, 응답이 클라이언트에게 전달되기 전(혹은 뷰가 렌더링되기 전)에 실행된다.
  • 컨트롤러에서 예외가 발생하여 정상적으로 메서드가 종료되지 않으면 호출되지 않는다.

``afterCompletion``

  • 컨트롤러의 응답이 클라이언트로 전달된 후(혹은 뷰가 렌더링된 후) 실행된다.
  • 예외 발생 여부와 관계없이 항상 실행된다.

⚙️ RestController로 요청이 들어왔을 때 각 메서드 호출 순서
1. preHandle(request, response, handler) → 컨트롤러 실행 전
2. 컨트롤러 실행 (@GetMapping, @PostMapping 등)
3. postHandler(request, response, handler, modelAndView) → 응답이 클라이언트로 전송되기 전
4. 응답이 클라이언트에게 전송됨
5. afterCompletion(request, response, handler, exception) → 응답이 전송된 후 (예외 발생 여부와 관계없이 실행됨)

AdminInterceptor를 만들어 사용해보자

AdminInterceptor 구현

/admin/** 으로 들어오는 요청은 `ADMIN` 권한을 가진 사용자인지 검증하여 접근을 제한하여야 한다.

이를 위해 AdminInterceptor를 만들었다.

@Component
public class AdminInterceptor implements HandlerInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(AdminInterceptor.class);
    private static final String USER_ROLE = "userRole";

    @Override
    public boolean preHandle(
            HttpServletRequest request, HttpServletResponse response, Object handler
    ) throws IOException {
        UserRole currentUserRole = UserRole.of((String)request.getAttribute(USER_ROLE));

        if(!UserRole.ADMIN.equals(currentUserRole)) {
            throw new AdminAccessDeniedException("관리자만 가능한 작업입니다.");
        }

        logAdminAccess(request);

        return true;
    }

    private void logAdminAccess(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        LocalDateTime requestTime = LocalDateTime.now();
        logger.info("admin 접근 요청 ({}, accepted) [{}]", requestTime, requestURI);
    }
}
  1. HanlderInterceptor 를 구현한다
  2. preHandle 메서드를 오버라이딩한다

현재 프로젝트에서 /auth/** 경로를 제외한 모든 요청은 `JwtFilter`를 통해 로그인을 검증하고 있다.

JwtFilter에서는 토큰을 검증하고 아래와 같이 HttpRequest에 사용자와 관련된 정보를 저장한다.

    UserRole userRole = UserRole.valueOf(claims.get("userRole", String.class));

    httpRequest.setAttribute("userId", Long.parseLong(claims.getSubject()));
    httpRequest.setAttribute("email", claims.get("email"));
    httpRequest.setAttribute("userRole", claims.get("userRole"));

id, email, userRole 정보를 저장하고 있음을 알 수 있다.

이를 `AdminInterceptor`에서 가져와 userRole이 `ADMIN`인지 검증하면 된다. (11~15라인)

만일 검증 실패한 경우 `AdminAccessDeniedException`이 발생하며, 이후 GlobalExceptionHandler로 책임이 위임된다.

WebMVcConfigurer에 등록

이제 WebMvcConfigurer를 구현한 config 클래스에 직접 만든 인터셉터를 등록해주면 된다.

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

    private final AdminInterceptor adminInterceptor;

    // ArgumentResolver 등록
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new AuthUserArgumentResolver());
    }

	// Interceptor 등록
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(adminInterceptor)
                .addPathPatterns("/admin/**");
    }
}
  • `addPathPatterns()`를 통해 적용할 특정 Path를 명시할 수 있다.
  • `excludePathPatterns()`를 통해 적용하지 않을 특정 Path를 명시할 수도 있다.

예를 들어, `/admin/**`의 경로에 모두 적용하되, `/admin/login`의 경로에만 적용하고 싶지 않을 경우 아래와 같이 작성한다.

        registry.addInterceptor(adminInterceptor)
                .addPathPatterns("/admin/**")
                .excludePathPatterns("/admin/login");

Reference

https://www.baeldung.com/spring-mvc-handlerinterceptor