웹페이지에 접속할 떄 클라이언트 또는 서버 잘못된 요청이나 응답을 할 경우 오류페이지가 뜨게됩니다.
자바 파일을 직접 실행
자바의 메인 메서드를 직접 실행하는 경우 main 이라는 이름의 쓰레드가 실행됩니다. 실행 도중에 예외를 잡지 못하고
처음 실행한 main() 메서드를 넘어서 예외가 던져지면, 예외 정보를 남기고 main 쓰레드는 종료됩니다.
웹 애플리케이션
웹 애플리케이션은 사용자 요청별로 별도의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행됩니다.
애플리케이션에서 예외가 발생했는데, 어디선가 try ~ catch로 예외를 잡아서 처리해준다면 아무 문제가 없습니다.
그런데 만약에 애플리케이션에서 예외를 잡지 못하고, 서블릿 밖으로 까지 예외가 전달된다면
WAS(Web ApplicationServer까지 예외가 전달됩니다.

예제를 통해 알아보겠습니다
.
server.error.whitelabel.enabled=false
먼저 application.properties 파일에서 스프링 부트에서 보여주는 기본 에러 페이지를 꺼두겠습니다.
@Slf4j
@Controller
public class ServletExController {
@GetMapping("/error-ex")
public void errorEx() {
throw new RuntimeException("예외 발생!");
}
}

/errror-ex 라는 url 을 요청하면 예외를 던지는 컨트롤러를 생성하고 해당 url 로 호출을 했더니
HTTP 상태 코드가 500을 보여주는 오류 페이지가 나오는걸 확인할 수 있습니다.
HTTP 상태 코드가 5XX 면 서버에서 문제가 생긴 에러가 발생함을 의미합니다.

이번에는 컨트롤러에서 매칭하지 못하는 url 을 요청해보았습니다.
전과는 다르게 HTTP 상태 코드가 404를 보여주는 오류 페이지가 나오는걸 확인할 수 있습니다.
HTTP 상태 코드가 4XX 면 클라이언트에서 문제가 생긴 에러가 발생함을 의미합니다.
오류가 발생했을 때는 서블릿에서 HTTP 응답 메시지 생성할 떄 사용하는 HttpServletResponse 의
sendError() 메서드를 이용할 수 있습니다.
이 메서드로 서블릿 컨테이너에게 오류가 발생했다는 점을 전달할 수 있으며
HTTP 상태 코드와 오류 메시지도 추가할 수 있습니다.
@Slf4j
@Controller
public class ExceptionTextController {
@GetMapping("/error-404")
public void error404(HttpServletResponse response) throws IOException {
response.sendError(404, "404 클라이언트측 오류!");
}
@GetMapping("/error-500")
public void error500(HttpServletResponse response) throws IOException {
response.sendError(500, "500 서버측 오류");
}
}


response.sendError() 를 호출하면 response 내부에는 오류가 발생했다는 상태를 저장해두며
서블릿 컨테이너는 클라이언트에게 응답 전에 response 에 sendError() 가 호출되었는지 확인합니다.
그리고 호출되었다면 설정한 오류 코드에 맞추어 기본 오류 페이지를 보여줍니다..
그런데 단순히 저런 HTTP 상태 코드만 나오는 오류 페이지는 클라이언트에게 친화적이지 않습니다.
HTTP 상태 코드나 예외에 따라 어떤 페이지를 보여줄지 설정하고 보여줄 페이지를
직접 클라이언트 친화적으로만든다면 좋지 않을까요?
그런 이유로 서블릿은 오류 화면 기능을 제공합니다.
@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory factory) {
ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");
ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, "/error-page/500");
factory.addErrorPages(errorPage404, errorPage500, errorPageEx);
}
}
이제 WAS 에 예외가 전달되거나, response.sendError() 에 맞는 HTTP 상태 코드가 위에 존재한다면 등록한
예외 페이지를 호출하는 url 이 요청됩니다/
서블릿 오류 페이지 등록은 WebServerFactoryCustomizer 를 구현한 클래스를 생성하고 customize() 메서드를
오버라이딩한 후 파라미터로 HTTP 상태 코드와 뷰 경로를 넣어준 ErrorPage 객체를
ConfigurableWebServerFactory 객체의 addErrorPages() 메서드로 추가해주면 됩니다.
이렇게 하면 HttpStatus.NOT_FOUND 인 404 에러가 발생하면 /error-page/404 url 요청
HttpStatus.INTERNAL_SERVER_ERROR 인 500 에러가 발생하면 /error-page/500 url 요청
500 예외가 서버 내부에서 발생한 오류라는 뜻을 포함하고 있기 때문에 여기서는 예외가 발생한 경우도 500 오류 화면
으로 처리하고 RuntimeException 클래스의 그 자식 클래스가 발생해도 /error-page/500 url 요청을 하게됩니다.
@Slf4j
@Controller
public class ErrorPageController {
@RequestMapping("/error-page/404")
public String errorPage404(HttpServletRequest request, HttpServletResponse response) {
log.info("errorPage 404");
return "error-page/404";
}
@RequestMapping("/error-page/500")
public String errorPage500(HttpServletRequest request, HttpServletResponse response) {
log.info("errorPage 500");
return "error-page/500";
}
}
서블릿 오류 페이지를 등록한 이후에는 오류가 발생했을 때 처리할 수 있는 컨트롤러를 생성해야 합니다.
예를 들어서 RuntimeException 예외가 발생하면 WebServerCustomizer 클래스의 errorPageEx 에서 지정한
/error-page/500 url 요청과 매칭되는 컨트롤러가 필요한 것이죠.
서블릿 예외 처리에서 오류 페이지 작동 원리
서블릿은 Exception(예외) 가 발생해서 서블릿 밖으로 전달되거나 response.sendError() 가 호출 되었을 때
설정된 오류 페이지를 찾습니다.


WAS는 해당 예외를 처리하는 오류 페이지 정보를 확인합니다.
예를 들어서 RuntimeException 예외가 WAS까지 전달되면, WAS는 오류 페이지 정보를 확인합니다.
ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, "/error-page/500");
RuntimeException 의 오류 페이지는 /error-page/500 이 지정되어 있는걸 확인하고
WAS는 오류 페이지를 출력하기 위해 /error-page/500 를 다시 요청합니다


중요한 점은 웹 브라우저(클라이언트)는 서버 내부에서 이런 흐름이 일어나는지 전혀 모른다는 점이다.
오직 서버 내부에서 오류 페이지를 찾기 위해 추가적인 호출을 하게됩니다.
예외가 발생해서 WAS 까지 전달되면 해당 예외와 매칭되는 url 요청을 해서 컨트롤러까지 가는데
필터, 서블릿, 인터셉터를 다시 호출하게 되는 것이죠. 예를 들어 로그인 체크 기능을 하는 필터와
인터셉터를 만들었다고 가정하면, 이미 한 번 필터나, 인터셉터에서 로그인 체크를 완료했는데 서버 내부에서
오류 페이지를 요청하면서 다시 필터나, 인터셉터가 동작해 로그인 체크를 하게 되는겁니다.
이렇게 되면 매우 비효율적입니다. 결국에는 클라이언트로 부터 발생한 정상 요청인지, 아니면 오류 페이지를
출력하기 위한 내부 요청인지 구분할 수 있어야합니다. 서블릿은 이런 문제를 해결하기 위해 DispatcherType 이라는
추가 정보를 제공합니다. 정확하게 말하자면 서블릿 필터에서 DispatcherType 옵션을 제공하고 실제 클라이언트가
요청하면 dispatcherType=REQUEST, 서버 내부에서 오류 페이지를 요청하면 dispatchType=ERROR 로 출력하도록
할 수 있습니다.
@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) {
log.info("EXCEPTION {}", e.getMessage());
throw e;
} finally {
log.info("RESPONSE [{}][{}][{}]", uuid, request.getDispatcherType(), requestURI);
}
}
@Override
public void destroy() {
log.info("log filter destroy");
}
}
필터를 생성하고 요청과 응답 로그를 출력하는 부분에 request.getDispatcherType() 을 넣었습니다.
@Configuration
public class WebConfig {
@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;
}
}
그리고 스프링 설정 클래스에 방금 생성한 필터를 빈으로 등록해줍니다.
setDispatcherTypes() 메서드에 DispatcherType.REQUEST 와 DispatcherType.ERROR 을 넣어줬는데 이렇게
하면 클라이언트 요청과 오류 페이지 요청에서 필터가 호출됩니다. 아무것도 넣지 않으면 기본 값이
DispatcherType.REQUEST 입니다. 즉 클라이언트의 요청이 있는 경우에만 필터가 적용되는 것이죠. 앞에서 언급한
로그인 체크 필터를 만들었다면 setDispatcherTypes() 만 적어주면 될 것입니다. 만약 오류 페이지 요청 전용 필터를
적용하고 싶으면 DispatcherType.ERROR 만 지정 하면 됩니다.
이번에는 인터셉터로 중복 호출을 제거하는 코드를 살펴보겠습니다.
@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);
}
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "*.ico", "/error", "/error-page/**");/
}
}
필터의 경우에는 필터를 등록할 때 어떤 DispatcherType 인 경우에 필터를 적용할 지 선택할 수 있었습니다.
그런데 인터셉터는 서블릿이 제공하는 기능이 아니라 스프링이 제공하는 기능입니다. 따라서 DispatcherType 과
무관하게 항상 호출됩니다. 대신 인터셉터는 excludePathPatterns() 메서드로 인터셉터 적용 제외 요청 url 을 지정할
수 있어서 오류 페이지들의 공통 경로를 적어주어 호출하지 못하게 할 수 있습니다. 여기서는 에러 페이지들이
error-page 폴터에 모여있다고 가정해 오류 페이지들의 공통 경로인 /error-page 를 excludePathPatterns() 에
추가해주었습니다.

현재 생성한 필터와 인터셉터를 적용한 후 /error-ex 오류 요청을 했을 때 흐름입니다.
컨트롤러에서 예외가 발생한 후 WAS 까지 전달되고 오류 페이지 /error-page/500 을 요청하니
필터와 인터셉터를 거치지 않고 컨트롤러까지 다시 전달됩니다. 필터와 인터셉터를 다시 호출되지 않게는 했지만
지금까지 예외 처리 페이지를 만들기 위해서 서블릿 오류 페이지 등록 클래스인 WebServerFactoryCustomizerr 를
만들고 예외 종류에 따라 ErrorPage 를 추가하며 예외 처리용 컨트롤러인 ErrorPageController 까지 만드는
복잡한 과정을 거쳤습니다. 이런 과정을 스프링 부트는 모두 기본으로 제공합니다. ErrorPage 를 자동으로 등록합니다.
이 때 new ErrorPage("/error") 코드처럼 /error 라는 경로로 기본 오류 페이지를 설정합니다. 상태코드와 예외를
설정하지 않으면 기본 오류 페이지로 사용됩니다. 서블릿 밖으로 예외가 발생하거나, response.sendError(...) 가
호출되면 모든 오류는 /error 를 호출하게됩니다. BasicErrorController 라는 스프링 컨트롤러를 자동으로 등록
합니다. ErrorPage 에서 등록한 /error 를 매핑해서 처리하는 컨트롤러입니다. 그렇기 때문에 ErrorPageController
와 같은 예외 처리용 컨트롤러를 따로 생성할 필요가 없습니다. 또한 ErrorMvcAutoConfiguration 이라는 클래스가
오류 페이지를 자동으로 등록하는 역할을 합니다.
스프링 부트는 오류가 발생했을 때 오류 페이지로 /error 를 기본 요청합니다. 스프링 부트가 자동 등록한
BasicErrorController 는 이 경로를 기본으로 받습니다. BasicErrorController 는 기본적인 로직이 모두 개발
되어있고 오류 페이지 화면만 BasicErrorController 가 제공하는 룰과 우선순위에 따라서 등록하면 됩니다.

오류 페이지 화면을 정적 HTML 로 만들고 싶다면 정적 리소스, 뷰 템플릿을 사용하면 되고 동적으로 오류 화면을
만들고 싶으면 뷰 템플릿 경로에 오류 페이지 파일을 만들어서 넣어두기만 하면됩니다.
스프링에서는 정적인 HTML 파일은 static 폴더에,
템플릿 엔진을 사용해서 동적으로 화면을 만드는 HTML 파일은 template 폴더에 넣습니다.
BasicErrorController 가 선택하는 뷰 우선순위
1. template 폴더안의 파일
2. 더 구체적인 HTTP 상태 코드로 이름을 갖는 파일
3. HTTP 상태 코드 숫자가 더 작은 이름을 갖는 파일
예를 들면 밑에와 같은 파일 6개가 있다고 가정하겠습니다.
resources/templates/error/500.html resources/templates/error/5xx.html resources/static/error/400.html
resources/static/error/404.html resources/static/error/4xx.html resources/templates/error.html
여 때 BasicErrorController 의 처리 순서는
1. 뷰 템플릿
resources/templates/error/500.html
resources/templates/error/5xx.html
2. 정적 리소스( static , public )
resources/static/error/400.html
resources/static/error/404.html
resources/static/error/4xx.html
3. 적용 대상이 없을 때 뷰 이름( error )
resources/templates/error.html
이렇게 됩니다. HTML 파일들은 HTTP 상태 코드로 이름을 갖는데 4XX 나 5XX 같은 이름은
400대, 500대 오류를 처리해준다는 의미입니다.
지금까지 예외 처리와 오류 페이지에 대해 알아보았습니다. 클라이언트 혹은 서버 측의 실수로 오류 페이지를
보여주어야 한다면 클라이언트가 이해할 수 있는 오류 패이지를 따로 생성하고 보여줄 수 있는 스프링 부트의 기능은
참 편리한거 같습니다.
'Spring' 카테고리의 다른 글
| [Spring] 컨버터(Converter) 포맷터(Fomatter) (0) | 2022.10.21 |
|---|---|
| [Spring] API 예외 처리(API exception handling) (0) | 2022.10.20 |
| [Spring] AOP(Aspect Oriented Programming) 학습 (0) | 2022.10.13 |
| [Spring] AOP(Aspect Oriented Programming) 개념 이해하기 (0) | 2022.10.13 |
| [Spring] 필터(Filter) 인터셉터(Interceptor) (0) | 2022.10.11 |
댓글