필터 단에서 예외 처리는 @ControllerAdvice 애너테이션으로 처리할 수 없다.
전역 예외 처리기에서 처리되어야 하는 부분들이 있었기 때문에 태워야하는 상황이었다.
일괄적인 예외 메시지를 처리하고 싶은 마음은 다들 있지 않은가? (아님 말고)
근데 디깅해보면 영 시원찮다. 내가 원하는 방법은 아니었다.
고민했던 여러 방법을 소개하고 마지막으로 적용한 코드를 소개하려고 한다.
BasicErrorController로 forward, redirect 하는 방식
public class MappingException2ResponseEntityConverter {
private static final Logger log = LoggerFactory.getLogger(MappingException2ResponseEntityConverter.class);
public ResponseEntity<?> convert(
HttpServletRequest request,
HttpServletResponse response,
BusinessException businessException
) {
switch (businessException.getLevel()) {
case ERROR -> log.error(businessException.getMessage(), businessException);
case INFO -> log.info(businessException.getMessage(), businessException);
case WARN -> log.warn(businessException.getMessage(), businessException);
}
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatusCode.valueOf(response.getStatus()), businessException.getMessage());
problemDetail.setInstance(URI.create(request.getRequestURI()));
return ResponseEntity.status(response.getStatus()).body(problemDetail);
}
...
}
Servlet을 직접 핸들링하면 신경 써야할 부분들이 있는데, 인코딩
과 직렬화
다.
보통 응답의 인코딩은 UTF-8
을 사용하지만 서블릿 컨테이너의 기본 값은 ISO-8859
이다.
application/json;charset=UTF-8
상수 값을 제공 하던 MediaType.APPLICATION_JSON_UTF8_VALUE
값은 한참 전에 deprecated
되었다.
뭐 설정 방법이 없는 것은 아니다.
# application properties
server.servlet.encoding.charset=UTF-8
server.servlet.encoding.force=true
를 별도로 설정해주면 된다.
아무튼 나의 요구사항은 인코딩과 직렬화, 그리고 응답 제어를 필터에서도 동일하게 예외 공통 처리 클래스를 활용해서 응답 해주고 싶다.
Spring MVC 라이프사이클 중, 요청과 응답 처리를 적절하게 변환해주는 클래스가 HttpMessageConverter
이다.
그 중 JSON 직렬화를 담당해주는 구상 클래스 MappingJackson2HttpMessageConverter
를 사용했다.
public class FilterAreaExceptionHandler {
private final MappingException2ResponseEntityConverter exceptionConverter;
private final MappingJackson2HttpMessageConverter httpMessageConverter;
public FilterAreaExceptionHandler(
MappingException2ResponseEntityConverter exceptionConverter,
MappingJackson2HttpMessageConverter httpMessageConverter
) {
this.exceptionConverter = exceptionConverter;
this.httpMessageConverter = httpMessageConverter;
}
public void handleException(
@Nonnull final HttpServletRequest request,
@Nonnull final HttpServletResponse response,
@Nonnull final BusinessException e
) throws IOException {
ResponseEntity<?> entity = exceptionConverter.convert(request, response, e);
response.setStatus(entity.getStatusCode().value());
assert entity.getBody() != null;
httpMessageConverter.write(entity.getBody(), MediaType.APPLICATION_PROBLEM_JSON, getOutputMessage(response));
}
private HttpOutputMessage getOutputMessage(HttpServletResponse response) {
return new ServletServerHttpResponse(response);
}
}
public class ExceptionHandlingFilter extends OncePerRequestFilter {
private final FilterAreaExceptionHandler exceptionHandler;
public ExceptionHandlingFilter(FilterAreaExceptionHandler exceptionHandler) {
this.exceptionHandler = exceptionHandler;
}
@Override
protected void doFilterInternal(
@Nonnull final HttpServletRequest request,
@Nonnull final HttpServletResponse response,
@Nonnull final FilterChain filterChain
) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (BusinessException e) {
exceptionHandler.handleException(request, response, e);
}
}
}
이렇게 처리하면 @ControllerAdvice에서 처리하는 결과와 동일한 응답을 내보내 줄 수 있다.
그렇다고 이것도 맘에 드는건 아니다... 그냥 차선일 뿐 나중에 또 생각나면 바꿔야지
잘 읽었습니다