Servlet 예외 처리

YH·2023년 4월 25일
0

✅ Servlet 예외 처리 - 시작

✔️ 해당 포스트에서는 스프링이 아닌 순수 서블릿 컨테이너가 어떻게 예외를 처리하는지 정리해본다.

✔️ 서블릿의 예외 처리 방식

  1. Exception(말그대로 예외 발생, RuntimeException 등..)
  2. response.sendError(Http 상태 코드, 오류 메시지)

1. Exception

  • 웹 애플리케이션에서 Exception 발생 시
    • 웹 애플리케이션은 사용자 요청별로 별도 쓰레드가 생성되고, 서블릿 컨테이너 안에서 실행된다.
    • 웹 애플리케이션에서 예외를 잡지 못하면 아래와 같이 WAS(Tomcat)까지 전달된다.
      WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
    • 이 경우에는, 톰캣에서 기본으로 제공하는 오류 화면이 페이지에 표시된다. (404, 500 등등)

2. response.sendError(Http 상태 코드, 오류 메시지)

  • 오류 발생 시, HttpServletResponse가 제공하는 sendError 메소드를 사용할 수 있다.
  • 해당 메소드를 호출 시, 바로 예외가 발생하는 것은 아니지만 서블릿 컨테이너에게 오류가 발생했다는 점을 전달할 수 있다.
  • sendError() 흐름
    • WAS(sendError 호출 기록 확인) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러 (response.sendError())
  • response.sendError() 를 호출하면 response 내부에는 오류가 발생했다는 상태를 저장해둔다. 그 후 서블릿 컨테이너가 클라이언트에 응답 전에 response에 sendError()가 호출되었는지 확인 후 호출된 경우 오류 코드에 맞추어 오류 페이지를 출력한다.

✅ 서블릿 예외 처리 - 오류 화면 제공

✔️ 서블릿은 Exception이 발생해서 WAS까지 전달되거나 response.sendError()가 호출되었을 때, 상황에 맞춘 오류 처리 기능을 제공한다. 즉, 오류에 따라 출력해줄 화면을 설정해줄 수 있는 것이다.

@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);
    }
}
  • 만약 response.sendError(404)가 호출되면, errorPage404 호출
  • response.sendError(500)가 호출되면, errorPage500 호출
  • RuntimeException 또는 그 자식 타입의 Exception 발생 시, errorPageEx 호출

✔️ 아래와 같이 해당 ErrorPage의 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";
    }
}

✅ 서블릿 예외 처리 - 오류 페이지 동작 원리

✔️ 위에서 정리한 오류 페이지가 어떤식으로 동작하는지 원리를 정리해본다.

예외 발생과 오류 페이지 요청 흐름

  1. WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
  2. WAS /error-page/500 다시 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(/errorpage/
    500) -> View
  1. 예외가 발생하면 WAS까지 전파된다.
  2. WAS는 오류 페이지 경로를 찾아서 오류 페이지를 호출한다.
    (new ErrorPage(RuntimeException.class, "/error-page/500" 정보를 찾는 것이다.)
  3. 이 때 오류 페이지 경로로 필터, 서블릿, 인터셉터, 컨트롤러가 모두 다시 호출된다.

ErrorPageController - 오류 출력

  • 예외 발생 후 WAS에서는 오류 페이지를 요청하는 것 말고도 request의 attribute에 오류 정보를 담아서 넘겨준다.
  • WAS에서 reuqest.attribute에 담아준 정보 (아래 정보 말고도 더 있지만 주요 내용만 나열한다.)
    • 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 상태 코드

✅ 서블릿 예외 처리 - 필터

  • 위에서 정리한 내용 중에, WAS에서 예외 발생 시 오류 페이지를 요청할 때 필터, 서블릿, 인터셉터, 컨트롤러가 모두 다 호출된다고 했다. 그런데 로그인 인증 체크의 경우 필터나 인터셉터를 통해 이미 완료된 상태인데, 다시 필터나 인터셉터가 호출되는 것은 매우 비효율적이다. 서블릿은 이런 문제를 해결하기 위해 DispatcherType 정보를 통해 정상 요청인지 혹은 오류 페이지를 위한 내부 요청인지를 구분할 수 있다.
  • 예외 처리에 따른 필터와 DispatcherType에 대해 정리한다.

✔️ DispatcherType

  • DispatcherType 은 enum으로 구성되어 있고 아래 값들이 존재한다.
public enum DispatcherType {
  FORWARD, // 서블릿에서 다른 서블릿이나 JSP를 호출할 때: RequestDispatcher.forward(request, response);
  INCLUDE, // 서블릿에서 다른 서블릿이나 JSP의 결과를 포함할 때: RequestDispatcher.include(request, response);
  REQUEST, // 클라이언트 요청
  ASYNC, // 서블릿 비동기 호출
  ERROR // 오류 요청
}

✔️ 필터와 DispatcherType이 어떻게 사용되는가?

  • 필터 소스 코드는 생략하고 실제 Filter를 설정하는 Config 부분에서 어떻게 사용되는지 알아보자.
@Configuration
public class WebConfig {

    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;
    }
}
  • filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR); : 이렇게 setDispatcherTypes() DispatcherType을 적용하면, 적용된 타입인 경우에 필터가 호출된다.
  • 기본 값은 DispatcherType.REQUEST 이므로 설정되지 않은 경우 클라이언트 요청인 경우에만 필터가 호출된다.

✅ 서블릿 예외 처리 - 인터셉터

✔️ 그렇다면 인터셉터에서는 DispatchType을 어떻게 사용할 수 있을까?
→ 인터셉터는 스프링이 제공하는 기능이므로 DispatcherType과 무관하게 항상 호출된다.
✔️ 때문에, 인터셉터는 DispatchType 대신에 excludePathPatterns를 사용해서 에러 페이지에 대한 경로를 제외해주면 된다.

//WebConfig
@Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**", "*.ico", "/error", "/error-page/**");
    }
  • "/error", "/error-page/**" 처럼 에러 페이지 경로를 제외해주면 된다.

✅ 스프링 부트 - 오류 페이지 1

✔️ 위에서 서블릿에서 동작하는 예외 처리 페이지를 학습했는데, 서블릿은 아래와 같은 복잡한 과정을 거친다.

  • WebServerCustomizer 클래스 생성
  • 예외 종류에 따라 ErrorPage 추가
  • 예외 처리용 컨트롤러 ErrorPageController를 생성

✔️ 스프링 부트는 위의 과정들을 모두 기본으로 제공한다.

  • 스프링 부트는 기본적으로 /error 라는 경로로 기본 ErrorPage를 자동으로 등록해둔다.
    • new ErrorPage("/error") 와 같고, 별도로 상태코드와 예외를 설정하지 않으면 이 기본 오류 페이지가 사용된다.
    • 서블릿 밖으로 예외가 발생되거나 response.sendErrr()가 호출되면 모든 오류는 /error 경로의 에러 페이지를 호출한다.
  • new ErrorPage("/error") 로 등록한 /error를 매핑해서 처리하기 위해 BasicErrorController 라는 스프링 컨트롤러를 자동으로 등록한다.
  • ErrorMvcAutoConfiguration 라는 클래스가 오류 페이지를 자동으로 등록하는 역할을 한다.

✔️ 개발자는 오류 페이지만 등록하면 된다.
BasicErrorController의 뷰 선택 우선 순위가 있는데 아래와 같다.

  1. 뷰 템플릿
    ex) resources/templates/error/500.html
    ex) resources/templates/error/5xx.html
  2. 정적 리소스(static, public)
    ex) resources/static/error/400.html
    ex) resources/static/error/4xx.html
  3. 적용 대상이 없을 때 뷰 이름(error)
    ex) resources/templates/error.html
  • 4xx.html, 5xx.html 은 400대, 500대 오류를 처리해준다.

✅ 스프링 부트 - 오류 페이지 2

✔️ BasicErrorController 는 아래의 정보들을 model에 담아서 뷰에 전달해준다. 뷰 템플릿은 이 값들을 활용해서 출력할 수 있다.

  • timestamp: Fri Feb 05 00:00:00 KST 2021
  • status: 400
  • error: Bad Request
  • exception: org.springframework.validation.BindException
  • trace: 예외 trace
  • message: Validation failed for object='data'. Error count: 1
  • errors: Errors(BindingResult)
  • path: 클라이언트 요청 경로 (/hello)

✔️ 그런데 기본적으로 오류 관련 내부 정보는 클라이언트에게 노출하는 것이 좋지 않기 때문에 기본적으로 출력되지 않는 오류 정보들도 있는데, 이런 정보들을 설정 파일(application.yaml)을 통해 model에 다 포함할지에 대한 여부를 선택할 수 있다.

  • 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 포함 여부

✔️ 위에서 기본 값이never 인 설정들은 아래 3가지 옵션을 사용할 수 있다. 디버그 시 문제확인을 위해 사용할 수 있지만 개발 서버에서만 사용하고 운영 서버에서는 사용하지 않는 것이 좋다.

  • never : 사용 안함
  • always : 항상 사용
  • on_param : 파라미터가 있을 때 사용, ex) message=&errors=&trace=

스프링 부트 오류 관련 옵션

  • server.error.whitelabel.enabled=true : 오류 처리 화면을 못 찾을 시, 스프링 whitelabel 오류 페이지 적용
  • server.error.path=/error : 오류 페이지 경로, 스프링이 자동 등록하는 서블릿 글로벌 오류 페이지 경로와 BasicErrorController 오류 컨트롤러 경로에 함께 사용된다.

확장 포인트

  • 에러 공통 처리 컨트롤러 기능을 변경하고 싶은 경우, ErrorController 인터페이스를 상속 받아서 구현하거나 BasicErrorController 를 상속 받아서 기능을 추가할 수 있다.
profile
하루하루 꾸준히 포기하지 말고

0개의 댓글