관련 내용
@ExceptionHandler, @ControllerAdvice를 사용한 예외를 원하는 Response 처리
개요 목적
@ExceptionHandler, @ControllerAdvice를 사용한 예외를 원하는 Response 처리 글에서 예외가 발생 했을 때 @ExceptionHandler를 사용해 컨트롤러 코드와 분리하는 방법을 알아보았다.
여기서 문제는 모든 @ExceptionHandler 메소드 Response 메세지를 처리하는 코드가 중복적으로 사용되고 있는 것이다.
이번 시간에는 Spring Interceptor와 ThreadLocal을 사용해서 예외 발생 시 Response 메세지 처리를 한 곳에서 통합적으로 처리하는 방법에 대해서 알아보자.
먼저 Spring Interceptor 이해하기
스프링 인터셉터를 사용하면, 공통 관심사(cross-cutting concern) 문제를 해결 할 수 있다.
- 공통 관심사란, 여러 로직에서 공통적으로 수행하는 동작을 의미한다. 예를 들어 로그인 확인이나, 예외 발생 시 리스폰스 보내기가 그 예이다. 만약 공통 관심 로직이 변경되면, 공통 관심이 포함된 모든 로직을 변경해야 하는 어려움이 발생한다.
서블릿이 제공하는 서블릿 필터도 공통 관심사 문제를 해결할 수 있지만, 인터셉터가 더 편리하고 정교한 다양한 기능을 제공한다.
<인터셉터 적용 순서>
//스프링 MVC가 제공하는 기능이기 때문에 디스패처 서블릿 이후에 작동된다.
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
//스프링 인터셉터 체인으로 인터셉터를 여러개 추가할 수 있다.
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터1 -> 인터셉터2 -> 컨트롤러ㅇ
인터셉터 인터페이스 기능 확인하기
public interface HandlerInterceptor {
//1 컨트롤러 호출 전 실행
default boolean preHandle(HttpServletRequest request, HttpServletResponse
response,Object handler) throws Exception {}
//2 컨트롤러 호출 후 실행
default void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, @Nullable ModelAndView modelAndView) throws Exception {}
//3 요청 완료 후 무조건 실행
default void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, @Nullable Exception ex) throws Exception {}
}
1) preHandle
컨트롤러 호출 전에 호출된다. preHandle 의 응답값이 true 이면 다음으로 진행하고, false 이면 더는 진행하지 않는다.
false인 경우 나머지 인터셉터는 물론이고, 핸들러 어댑터도 호출되지 않는다. 그림 1번에서 끝이 나버린다.
2) postHandle
컨트롤러 호출 후에 호출된다.
컨트롤러에서 예외가 발생하면 postHandle 은 호출되지 않는다.
3) afterCompletion
뷰가 렌더링 된 이후에 호출된다.
컨트롤러 예외가 발생해도 항상 호출된다. 이 경우 예외( ex )를 파라미터로 받아서 어떤 예외가 발생했는지 알 수 있다.
예외와 무관하게 공통 처리를 하려면 afterCompletion()을 사용해야 한다.
인터셉터 파라미터 확인하기, 인터셉터 등록 하기
파라미터 정보 확인하기
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse
response, Object handler) throws Exception {
//기본적으로 request정보와 response 정보를 확인할 수 있다.
String requestURI = request.getRequestURI();
//@RequestMapping으 경우 HandlerMethod로 핸들러 정보가 들어온다.
//호출할 컨트롤러 메서드의모든 정보가 포함되어 있다.
//정적 리소스의 경우 ResourceHttpRequestHandler가 핸들러 정보로 넘어온다.
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse
response, Object handler, Exception ex) throws Exception {
//넘어온 예외에 대한 정보를 얻을 수 있다.
if (ex != null) {
log.error("afterCompletion error!!", ex);
}
}
}
인터셉터 등록하기
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/*.ico", "/error");
}
}
- registry.addInterceptor(new LogInterceptor()) : 인터셉터를 등록한다.
- order(1) : 인터셉터의 호출 순서를 지정한다. 낮을 수록 먼저 호출된다.
- addPathPatterns("/") : 인터셉터를 적용할 URL 패턴을 지정한다.
- excludePathPatterns("/css/", "/*.ico", "/error") : 인터셉터에서 제외할 패턴을 지정한다.
ThreadLocal과 Interceptor를 사용해 예외 Response 처리 통합하기
작동 과정은 컨트롤러에서 예외가 발생하면, @ExceptionHandler 메소드에서 예외를 잡아 해당 예외에 맞는 Response 정보를 ThreadLocal에 저장한다.
그 이후 Interceptor afterCompletion 메소드 실행되어, ThreadLocal에 저장된 정보를 가져와 Response 메세지 생성 반환을 일관적으로 처리한다.
ThreadLocal과 @ExceptionHandler 사용 법은 아래 블로그 글에서 확인 할 수 있다.
@ExceptionHandler, @ControllerAdvice를 사용한 예외를 원하는 Response 처리
ThreadLocal 설정하기
@Data
public class ErrorInformation {
private String errorType;
private String errorTitle;
private String errorDetail;
}
@Component
public class ErrorInformationTlsContainer {
private ThreadLocal<ErrorInformation> threadLocal = ThreadLocal.withInitial(
() -> new ErrorInformation("none", "none", "none")
);
public ThreadLocal<ErrorInformation> getThreadLocal() {
return threadLocal;
}
public void removeThreadLocal() {
threadLocal.remove();
}
}
ThreadLocal을 사용할 때 주의할 점은 값이 비어져 있는 지 확인 하는 것이다.
그래서 ThreadLocal.withInitial()을 사용해서 초기 값을 설정했고, get 메소드로 내용물이 none 인지를 확인해서 값이 비어져 있는 지 확인 할 수 있다.
@ExceptionHandler에서 Response 정보를 ThreadLocal로 저장하기
@RequiredArgsConstructor
public class AdminSentenceExceptionAdvice {
private final ErrorInformationTlsContainer errorInformationTlsContainer;
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Void> methodArgumentNotValidExceptionAdvice
(MethodArgumentNotValidException e) {
ThreadLocal<ErrorInformation> threadLocal =
errorInformationTlsContainer.getThreadLocal();
//ThreadLocal이 비어져 있는 지 화인하기
if (!threadLocal.get().getErrorTitle().equals("none")) {
throw new ErrorInformationTlsException("errorInformationTls is not Empty");
}
//에러 메시지로 해당 에러가 정확히 어떤 에러인지 확인하기
String errorMessage = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
ErrorInformation errorInformation = new ErrorInformation();
switch (findByErrorMessage(errorMessage)) {
case ADD_SENTENCE_STRING_BLANK:
//enum으로 등록해 놓은 에러별 response 정보를 ThreadLocal에 저장한다
errorInformation.setErrorType(ADD_SENTENCE_STRING_BLANK.getErrorType());
errorInformation.setErrorTitle(ADD_SENTENCE_STRING_BLANK.getErrorTitle());
errorInformation.setErrorDetail(ADD_SENTENCE_STRING_BLANK.getErrormessage());
threadLocal.set(errorInformation);
//response 상태 코드는 미리 보낸다.
return new ResponseEntity<>(ADD_SENTENCE_STRING_BLANK.getHttpStatus());
}
throw new RuntimeException();
}
}
Interceptor 에서 Response 일관적으로 만들고 반환하기
@RequiredArgsConstructor
public class ExceptionResponseInterceptor implements HandlerInterceptor {
private final ErrorInformationTlsContainer errorInformationTlsContainer;
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
if (!errorInformationTlsContainer.getThreadLocal().get().getErrorTitle().equals("none")) {
//ThreadLocal에 들어온 정보가 있으면 -> 예외 Response 처리하기
ErrorInformation errorInformation = errorInformationTlsContainer.getThreadLocal().get();
String errorType = errorInformation.getErrorType();
String errorDetail = errorInformation.getErrorDetail();
String errorTitle = errorInformation.getErrorTitle();
errorInformationTlsContainer.removeThreadLocal();
ErrorResponse error = new ErrorResponse(errorType, errorTitle,
response.getStatus(), errorDetail, request.getRequestURI());
String errorResponseBody = objectMapper.writeValueAsString(error);
//파리미터로 받은 response에 에러 메세지를 등록한다.
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(errorResponseBody);
}
}
}
webConfig에 만든 Interceptor 등록하기
@Configuration
@RequiredArgsConstructor
public class InterceptorWebConfig implements WebMvcConfigurer {
private final ErrorInformationTlsContainer errorInformationTlsContainer;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new ExceptionResponseInterceptor(errorInformationTlsContainer))
.order(1)
.addPathPatterns("/**");
}
}
'Web Sever 개발과 CS 기초 > 스프링' 카테고리의 다른 글
org.passay 사용하여 @Password Validation 검증기 만들기 (0) | 2023.04.27 |
---|---|
Validation 사용하여 Respuset 정보 검증하기 (0) | 2023.04.27 |
자바 예외에 대한 이해 (0) | 2023.04.26 |
Spring을 사용한 JDBC 예외 누수 문제 해결하기 (0) | 2023.04.26 |
Logback 설정 파일, 콘솔과 파일에 원하는 형식 로그 출력하기 (0) | 2023.04.26 |