
JWT 기반 인증을 구현하고 GlobalExceptionHandler도 잘 만들어뒀다. 컨트롤러에서 발생하는 예외는 모두 깔끔하게 처리되고 있었다. 그런데 만료된 JWT 토큰으로 요청을 보내면 GlobalExceptionHandler를 거치지 않고 그냥 500 에러가 터졌다.
분명 @RestControllerAdvice로 모든 예외를 처리하게 해뒀는데 왜 안 잡히지? 로그를 찍어봐도 GlobalExceptionHandler는 아예 호출조차 안 된다. 바보 같이 JWT 검증은 필터에서 일어나고, 필터 예외는 컨트롤러까지 도달하지 않는다는 사실을 까먹고 있었다.
결국 ExceptionHandlerFilter를 따로 만들어서 해결했고, 또 까먹고 바보 같은 짓을 반복하지 않기 위해 정리를 좀 해두려고 한다.
Spring Security를 사용하는 애플리케이션에서 예외 처리는 두 계층으로 나뉜다:
이 둘은 동작하는 위치가 다르기 때문에 처리할 수 있는 예외의 범위도 다르다.

ExceptionHandlerFilter
OncePerRequestFilter 상속GlobalExceptionHandler
@RestControllerAdvice 사용ExceptionHandlerFilter
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) {
try {
filterChain.doFilter(request, response);
} catch (InvalidTokenException e) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
objectMapper.writeValue(response.getWriter(), errorResponse);
}
}
GlobalExceptionHandler
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ServiceException.class)
public ResponseEntity<ErrorResponse> handleServiceException(ServiceException e) {
return ResponseEntity.status(e.getErrorCode().getStatus())
.body(errorResponse);
}
}
ExceptionHandlerFilter에서는 CORS 헤더를 명시적으로 추가해야 한다. 필터 단계에서는 Spring의 CORS 설정이 적용되지 않기 때문이다.
ExceptionHandlerFilter는 반드시 다른 필터들보다 앞에 위치해야 한다.
http.addFilterBefore(exceptionHandlerFilter, JwtAuthenticationFilter.class);