프로젝트를 진행하다가, 토큰 필터에서 발생한 예외가 GlobalExceptionHandler로 정의한 규격에 맞지 않게 return이 되는 것을 확인하였다. 포스트맨에서는 403 에러가 뜨고, 인텔리제이 로그에서 우리가 정의한 에러 메세지가 출력되는것을 확인하였다. 왜 이런 현상이 발생을 할까?



| 구분 | 필터 | 인터셉터 |
|---|---|---|
| 위치 | 서블릿 컨테이너 레벨 | Spring MVC 레벨 |
| 적용 범위 | DispatcherServlet 이전 | DispatcherServlet 이후 |
| 사용 목적 | 전역적인 요청/응답 처리 | 컨트롤러 실행 전후의 요청/응답 처리 |

1️⃣ 필터에서 예외 발생 시 DispatcherServlet으로 전달되지 않음
DispatcherServlet까지 도달하지 않는다.GlobalExceptionHandler는 이러한 예외를 감지하거나 처리할 수 없다.2️⃣ Spring MVC의 예외 처리 흐름
DispatcherServlet 을 거치지 않으므로, MVC 예외 처리 메커니즘이 동작하지 않는다.@Component
public class TokenExceptionHandlerFilter extends OncePerRequestFilter { // OncePerRequestFilter : 한 요청당 필터가 딱 한 번만 실행되도록 보장하는 추상 클래스
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try{
filterChain.doFilter(request, response);
}catch(BusinessException e){
handleBusinessException(request,response,e);
}
}
private void handleBusinessException(HttpServletRequest request, HttpServletResponse response, BusinessException e) throws IOException {
ExceptionType exceptionType = e.getExceptionType();
response.setStatus(exceptionType.getStatus().value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8"); // HttpServletResponse: ISO-8859-1 인코딩 사용하기 때문에 한글 출력을 위해 UTF-8 설정
ResponseBody<Void> body= ResponseUtil.createFailureResponse(exceptionType);
writeErrorResponse(response,body);
}
private void writeErrorResponse(HttpServletResponse response, ResponseBody<Void> body) throws IOException{
ObjectMapper objectMapper = new ObjectMapper(); // Jackson ObjectMapper 인스턴스 생성
String json = objectMapper.writeValueAsString(body); // ResponseBody 객체를 JSON 문자열로 직렬화
try (PrintWriter writer = response.getWriter()) { // PrintWriter 자원을 안전하게 닫기 위해 try-with-resource 사용
writer.write(json);
writer.flush();
}
}
}
OncePerRequestFilter: HTTP 요청당 딱 한 번만 실행되도록 보장하는 추상클래스이다.
doFilterInternal() : 요청을 필터 체인에서 다음 단계로 전달한다.
handleBusinessException
writeErrorResponse():
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
TokenAuthenticationFilter tokenAuthenticationFilter = new TokenAuthenticationFilter(tokenProvider, customUserDetailsService);
http
.csrf(AbstractHttpConfigurer::disable) //csrf 무시
.cors(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers(AUTH_WHITELIST).permitAll()
.anyRequest().denyAll()
);
// JWT 필터를 UsernamePasswordAuthenticationFilter 앞에 추가
http.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(new TokenExceptionHandlerFilter(), TokenAuthenticationFilter.class);
http.sessionManagement(sessionManagement->sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// FormLogin, BasicHttp 비활성화
http.formLogin(AbstractHttpConfigurer::disable);
http.httpBasic(AbstractHttpConfigurer::disable);
return http.build();
}
response 객체로 직접 리턴하나요? ResponseEntity를 사용하면 안 되나요?ResponseEntity는 MVC 컨텍스트에서만 사용 가능ResponseEntity는 Spring MVC에서 컨트롤러(@Controller, @RestController) 계층에서 HTTP 응답을 생성하고 반환할 때 사용하는 객체입니다.Filter는 Spring MVC 레이어의 이전 단계에서 동작합니다.Filter는 서블릿 컨테이너(javax.servlet.Filter) 기반으로 작동하기 때문에, MVC와는 전혀 다른 레벨에서 동작합니다.HttpServletResponse를 직접 조작해서 응답을 만들어야 합니다.ResponseEntity를 사용할 수 없습니다.response 객체HttpServletResponse 객체를 직접 다뤄야 합니다.response.getWriter().write()를 사용해 JSON 데이터를 직접 작성해야 합니다.writeErrorResponse로 직렬화를 직접 하나요?HttpServletResponse는 객체 직렬화를 지원하지 않음HttpServletResponse는 단순히 HTTP 상태 코드, 헤더, 바디를 설정할 수 있는 서블릿 컨테이너 레벨의 객체입니다.@RestController)에서 반환한 객체를 JSON으로 직렬화해서 응답 바디에 넣어줍니다.response.getWriter().write()로 응답 바디에 넣어야 합니다.ObjectMapper(Jackson)를 사용하여 객체를 JSON 문자열로 변환한 뒤, 이를 response의 출력 스트림에 작성합니다.