[Spring] Interceptor에 대해 알아보자
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);
}
}
- HanlderInterceptor 를 구현한다
- 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");