Servlet - 예외처리와 오류 페이지 (Exception)

컴업·2021년 11월 19일
2

예외 처리의 흐름

서블릿이 예외(Exception)를 받는 경우는 두가지 입니다.

첫번째로 서블릿 내에서 발생한 예외가 서블릿 밖으로 나온경우, 그리고 response.sendError()메서드가 호출된 경우입니다.

response.sendError()메서드는 우리가 임의로 만들어주는 에러로, 이를 통해 상태 코드를 지정하지 않은 기타 예외들은 모두 500으로 처리합니다.

예외 발생

예외가 발생하고 그것이 서블릿 밖으로 빠져나오면 WAS가 이 예외를 받게됩니다.

WAS는 이미 만들어 둔 예외 페이지가 존재한다면 그 페이지 url로 다시 요청메세지를 보내게 되고, 만약 없다면 톰캣이 기본으로 제공하는 못생긴 오류 페이지에 HTTP 상태 코드 500으로 응답을 보내게 됩니다.

response.sendError() 사용

서버는 발생한 예외를 모두 500으로 퉁쳐버립니다. 이를 바꾸기 위해 response객체의 sendError() 메서드를 사용할 수 있습니다.

먼저 에러가 발생하면 이를 try{}catch{}등으로 받고, sendError()에 Http 상태코드, 에러 메세지를 담아 호출합니다.
(당장 오류가 발생하거나 하지 않음.)

그럼 서버는 정상적으로 서블릿 -> 인터셉터 -> 필터 -> WAS까지 응답이 갈텐데, 이 때 WAS가 sendError()의 호출 유무를 확인하고 만약 호출된 적이 있으면 에러처리를 그렇지 않으면 정상적으로 클라이언트에게 HTTP 응답 메세지를 보냅니다.

커스텀 오류 페이지

톰캣이 제공하는 오류 페이지로는 무슨 에러가 발생한건지 사용자가 알기는 어렵습니다.

개발자는 좀 더 사용자 친화적인 오류 페이지를 제공해야할 필요가 있습니다.

오류 페이지 등록

스프링 부트를 사용하지 않는 과거에는 web.xml에 다음처럼 오류페이지를 등록할 수 있었습니다.

<web-app>
  <error-page>
  	<error-code>404</error-code>
  	<location>/error-page/404.html</location>
  </error-page>
	<error-page>
  	<error-code>500</error-code>
  	<location>/error-page/500.html</location>
  </error-page>
  	<error-page>
  	<exception-type>java.lang.RuntimeException</exception-type>
  	<location>/error-page/500.html</location>
  </error-page>
</web-app>

"status 코드가 404면 /error-page/404.html을 보여줘라" 라는 식으로 말이지요.

스프링 부트를 사용한다면 다음 처럼 좀 더 간단하게 오류페이지를 등록할 수 있습니다.

참고
스프링 부트는 whitelabel이라는 자체 오류페이지를 제공하기 때문에 톰캣이 제공하는 오류 페이지를 보려면 resource/application.properties에 아래 코드를 추가시켜줘야한다.

server.error.whitelabel.enabled=false

다시 돌아와서

import org.springframework.boot.web.server.ConfigurableWebServerFactory;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

@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);
    }
}

WebServerFactoryCustomizer로 오류 페이지를 등록할 수 있습니다.

위 코드처럼

  • ErrorPage errorPage = new ErrorPage(Http 상태코드, 오류페이지 경로):
    response.sendError()에 설정한 상태코드를 받는다.

  • ErrorPage errorPage = new ErrorPage(Exception.class, 오류페이지 경로):
    발생한 예외 및 그 자식 예외 클래스를 받는다.

  • factory.addErrorPages(errorPage, ...)


오류 페이지 컨트롤러

위 처럼 오류페이지를 등록하고 예외가 발생하면 WAS는 지정해둔 URL로 일반적인 페이지 요청 신호를 보내기 때문에 이를 받을 컨트롤러가 필요합니다.

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@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";
	}
}

오류 페이지 html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
</head>
<body>
<div class="container" style="max-width: 600px">
    <div class="py-5 text-center">
        <h2>404 오류 화면</h2>
    </div>
    <div>
        <p>오류 화면 입니다.</p>
    </div>
    <hr class="my-4">
</div> <!-- /container -->
</body>
</html>
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
</head>
<body>
<div class="container" style="max-width: 600px">
    <div class="py-5 text-center">
        <h2>500 오류 화면</h2>
    </div>
    <div>
        <p>오류 화면 입니다.</p>
    </div>
    <hr class="my-4">
</div> <!-- /container -->
</body>
</html>

오류 정보 추가

WAS는 오류 페이지를 단순히 요청만 하는것이 아니라, 오류 정보를 request의 attribute에 추가해 넘겨줍니다.

덕분에 오류 페이지에서 이 정보를 사용할 수 있습니다.

오류 정보 관련 attribute의 key값.

  • javax.servlet.error.exception : 예외
  • javax.servlet.error.exception_type : 예외 타입
  • javax.servlet.error.message : 오류 메시지
  • javax.servlet.error.request_uri : 클라이언트 요청 URI
  • javax.servlet.error.servlet_name : 오류가 발생한 서블릿 이름
  • javax.servlet.error.status_code : HTTP 상태 코드

@Slf4j
@Controller
public class ErrorPageControllertemp {

    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";

    @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";
    }

    private void printErrorInfo(HttpServletRequest request) {
        log.info("ERROR_EXCEPTION_TYPE: {}", request.getAttribute(ERROR_EXCEPTION_TYPE));
        log.info("ERROR_MESSAGE: {}", request.getAttribute(ERROR_MESSAGE));
        log.info("ERROR_EXCEPTION: {}", request.getAttribute(ERROR_EXCEPTION));
        log.info("ERROR_REQUEST_URI: {}", request.getAttribute(ERROR_REQUEST_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());
    }
}

중복호출 제거


그림을 다시 보면 예외를 받던, sendError() 호출을 확인하던, WAS는 오류페이지를 위해 서블릿에 다시한번 요청을 보냅니다.

이 요청은 일반적인 HTTP 요청과 크게 다르지 않아, 필터 와 인터셉터를 다시 호출하게 됩니다.

필터와 인터셉터는 예외가 터지기 전 처음 Http 요청시에도 이미 통과했으므로 다시한번 이를 호출하는 것은 낭비일 수 있습니다.

이 때문에 기본적으로 오류 페이지 요청에는 필터가 호출되지 않습니다. 그러나 LOG를 찍는등 필요에 따라 필터를 호출해야 하는 경우가 있을 수 있습니다.

DispatcherType

WAS가 내부로 보내는 요청에는 여러가지 종류가 있습니다.
이를 DispatcherType 이라고 합니다.

request.getDispatcherType()를 찍어보면 타입을 알 수 있습니다.

타입 종류

  • REQUEST : 클라이언트 요청
  • ERROR : 오류 요청
  • FORWARD : MVC에서 배웠던 서블릿에서 다른 서블릿이나 JSP를 호출할 때
  • RequestDispatcher.forward(request, response);
  • INCLUDE : 서블릿에서 다른 서블릿이나 JSP의 결과를 포함할 때
  • RequestDispatcher.include(request, response);
  • ASYNC : 서블릿 비동기 호출

필터

필터는 이를 활용하기 위해 DispatcherType()이라는 메서드를 제공합니다.

    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<Filter>();
        filterRegistrationBean.setFilter(new LogFilter());
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.addUrlPatterns("/*");
        filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
        return filterRegistrationBean;
    }
  • filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);

위처럼 필터 등록시 DispatchetTypes을 지정해주면 정상적인 Request요청 외에도 필터를 호출 할 수 있습니다.

인터셉터

인터셉터는 서블릿이 제공하는 기능이 아니라 스프링이 제공하는 기능입니다. 따라서 DispatcherType과 무관하게 항상 호출됩니다.

대신 인터셉터는 이를 호출하는 url패턴을 지정하기 쉬워기 때문에 오류 페이지 요청 url을 제외 하는 방식으로 해결합니다.

@Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**", "*.ico", "/error", "/error-page/**");
        // DispatcherType을 지정할 수 없다. 대신 URL을 지정해서 오류페이지를 제외한다.
    }


<출처>
Inflearn 김영한 선생님, Spring MVC 2

profile
좋은 사람, 좋은 개발자 (되는중.. :D)

0개의 댓글