Spring

[Spring] 예외 처리와 오류 페이지

이덩우 2023. 7. 28. 18:42

사용자가 잘못된 URL로 접근하거나 서버 내부에서 문제가 발생하는 경우

사용자는 404 Not Found와 같은 오류 페이지를 만나게 된다.

 

아무래도 일반적인 사용자 입장에서 알 수 없는 오류가 가득 적힌 페이지를 마주하게 된다면

무슨 문제 때문인지 파악하기 어려울 것이다.

 

실제 이런 오류 페이지가 잘 설계된 웹 사이트를 보면 오류 페이지도 깔끔하고 무슨 문제가 있는지 잘 보여준다.

이런 예외 상황들을 어떻게 처리하고 오류 페이지를 사용자에게 보여주는 방법들에 대해서 알아보자.

 

예외 처리는 스프링 기술을 사용하지 않은 순수 서블릿 컨테이너에서의 예외 처리, 스프링이 지원하는 예외 처리 두 가지 방식에 대해서 알아볼 것이다.

 


 

- 서블릿의 예외 처리

서블릿은 다음 두 가지 방식으로 예외 처리를 지원한다.

  • XXXXException
  • response.sendError(HTTP 상태 코드, 오류 메시지)

 

Exception

  • 기본적으로 예외가 발생해서 처리하지 못하면 자신을 호출한 상위 레벨로 예외를 던지게 된다.
  • 자바의 메인 메소드를 직접 실행하는 경우, main이라는 이름의 쓰레드가 실행된다. 실행 도중 예외를 잡지 못하고 main메소드 조차 넘어서 밖으로 던져지게 되면, 예외 정보를 표시하고 해당 쓰레드를 종료한다.
  • 웹 애플리케이션은 사용자 요청별로 별도의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행된다.
  • 만약 Controller에서 예외가 터졌는데 아무도 예외를 잡지 않으면 서블릿을 벗어나 WAS까지 예외가 전달될 것이다.
  • WAS(전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
  • 결국 톰캣 같은 WAS에서 예외를 처리하게 된다.
  • 이 경우가 웹 페이지에서 404 Not Found를 만나는 상황이다!
  • 스프링이 지원하는 기본 오류 페이지 설정을 끄고, 실제로 WAS의 오류 페이지를 확인해보자.
  • 오류 상황을 만들기 위해 다음 컨트롤러를 만든다.
@Slf4j
@Controller
public class ServletExController {

    @GetMapping("error-ex")
    public void errorEx() {
        throw new RuntimeException("예외 발생");
    }

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

}

톰캣의 오류 페이지

  • 예외 처리 화면이 너무 별로다. 보여주고 싶은 오류 페이지를 만들고, 실제 오류 페이지로 등록하는 방법에 대해 알아보자.

 

 

오류 화면 제공

우선 다음과 같이 웹서버를 커스터마이징 할 수 있는 클래스를 만들어야 한다.

@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
    @Override
    public void customize(ConfigurableWebServerFactory factory) {
        ErrorPage error404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
        ErrorPage error500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");
        ErrorPage errorEx = new ErrorPage(RuntimeException.class, "/error-page/500");

        factory.addErrorPages(error404, error500, errorEx);
    }
}
  • 위에서 봤던 기본 오류 페이지 말고 다른 페이지를 보여주고 싶을 때 사용한다.
  • 임의의 오류를 만났을 때, 다른 URL을 호출해주는 모습이다.
  • 어? 그럼 WAS에서 컨트롤러까지 갔다가 예외가 발생해서 다시 WAS까지 예외가 던져지고, 다시 어떤 컨트롤러 내 URL을 호출하는 것이다.
  • 총 2번 왔다갔다 하게 된다.
  • 그렇다면 오류 상황시 넘겨지는 URL을 처리하기 위해 에러처리 담당 컨트롤러를 또 만들어보자.
@Slf4j
@Controller
public class ErrorPageController {

    //RequestDispatcher 상수로 정의되어 있음
    public static final String ERROR_EXCEPTION = "jakarta.servlet.error.exception";
    public static final String ERROR_EXCEPTION_TYPE = "jakarta.servlet.error.exception_type";
    public static final String ERROR_MESSAGE = "jakarta.servlet.error.message";
    public static final String ERROR_REQUEST_URI = "jakarta.servlet.error.request_uri";
    public static final String ERROR_SERVLET_NAME = "jakarta.servlet.error.servlet_name";
    public static final String ERROR_STATUS_CODE = "jakarta.servlet.error.status_code";


    @RequestMapping("/error-page/404")
    public String error404(HttpServletRequest request, HttpServletResponse response) {
        log.info("404 에러 발생!");
        printErrorInfo(request);
        return "error-page/404";
    }
    @RequestMapping("/error-page/500")
    public String error500(HttpServletRequest request, HttpServletResponse response) {
        log.info("500 에러 발생!");
        printErrorInfo(request);
        return "error-page/500";
    }

    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));
        log.info("ERROR_SERVLET_NAME: {}", request.getAttribute(ERROR_SERVLET_NAME));
        log.info("ERROR_STATUS_CODE: {}", request.getAttribute(ERROR_STATUS_CODE));
    }
}
  • 위에서 에러 상황 발생시 재요청되는 URL 정보들을 처리하기 위한 메소드를 만들었다.
  • 각 메소드들은 사전에 만들어놓은 실제 보여지길 원하는 오류 페이지로 렌더링하는 모습이다.
  • 예외 발생시 오류 페이지 요청 흐름을 정리해보자.
    1. WAS(예외 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외 발생)
    2. WAS '/error-page/404' 다시 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(/error-page/404) -> View 반환
  • 다시 말하면 총 2번 왔다갔다 하는 것이다.
  • 근데 필터나 인터셉터의 경우, 이미 첫번째 요청 시 원하는 동작을 수행했는데 예외 처리로 인한 2번째 요청 시에도 필터나 인터셉터를 적용하면 굉장히 비효율적일 것이다.
  • 심지어 필터나 인터셉터를 중복적용하면 안되는 상황이 있을 수 있다.
  • 필터와 인터셉터에서 각각 이 상황을 어떻게 처리하는지 한번 알아보자.

 

필터의 중복 호출 제거

  • 필터는 이런 경우를 위해서 dispatcherTypes라는 옵션을 제공한다.
  • request.getDispatcherType()을 꺼내보면 REQUEST , ERROR 등이 담겨있다.
  • 최초 요청시에는 클라이언트의 요청으로 흐름이 시작되므로 REQUEST가 조회되고,
  • 예외 처리로 인해 재요청이 발생하면 ERROR가 조회된다.
  • 정리하자면 현재 요청이 최초 요청인지, 예외 처리로 인한 두 번째 요청인지 구분할 수 있다는 것이다!
  • 그럼 해당 필터를 스프링 빈으로 등록하는 WebConfig에서 이 DispatcherType을 구분해보자
    @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;
    }
  • 마지막 줄을 보면 허용할 DispatcherTypes을 지정해주고 있다.
  • 현재는 REQUEST, ERROR 모두 허용하게 만들어 필터가 두 번 동작하게 설정해놨다.
  • 아무것도 적지 않은 디폴트 값은 REQUEST만 허용하는 설정이다.
  • 따라서 예외 요청 시에도 필터를 호출하고 싶은게 아니라면 설정을 따로 건드리지 않아도 된다.

 

인터셉터의 중복 호출 제거

  • 인터셉터의 경우 서블릿의 필터처럼 DispatcherType을 스프링 빈으로 등록할 때 따로 지정하는 기능이 없다.
  • 하지만 알다시피 인터셉터는 excludePathPatterns라는 아주 강력한 기능을 갖고있다.
  • 인터셉터를 적용시키는 범위에서 오류 페이지로 향하는 URL을 차단시켜버리면 간단하게 끝난다.
@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/**");

    }
   
}

 

 


 

- 스프링 부트의 예외 처리

지금까지 예외 처리 페이지를 만들기 위해서 굉장히 복잡한 과정을 거쳤다.

  • WebServerCustomizer 만들기
  • ErrorPage 직접 추가
  • 예외 처리용 컨트롤러 설계

 

스프링 부트는 이 모든 과정을 기본으로 제공한다.

ErrorPage를 자동으로 등록해주고, 컨트롤러에서 사용할 기본 오류페이지의 경로를 "/error"로 설정한다.

컨트롤러도 마찬가지로 직접 등록해야하는 것이 아닌 BasicErrorController라는 스프링 컨트롤러를 자동으로 등록한다.

 

그럼 뭐만하면 되나?

자동으로 등록된 컨트롤러의 스펙에 맞춰 지정된 디렉토리 위치에 오류 페이지 파일만 넣어주면 된다!

컨트롤러에서 지정된 디렉토리 위치, 또 스펙은 뭘까?

 

뷰 선택 우선순위

  1. 뷰 템플릿
    • resources/templates/error/500.html
    • resources/templates/error/5xx.html
  2. 정적 리소스(static, public)
    • resource/static/error/400.html
    • resource/static/error/4xx.html
  3. 위 두 가지 경로에 적용 대상이 없을 때, 'error'라는 뷰 이름으로 직접 찾음
    • resources/templates/error.htm

실제로 5xx.html과 같이 상태 코드를 특정짓지 않고 파일명을 작성하면 500번대의 모든 오류를 포함할 수 있지만,

500.html과 같이 더 자세한 정보가 있으면 우선순위가 밀린다.

이렇게 지정된 위치에 지정된 이름으로 뷰 파일을 넣기만 하면 알아서 스프링 부트가 오류 페이지를 보여준다.

 

 

 

 

출처 : 인프런, 김영한의 스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술