서블릿 예외 처리
서블릿 예외 처리 방식
Exception
Response.sendError(HttpStatus.StatusCode, "ErrorMessage")
1. Exception
컨트롤러에서 발생한 예외는 인터셉터 -> 서블릿 -> 필터를 거쳐 WAS까지 반환됨
WAS로 예외가 올라오는 경우 Tomcat이 기본으로 제공하는 오류 화면을 볼 수 있다.
p.s) 이를 보기 위해 whitelabel 에러 페이지 생성 기능을 꺼두었다.
2. response.sendError(HttpStatus.상태코드, "errmsg")
HttpServletResponse
가 제공하는 .sendError()
메서드를 사용하는 방법도 있다.
서블릿 컨테이너에게 오류가 발생한 것을 전달할 수 있다
이 메서드는 HttpStatus.상태코드
와 errmsg
즉 오류메세지도 전달할 수 있다.
사용 용례는 다음과 같다
package hello.exception.servlet;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@Controller
public class ServletExController {
@GetMapping("/error-ex")
public void errorEx(){
throw new RuntimeException("Exception Occurred");
}
// WAS까지 예외가면 WAS에서 다시 여기로 호출함.
@GetMapping("/error-404")
public void error404(HttpServletResponse response) throws IOException {
// 발생하는 것은 아니고 메서드자체가 checked 예외로 IOException이 있어서 던져줘야함.
response.sendError(404, "404 오류!");
}
@GetMapping("/error-500")
public void error500(HttpServletResponse response) throws IOException {
// Exception 터진건 무조건 500으로 나간다.
// 아니면 직접 예외 상태코드를 아래처럼 만들어서 처리할 수 있다!
response.sendError(500);
}
}
sendError
실행 과정
WAS (.sendError
호출 기록 확인) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러 (response.sendError
)
WAS는 .sendError
의 호출을 확인하면 설정된 오류코드에 맞추어 기본 오류페이지를 보여준다.
서블릿 예외처리 - 오류화면 제공
스프링 부트를 통해 서블릿 컨테이너를 실행하기 때문에 WebServerFactoryCustomizer
의 구현체를 생성하고 이를 @Component
로 스프링 빈 등록함으로써 다음과 같이 서블릿 오류페이지를 등록할 수 있다.
// 서블릿 방식
//@Component
public class WebServerCustomizer // TOMCAT Server Error page Customization
implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory factory) {
ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
// errorPage404 호출
ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");
/// errorPage500 호출
ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, "/error-page/500");
// errorPageEx 호출
factory.addErrorPages(errorPage404, errorPage500, errorPageEx);
}
}
오류 페이지는 해당 예외와 자식예외까지 한꺼번에 처리한다.
내부 로직에 뭔가 isAssignableFrom
같은 supports
로직이 있을거같은데 그건 아니고 자바 Generic
이 이용됐다.
하긴 이건 Resolver
와는 느낌이 다르니,,,
(물론 뒤에 Exception
관련 Resolver
들이 등장한다. 이건 애노테이션 기반인지, HTTP text/json인지 구분)
public ErrorPage(Class<? extends Throwable> exception, String path) {
this.status = null;
this.exception = exception;
this.path = path;
}
위에서 특정 컨트롤러로 다시 호출하도록 코드를 구성했기 때문에 해당 urlPattern
에 대응하는 컨트롤러를 구현해야한다.
@Slf4j
@Controller
public class ErrorPageController {
@RequestMapping("/error-page/404")
public String errorPage404(HttpServletRequest request, HttpServletResponse response){
log.info("errorPage 404");
printErrorInfo(request);
return "error-page/404";
}
@RequestMapping("/error-page/500")
public String errorPage500(HttpServletRequest request, HttpServletResponse response){
log.info("errorPage 500");
printErrorInfo(request);
return "error-page/500";
}
}
그리고 리턴한 뷰에 대한 타임리프 템플릿을 이용한 html
페이지도 구성하였는데 별게 없어서 여기서는 다루지 않는다. (그냥 오류화면입니다 띄우는게 전부였기에)
서블릿 예외 처리 - 오류페이지 작동 원리
서블릿은 Exception
이 발생해서 서블릿 밖으로 전달되거나Response.sendError()
가 호출되었을 때 설정된 오류 페이지를 찾는다.
WAS는 해당 예외를 처리하는 오류 페이지 정보를 확인한다
오류 정보 추가
WAS는 오류 페이지를 요청할 때 오류 정보를 Request
의 attribute
에 추가해서 넘겨준다
@Slf4j
@Controller
public class ErrorPageController {
// RequestDispatcher 상수화 되어있음
public static final String ERROR_EXCEPTION =
"javax.servlet.error.exception";
public static final String ERROR_EXCEPTION_TYPE =
"javax.servlet.error.exception_type";
public static final String ERROR_MESSAGE = "javax.servlet.error.message";
public static final String ERROR_REQUEST_URI =
"javax.servlet.error.request_uri";
public static final String ERROR_SERVLET_NAME =
"javax.servlet.error.servlet_name";
public static final String ERROR_STATUS_CODE =
"javax.servlet.error.status_code";
private void printErrorInfo(HttpServletRequest request){
log.info("Error Exception :{} ", request.getAttribute(ERROR_EXCEPTION)); // 예외
log.info("Error Exception Type :{} ", request.getAttribute(ERROR_EXCEPTION_TYPE)); // 예외 타입
log.info("Error Message :{} ", request.getAttribute(ERROR_MESSAGE)); // 오류 메세지
log.info("Error Request URI :{} ", request.getAttribute(ERROR_REQUEST_URI)); // 클라이언트 요청 URI
log.info("Error Servlet Name :{} ", request.getAttribute(ERROR_SERVLET_NAME)); // 오류 발생 서블릿 이름
log.info("Error Status Code :{} ", request.getAttribute(ERROR_STATUS_CODE));
log.info("dispatchType :{} ", request.getDispatcherType());
// HTTP 상태 코드
}
}
request.getDispatcherType()
랑 request.getAttribute(ERROR_EXCEPTION_CODE)
이랑 다른게 뭔지 고민하고 있었는데 사실상 같은거였다.
위에 정의된 상수를 .getDispatcherType()
에서 가져온거라...
서블릿 예외 처리 - 필터
일단 WAS가 예외를 감지하고 다른 페이지를 호출했다고 치자.
그런데, 에러로 인해 요청된 다음 페이지에서 필터 / 인터셉터 호출이 모두 다시 호출되는데
이 필터 인터셉터는 이미 이전에 컨트롤러에서 예외를 던지기 전 과정에서 모두 진행된 로직이다.
결국 불필요한 로직이 두 번 실행되는 것이고 이건 한번만 검증해줘도 될거같은데 방법이 없을까?
서블릿은 이런 문제를 해결하기 위해 이것이 고객의 요청인지, 아니면 WAS에서 오류 페이지를 호출한 것인지DispatcherType
이라는 추가 정보를 통해 구별한다.
DispatcherTypes
/*
* Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */
* package javax.servlet;
/**
* Enumeration of dispatcher types. Used both to define filter mappings and by Servlets to determine why they were * called. * * @since Servlet 3.0
*/public enum DispatcherType {
/**
* {@link RequestDispatcher#forward(ServletRequest, ServletResponse)}
*/ FORWARD,
/**
* {@link RequestDispatcher#include(ServletRequest, ServletResponse)}
*/ INCLUDE,
/**
* Normal (non-dispatched) requests. */
REQUEST,
/**
* {@link AsyncContext#dispatch()}, {@link AsyncContext#dispatch(String)} and
* {@link AsyncContext#addListener(AsyncListener, ServletRequest, ServletResponse)}
*/ ASYNC,
/**
* When the container has passed processing to the error handler mechanism such as a defined error page. */
ERROR
}
다음과 같이 요청의 종류에 따라 DispatcherType
을 구분한다.
그리고 WebMvcConfigurer
의 구현체인 WebConfig
에서 다음과 같이 DispatcherType
에 따른 필터 실행 조건을 추가할 수 있다
setDispatcherTypes(...)
@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);
// 이 필터는 Request, Error인 경우 호출된다.
return filterRegistrationBean;
}
}
서블릿 예외 처리 - 인터셉터
@Slf4j
@Component
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);
}
}
}
필터와는 다르게 인터셉터의 경우 스프링 MVC에 의해 제공되는 기능으로 서블릿이 제공하는 DispatcherType
를 직접 사용할 수 없는데 때문에 WebConfig
에서 excludPathPatterns
를 사용하여 오류 페이지 경로에서는 인터셉터를 사용하지 않도록 설정해주어야 한다.
코드는 다음과 같다
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "*.ico", "/error", "/error-page/**");
// 오류페이지 경로를 넣어버리면 된다 / DispatcherType을 가지고는 어떻게 할 수 없다
// provided by Spring MVC Not Servlet(DispatcherType - Servlet Functionality)
}
정리
정상 요청 처리 과정WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 -> 뷰
오류 요청 처리 과정
- WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러
- 컨트롤러(예외) -> 인터셉터 -> 서블릿 -> 필터 -> WAS
- WAS에서는
.sendError()
호출 여부와 에러 페이지를 확인 - WAS(dispatcherType=error) -> 필터(x) -> 인터셉터(x) -> 컨트롤러 -> View 호출
오류 페이지 생성의 경우 내용이 너무 간단해서, 따로 정리하지는 않겠다.
만약 스프링 프로젝트를 하다가 오류페이지 관련해서 기억이 안나는게 있으면 교안을 다시 한번 보자(나를 위한 메모)
'스프링 공부 (인프런 김영한 선생님) > 스프링 MVC 2편' 카테고리의 다른 글
[스프링 웹 MVC 2편] 011. 로그인 처리 - 인터셉터 (0) | 2023.08.18 |
---|---|
[스프링 웹 MVC 2편] 010. 로그인 처리 - 필터 (0) | 2023.08.18 |
[스프링 웹 MVC 2편] 009. 로그인 처리 - 서블릿 HTTP 세션 (0) | 2023.08.17 |
[스프링 웹 MVC 2편] 008. 로그인 처리 - 쿠키. 세션 (0) | 2023.08.16 |
[스프링 웹 MVC 2편] 007. Bean Validation (0) | 2023.08.15 |