둘셋 개발!

[spring mvc 2편-웹 개발 활용 기술 - 6 ] 예외처리와 오류 페이지 본문

SPRING/MVC

[spring mvc 2편-웹 개발 활용 기술 - 6 ] 예외처리와 오류 페이지

23 2022. 2. 16. 02:55

예외처리

스프링이 어떻게 예외를 처리하는지 알아보기 전에

서블릿은 어떻게 처리하는 지부터 알아보자


1. 서블릿의 예외 처리

서블릿이 예외를 처리하는 2가지 방식

1. Exception
2. response.sendError(HTTP 상태 코드, 오류 메세지)

(1)  Exception 

웹 애플리케이션은 사용자 요청별로 별도의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행된다.

만약에 서블릿 컨테이너 안에서 예외가 발생하면 어떻게 될까?

바로 톰캣 같은 WAS까지 예외가 전달된다.

WAS (여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러 (예외발생) 

예외가 발생되면 톰캣같은 경우는 기본적으로 제공하는 오류화면을 보여준다.

Exception의 경우 서버 내부에서 해결할 수 없는 오류라고 생각해서 HTTP 상태코드 500을 반환한다.

 

(2) response.sendError(HTTP 상태코드, 오류메세지)

response.sendError()를 호출하면 response 내부에 오류가 발생했다는 것을 저장해둔다.

그러면 서블릿 컨테이너는 고객에게 응답하기 전에 response에 오류가 있는지 확인한다.

만약에 오류가 있으면 설정한 오류코드에 맞춰서 오류페이지를 보여준다.

WAS (sendError호출 확인) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러 ( response.sendError() ) 

 

 

서블릿의  오류 화면 제공

서블릿 컨테이너가 제공하는 기본적인 예외 페이지가 고객 입장에서 보면 큰일이 난 것 같은 그런 느낌을 준다.

서블릿 컨테이너의 기본적인 예외 페이지

 

여기서는 스프링 부트를 통해서 서블릿 컨테이너를 실행하기 때문에, 스프링 부트가 제공하는 기능을 사용해서 

서블릿 오류페이지를 등록하면 된다.

 

서블릿 오류 페이지 등록

@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
    @Override
    public void customize(ConfigurableServletWebServerFactory 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);
    }
}

( url 경로에 /error-page/404 , /error-page/500, /error-ex를 쓰면 오류가 발생하도록 이미 만들어 놓았다)

 

- 그냥 예외가 발생한 경우도 서버 내부에서 발생한 경우이기 때문에 500오류 화면으로 처리

- 오류페이지는 예외를 다룰 때 예외의 자식 타입의 오류도 처리

 

이렇게 오류 페이지를 등록하게 되면 , 예외가 발생할 경우 WAS가 해당 예외의 오류 페이지를 찾을 수 있다.

예를 들어 404오류 발생되면 WAS는 오류 페이지를 출력하기 위해  /error-page/404 를 요청한다.

그러면 다시 필터 -> 서블릿 -> 인터셉터 를 거쳐 컨트롤러에서 /error-page/404를 받아 오류View를 보여준다.

 

오류를 처리할 컨트롤러

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

 

- WAS는 오류 페이지 뿐만 아니라 오류 정보를 request.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 상태 코드

 

정리!!

1 (1). 예외 발생 - Exception

WAS (여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러 (예외발생) 

 

1 (2). 예외 발생 - sendError 호출

WAS (sendError호출 확인) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러 ( response.sendError() ) 

 

2. WAS의 오류페이지 요청 후 컨트롤러의 처리

WAS '/error-page/404' 다시 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 -> View

여기서 문제점은 필터와 인터셉터가 2번씩 호출된다는 것이다.

클라이언트의 요청으로 필터/인터셉터가 호출된 것인지, 오류 페이지를 호출 하기 위해 호출된 것인지 구분할 수 있어야 한다.

그래서 서블릿 필터의 경우 DIspatcherType이라는 추가정보를 제공하고

스프링 인터셉터의 경우 excludePathPatterns을 이용한다.

 

1. 서블릿 필터

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

setDispatcherTypes에 DispatcherType.ERROR를 넣으면 오류 페이지 요청에도 필터를 호출한다는 의미이다.

따라서 오류시 필터를 중복으로 호출하고 싶지 않으면 DispatcherType.ERROR 를 넣지 않으면 된다.

 

2. 스프링 인터셉터

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

excludePathPatterns에 오류페이지 경로를 추가하면 오류페이지경로는 인터셉터가 적용되지 않는다.

 


스프링 부트로 한번에 해결하자 (사실 이것만 하면 됨......)

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

WebServerCustomizer를 만들고,  예외종류에 따라 에러페이지를 추가하고,  예외 처리용 컨트롤러를 만들었다.

 

스프링 부트는 이런 과정을 기본적으로 제공한다

- ErrorPage를 자동으로 등록해줌
             이때 별도로 상태코드와 예외를 설정하지 않으면 /error 라는 경로로 기본 오류 페이지 설정
- BasicErrorController라는 스프링 컨트롤러를 자동으로 등록해줌
            ErrorPage에서 등록한 /error를 매핑해서 처리

 

그러면 개발자는 오류 페이지 화면만 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

 

 

BasicErrorController가 제공하는 기본 정보들

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.properties에 다음과 같이 추가하면 된다.

server.error.include-exception=false //exception 포함여부
server.error.include-message=never  //message 포함여부
server.error.include-stacktrace=never  //trace 포함여부
server.error.include-binding-errors=never  //errors 포함여부

첫번째 줄 말고는 기본옵션이 never이다.

이 부분들은 always(항상 사용), on_param(파라미터가 있을 때 사용) 으로도 변경할 수 있다.

 


결론

오류 페이지를 관리하고 싶다면 BasicErrorController가 제공하는 룰과 우선순위에 따라서 등록만 하면 된다.

그리고 오류페이지는 고객이 이해할 수 있는 간단한 오류 메세지만 보여주고 나머지는 서버에 로그로 남겨서 확인한다.

 

(참고 : 인프런 김영한 강사님 - 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 )