우리는 처음부터 스프링에 관한 예외처리를 알기 전 '순수 서블릿 컨테이너'는 예외를 어떻게 처리하는지부터 알ㅇ보자! 👍🤔
서블릿은 다음 2가지 방식으로 예외처리를 지원.
자바에 메인 메서드를 직접 실행하는 경우 main 이라는 이름의 쓰레드가 실행된다.
이 때 실행 도중 예외를 잡지 못하고 처음 실행한 main() 메서드를 넘어서 예외가 던져지면,
=> "예외 정보"를 남기고 해당 쓰레드는 종료된다
웹 애플리케이션은 사용자 요청별로 별도의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행된다.
곧 WAS 까지 예외가 전달된다... 😢🤦♀️🤦♀️🤦♀️
(이러면 안된다...)
server.error.whitelabel.enabled=falㄴe
실제로 해보니 tomcat에서 '기본 제공' 되는 오류화면이 뜬다. => 500에러
Exception
의 경우라...
- F12로 보니, 웹 브라우저는 ' 서버 내부에서 처리할 수 없는 오류로군🤔' 이라 생각해 500 에러를 띄웠다.- 곧, '서버 내부'에서 예외를 처리하지 못한다면 WAS 는 반환할 Response가 없다는 뜻이다.
HttpServletResponse
가 제공하는 sendError
라는 메서드를 사용해도 된다.
또한 사용하면 'HTTP 상태코드' , '오류메시지'도 추가 (ㅇ)
예외를 처리하지 않고, WAS까지 던져버린다,,, 이것은 좋지 못한 방법이다. 그래서 서블릿 response 객체엔 이런 방법이 있다. => sendError()
response.sendError(HTTP 상태 코드, 오류 메시지)
sendError()
호출되면, response 내부에 '오류가 발생했다' 라는상태를 저장함- '서블릿 컨테이너'는 고객에게 응답 전에
response
에sendError()
가 호출되었는지 확인한다.- 그리고
호출되었다면, 설정한 오류 코드에 맞추어 '기본 오류 페이지'를 보여준다.
서블릿이 제공하는 오류 화면 기능을 사용해보자.
서블릿은 ¹
Exception
(예외)가 발생해서 서블릿 밖으로 전달되거나 또는 ²response.sendError()
가 호출 되었을 때 ... 각각의 상황에 맞춘 오류 처리 기능을 제공한다.😁
=> 이 기능을 통해서 고객 친화적인 오류화면을 보여줄 수 있다.
지금은 스프링 부트를 통해서 서블릿 컨테이너를 실행하기 때문에,
response.sendError(404)
: errorPage404 호출response.sendError(500)
: errorPage500 호출RuntimeException
또는 그 자식 타입의 예외: errorPageEx 호출[주의]
+) 물론 html에서 View 만들어주는 것은 당연한거임...
RuntimeException
예외가 WAS까지 전달되면, WAS는 오류 페이지 정보를 확인한다. RuntimeException
의 오류 페이지로 /error-page/500
이 지정되어 있다./error-page/500
를 다시 요청한다.그렇다면 오류 페이지 요청 흐름을 봐보자!!
[전제] 예외가 WAS까지 전달.
1. 예외가 발생해서 WAS까지 전파된다.
2. WAS는 오류 페이지 경로를 찾아서 내부에서 오류 페이지를 호출한다.
-> 이때 오류 페이지 경로로 필터, 서블릿, 인터셉터, 컨트롤러가 모두 다시 호출된다.😂....
request
의 attribute
에 '추가'해서 넘겨준다.참고 : ErrorPageController
목표
예외 처리에 따른 '필터'와 '인터셉터' 그리고 서블릿이 제공하는 DispatchType
이해하기
근데 이상하다...
- 우리는 오류가 발생하면 오류 페이지를 출력하기 위해 WAS 내부에서 다시 한 번 호출이 발생!
-> 그런데 로그인 인증 체크 같은 경우를 생각해보면, 이미 한 번 필터나, 인터셉터에서로그인 체크를 완료했다
=> 따라서 서버 내부에서 오류 페이지를 호출한다고 해서 '필터', '인터셉터', 서블릿 모두!!!! 다시 호출하는건...🤷♂️(너무 비효율적)- [목표]
클라이언트로 부터 발생한 '정상 요청'인지, 아니면 오류 페이지를 출력하기 위한 '내부 요청'인지 구분할 수 있어야 한다.
DispatchType
이라는 추가 정보를 제공한다.REQUEST
: 클라이언트 요청ERROR
: 오류 요청FORWARD
: MVC에서 배웠던 서블릿에서 다른 서블릿이나 JSP를 호출할 때RequestDispatcher.forward(request, response);
INCLUDE
: 서블릿에서 다른 서블릿이나 JSP의 결과를 포함할 때RequestDispatcher.include(request, response);
ASYNC
: 서블릿 비동기 호출
@Slf4j
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("log filter init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
String uuid = UUID.randomUUID().toString();
try {
log.info("REQUEST [{}][{}][{}]", uuid,
request.getDispatcherType(), requestURI);
chain.doFilter(request, response);
} catch (Exception e) {
throw e;
} finally {
log.info("RESPONSE [{}][{}][{}]", uuid,
request.getDispatcherType(), requestURI);
}
}
@Override
public void destroy() {
log.info("log filter destroy");
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new
FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST,
DispatcherType.ERROR);
return filterRegistrationBean;
}
}
고객이 처음 요청하면?
- dispatcherType=REQUEST
이다
그리고 출력해보면 오류 페이지에서
- dispatchType=ERROR
원래 기본은 filterRegistrationBean.setDispatcherTypes()
이면 DispatcherType.REQUEST 이 디폴트로 되어 있다...
특별히 오류 페이지 경로도 필터를 적용할 것이 아니면, 걍,,,,기본 값을 그대로 사용하면 된다.
-> 얘는 따로 처리해주지 않는 이상 항상 호출해준다...🤦♀️
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
public static final String LOG_ID = "logId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse
response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
String uuid = UUID.randomUUID().toString();
request.setAttribute(LOG_ID, uuid);
log.info("REQUEST [{}][{}][{}][{}]", uuid,
request.getDispatcherType(), requestURI, handler);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse
response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle [{}]", modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse
response, Object handler, Exception ex) throws Exception {
String requestURI = request.getRequestURI();
String logId = (String) request.getAttribute(LOG_ID);
log.info("RESPONSE [{}][{}][{}]", logId, request.getDispatcherType(),
requestURI);
if (ex != null) {
log.error("afterCompletion error!!", ex);
}
}
}
//인터셉터
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns(
"/css/**", "/*.ico"
, "/error", "/error-page/**" //오류 페이지 경로
);
}
excludePathPatterns
를 사용해서 빼주면 된다./error-page/**
를 제거하면 error-page/500
같은 내부 호출의 경우에도 인터셉터가 호출된다. /hello
정상 요청/error-ex
오류 요청DispatchType
으로 중복 호출 제거excludePathPatterns("/error-page/**")
)예외처리를 만들기 위해 우리는 WebServerCustomizer
,ErrorPage
, ErroController
를 따로 만들어야 했다.
사실 번거롭게 만들지 않아도 스프링 부트는 이런 과정을 모두 기본으로 제공한다.
/error/오류코드.html
을 기본 오류페이지 경로로 인식
스프링 부트에서 BasicErrorController
라는 컨트롤러를 자동으로 등록한다.
➡ 기본 경로(/error/오류코드.html
)를 매핑한다.
스프링 부트의 ErrorMvcAutoConfiguration
라는 클래스가 사용할 오류페이지를 WAS에 등록한다.
➡ 이제 서블릿 예외나 response.sendError(...)
가 호출되면 모든 오류를 /error/... URL
을 호출한다.
그리고 더 xxx.html은 더 세세한 것이 우선순위가 높다.
이제 오류페이지를 만들려고 WebServerCustomizer를 설정하고, 컨트롤러를 만들필요가 없다.
스프링부트를 이용해서 /error/...
경로에 404.html 같은 에러페이지를 만들면 자동으로 등록된다.
- 내부정보를 노출하지 말자
- 아래와 같이 오류정보를 model에 포함할지 여부를 선택할 수도 있다..
application.properties
server.error.include-exception=false : exception
포함 여부( true
, false
)server.error.include-message=never
: message
포함 여부server.error.include-stacktrace=never
: trace
포함 여부server.error.include-binding-errors=never
: errors
포함 여부기본 값이 이렇게 3가지 있다.
never
: 사용하지 않음always
:항상 사용on_param
: 파라미터가 있을 때 사용제일 좋은 것은 사용자에게 '이쁜 오류 화면'과 고객이 이해할 수 있는 간단한 오류 메시지를 보여주고
server.error.whitelabel.enabled=true
server.error.path=/error
: