본문 바로가기
Backend/Spring | SpringBoot

[SpringBoot] 예외 처리 - 오류 페이지

by 2245 2023. 9. 26.

목차

     

    서론

    서블릿과 스프링에서 각각 예외를 처리하는 방법에 대해 알아보겠습니다.

    예외 발생에는 다음과 같이 2가지 방식이 있습니다.

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

     

    Exception (예외)

    • 웹 애플리케이션은 사용자 요청별로 쓰레드가 할당이 되고, 서블릿 컨테이너 안에서 실행이 됩니다.
    • 만약, 애플리케이션에서 예외가 발생했을 때, try~catch로 예외를 잡아서 처리하지 않는다면 다음과 같은 방향으로 예외가 전파됩니다.
    WAS(여기까지 전파) ← 필터 ← 서블릿 ← 인터셉터 ← 컨트롤러 (예외발생)

     

    • 우선 톰캣과 같은 WAS는 예외를 어떻게 처리할까요?
    • 이를 확인하기 위해선 우선, 스프링 부트가 제공하는 기본 예외 페이지 설정을 false로 변경해야 합니다.

    application.properties

    server.error.whitelabel.enabled=false

    ServletExController - 서블릿 예외 발생 컨트롤러

    @Slf4j
    @Controller
    public class ServletExController {
    
        @GetMapping("/error-ex")
        public void errorEx() {
            throw new RuntimeException("예외 발생!");
        }
    }

    실행

    tomcat이 기본으로 제공하는 오류 화면을 볼 수 있습니다.

     

    • Exception의 경우, 서버 내부에서 처리할 수 없는 오류가 발생한 것으로 생각해 HTTP 상태 코드 500을 반환합니다.

    아무 사이트 호출

    • http://localhost:8080/no-page
    • 톰캣이 기본으로 제공하는 404 오류 화면을 볼 수 있습니다. 

     

     

     

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

    • 오류를 발생했을 때, HttpServletResponse가 제공하는 sendError라는 메서드를 사용할 수 있습니다.
    • 해당 메서드를 사용해 서블릿 컨테이너에게 오류가 발생했다는 점을 전달할 수 있습니다.
    • 이 메서드를 사용하면 HTTP 상태 코드와 오류 메시지를 추가할 수 있습니다. 
      • response.sendError(HTTP 상태 코드)
      • response.sendError(HTTP 상태 코드, 오류 메시지)

    ServletExController - 예외 발생 추가

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

    sendError 흐름

    WAS(sendError 호출 기록 확인) ← 필터 ← 서블릿 ← 인터셉터 ← 컨트롤러(response.sendError())
    • 컨트롤러에서 response.sendError()를 호출하면 response 내부에는 오류가 발생했다는 상태가 저장이 됩니다.
    • 서블릿 컨테이너는 고객에게 응답하기 전에 response에 sendError()가 호출되었는지 확인합니다. 
    • 호출이 되었다면 설정한 오류 코드에 맞춰 기본 오류 페이지를 보여줍니다.

    실행

    서블릿 컨테이너가 기본으로 제공하는 오류 화면을 볼 수 있습니다. 

    • http://localhost:8080/error-404
    • http://localhost:8080/error-500

     

     

    이번엔 서블릿 컨테이너가 기본으로 제공하는 기본 예외 처리 화면말고, 의미 있는 오류 화면을 제공해 봅시다.

    서블릿 - 오류 화면 제공

    서블릿 컨테이너가 제공하는 기본 예외 처리 화면 말고 서블릿이 제공하는 오류 화면 기능을 사용해 봅시다.

    • 서블릿은 Exception이 발생해서 서블릿 밖으로 전달이 되었거나 response.sendError()가 호출되었을 때, 각각의 오류 상황에 맞춘 처리 기능을 제공합니다.
    • 과거에는 web.xml이라는 파일을 사용하여 오류 코드에 맞춘 오류 화면을 등록했지만, 지금은 스프링 부트를 통해 서블릿 컨테이너를 실행하기 때문에 스프링 부트가 제공하는 기능을 사용해 서블릿 오류 페이지를 등록하면 됩니다.

    WebServerCustomizer - 서블릿 오류 페이지 등록

    @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)  →  /error-page/404 호출
    • response.sendError(500) → /error-page/500 호출
    • RuntimeException 또는 그 자식 타입의 예외 → /error-page/500 호출
      • 오류 페이지는 예외를 다룰 때 해당 예외와 그 자식 타입의 오류도 함께 처리합니다. 

    ErrorPageController - 오류 처리 컨트롤러

    @Slf4j
    @Controller
    public class ErrorPageController {
        
        @RequestMapping("/error-page/404")  //모든 HTTP method 처리
        public String errorPage404(HttpServletRequest request, HttpServletResponse response) {
            log.info("errorPage 404");
            return "error-page/404";   //view 호출
        }
        
        @RequestMapping("/error-page/500")
        public String errorPage500(HttpServletRequest request, HttpServletResponse response) {
            log.info("errorPage 500");
            return "error-page/500";
        }
    }

    오류 처리 View 

    /templates/error-page/404.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>

     

    /templates/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>500 오류 화면</h2>
      </div>
      <div>
        <p>오류 화면 입니다.</p>
      </div>
      <hr class="my-4">
    </div> <!-- /container -->
    </body>
    </html>

    실행

    • http://localhost:8080/error-ex
    • http://localhost:8080/error-404
    • http://localhost:8080/error-500

     

    서블릿 - 오류 페이지 작동 원리

    서블릿은 Exception(예외)가 발생해서 서블릿 밖으로 전달이 되거나, response.sendError()가 호출되었을 때 설정된 오류 페이지를 찾습니다.

     

    예외 발생 흐름

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

     

    sendError 흐름

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

     

    1. WAS는 예외가 도달하면, 해당 예외 처리를 담당하는 오류 페이지 정보를 확인합니다. ex) ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, "/error-page/500");
    2. 해당 예외 페이지로 /error-page/500이 지정되어 있습니다. WAS는 오류 페이지를 출력하기 위해 /error-page/500 을 다시 호출합니다. 

     

    오류 페이지 요청 흐름

    WAS(`/error-page/500` 다시 요청) → 필터 → 서블릿 → 인터셉터 → 컨트롤러(/error-page/500) → View

     

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

    1. WAS (여기까지 전파) ← 필터 ← 서블릿 ← 인터셉터 ← 컨트롤러 (예외발생)
    2. WAS (`/error-page/500` 다시 요청) → 필터 → 서블릿 → 인터셉터 → 컨트롤러 (/error-page/500) → View

     

    중요 클라이언트는 다시 호출하는 것을 모른다.
    웹 브라우저(클라이언트)는 서버 내부에서 이런 일이 일어나는지 전혀 모릅니다. 
    오직 서버 내부에서 오류 페이지를 찾기 위해 추가적인 호출을 합니다. 

     

     

    필터와 인터셉터 중복 호출

    • 위에 전체 흐름을 다시 보면 오류가 발생했을 때 오류 페이지를 출력하기 위해 필터, 서블릿, 인터셉터가 모두 다시 호출됩니다.
    • 로그인에서 사용하는 인증 체크를 수행하는 필터나 인터셉터의 경우, 이미 요청 시에 필터나 인터셉터에서 로그인 인증 체크를 완료했습니다. 즉, 오류 페이지를 호출하기 위해 해당 필터나 인터셉터가 한번 더 호출되는 것은 매우 비효율적입니다.
    • 결국 클라이언트로부터 발생한 정상적인 요청인지, 아니면 오류 페이지를 출력하기 위한 서버 내부의 요청인지 구분할 수 있어야 합니다.
    • 서블릿은 이러한 구별을 위해 DispatcherType이라는 추가 정보를 제공합니다.

     

    DispatcherType

    • dispatcherType=ERROR : 서버 내부에서 오류 페이지 호출
    • dispatcherType=REQUEST  : 고객이 처음으로 요청

    javax.servlet.DispatcherType

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

     

    필터와 DispatcherType

    필터에서 DispatcherType이 어떻게 쓰이는지 알아봅시다.

    LogFilter - DispatcherType 로그 추가

    @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 {
                //dispatcherType 추가
                log.info("REQUEST [{}][{}][{}]", uuid, request.getDispatcherType(), requestURI);	
                chain.doFilter(request, response);
            } catch(Exception e) {
                throw e;
            } finally {
                //dispatcherType 추가
                log.info("RESPONSE [{}][{}][{}]", uuid, request.getDispatcherType(), requestURI);
            }
        }
    
        @Override
        public void destroy() {
            log.info("log filter destroy");
        }
    }

    WebConfig

    @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;
        }
    }
    filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
    • DispatcherType이 REQUEST인 경우와 ERROR인 경우 둘 다 해당 필터를 적용합니다. 
    • 즉, 클라이언트 요청과 오류 페이지 요청 둘 다 필터가 적용이 됩니다. 
    • 아무것도 넣지 않는다면 기본 값은 DispatcherType.REQUEST입니다.
    • 즉, 클라이언트의 요청이 있는 경우에만 필터가 적용됩니다. 
    • 특별히 오류 페이지 경로도 필터를 적용할 것이 아니면 기본 값을 그대로 사용하면 됩니다.

    실행 로그

    REQUEST [31d49e67-b518-4f00-b64f-81342e8b3b70][ERROR][/error-page/500]
    errorPage 500
    ERROR_EXCEPTION: {}
    ERROR_EXCEPTION_TYPE: class java.lang.RuntimeException
    ERROR_MESSAGE: Request processing failed; nested exception is java.lang.RuntimeException: 예외 발생!
    ERROR_REQUEST_URI: /error-ex
    ERROR_SERVLET_NAME: dispatcherServlet
    ERROR_STATUS_CODE: 500
    dispatchType=ERROR
    RESPONSE [31d49e67-b518-4f00-b64f-81342e8b3b70][ERROR][/error-page/500]

     

     

    인터셉터와 중복 호출 제거

    • 앞서 필터의 경우는 필터를 등록할 때 어떤 DispatcherType인지에 따라 필터를 적용할 것인지 선택할 수 있었습니다.
    • 하지만, 인터셉터는 서블릿이 제공하는 기능이 아니라 스프링이 제공하는 기능입니다.
    • 따라서, DispatcherType과 무관하게 항상 호출됩니다.
    • 대신, 인터셉터는 excludePathPatterns를 사용해 요청 경로를 추가하거나 제외하기가 쉽기 때문에, 해당 설정을 사용해 오류 페이지 요청은 인터셉터를 제외하도록 설정할 수 있습니다.

    LogInterceptor - DispatcherType 로그 추가

    @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);
            
            //dispatcherType 추가
            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);
            
            //dispatcherType 추가
            log.info("RESPONSE [{}][{}][{}]", logId, request.getDispatcherType(), requestURI);
            if(ex != null) {
                log.error("afterCompletion error!!", ex);
            }
        }
    }

    WebConfig

    @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/**"    //오류 페이지 경로
                    );
        }
    }

     

    전체 흐름 정리

    /hello 정상 요청

    WAS(/hello, dispatchType=REQUEST) → 필터 → 서블릿 → 인터셉터 → 컨트롤러 → View

     

    /error-ex 오류 요청

    • 필터 : DispatcherType으로 중복 호출 제거 (dispatcherType=REQUEST)
    • 인터셉터 : 제외 경로 지정으로 중복 호출 제거 (excludePathPatterns("/error/page/**");
    1. WAS(/error-ex, dispatchType=REQUEST) → 필터 → 서블릿 → 인터셉터 → 컨트롤러
    2. WAS(여기까지 전파) ← 필터 ← 서블릿 ← 인터셉터 ← 컨트롤러(예외발생)
    3. WAS 오류 페이지 확인
    4. WAS(/error-page/500, dispatchType=ERROR) → 필터(x) → 서블릿 → 인터셉터(x) → 컨트롤러(/error-page/500) → View

     

     

    서블릿 - 추가 오류 정보 제공

    • WAS는 단순히 오류 페이지를 호출하는 것으로 끝나지 않습니다.
    • 오류 정보를 request의 attribute에 담아 넘겨줍니다.
    • 필요하다면 오류 페이지에서 해당 오류 정보를 사용할  수 있습니다.

    ErrorPageController - 오류 정보 출력

    @Slf4j
    @Controller
    public class ErrorPageController {
    
        //RequestDispatcher에 상수로 정의되어 있음
        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: ex=", request.getAttribute(ERROR_EXCEPTION));
            log.info("ERROR_EXCEPTION_TYPE: {}", request.getAttribute(ERROR_EXCEPTION_TYPE));
            log.info("ERROR_MESSAGE: {}", request.getAttribute(ERROR_MESSAGE)); //ex의 경우 NestedServletException 스프링이 한번 감싸서 반환
            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());
        }
    }

    request.attribute에 WAS가 담아준 정보

    • 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 상태 코드

    실행

    http://localhost:8080/error-ex

    ERROR_EXCEPTION_TYPE: class java.lang.RuntimeException
    ERROR_MESSGE: Request processing failed; nested exception is java.lang.RuntimeException: 예외 발생!
    ERROR_REQUEST_URI: /error-ex
    ERROR_SERVLET_NAME: dispatcherServlet
    ERROR_STATUS_CODE: 500
    dispatchType=ERROR

     

     

    스프링 부트 - 오류 화면 제공

    지금까지 서블릿이 예외 처리 페이지를 제공하기 위해 했던 복잡한 과정을 스프링 부트는 기본으로 제공합니다.

    • ErrorPage 등록 → /error 라는 경로로 자동으로 기본 오류 페이지를 설정합니다. 
      • new ErrorPage("/error")
      • 상태 코드와 예외를 설정하지 않으면 기본 오류 페이지로 사용이 됩니다.
      • 서블릿 밖으로 Exception이 발생하거나 response.sendError()가 호출되면, 해당하는 모든 오류는 /error 를 호출하게 됩니다.
    • ErrorPageController 등록 →  BasicErrorController라는 스프링 컨트롤러를 자동으로 등록: Errorpage에서 등록한 /error 를 매핑해서 처리하는 컨트롤러입니다.  
    참고 오류 페이지 자동 등록
    ErrorMvcAutoConfiguration이라는 클래스가 오류 페이지를 자동으로 등록합니다. 
    주의
    스프링 부트가 제공하는 기본 오류 매커니즘을 사용하기 위해선, WebServerCutomizer에 있는 @Component를 주석처리해야  합니다. 

     

    개발자는 오류 페이지만 등록한다.

    • BasicErrorController에는 기본적인 로직이 모두 개발되어 있습니다.
    • 개발자는 오류 페이지 화면만 BasicErrorController가 제공하는 룰과 우선순위에 따라 등록하면 됩니다.
    • 정적 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. 적용 대상이 없을 때 
      • resources/templates/error.html
    • 뷰 템플릿이 정적 리소스보다 우선순위가 높고, 404, 400처럼 구체적인 것이 4xx처럼 덜 구체적인 것보다 우선순위가 높습니다.
    • 5xx, 4xx 라고 하면 500대, 400대 오류를 처리해 줍니다.

     

    오류 뷰 템플릿 추가

    resources/templates/error/4xx.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>4xx 오류 화면 스프링 부트 제공</h2>
        </div>
        <div>
            <p>오류 화면 입니다.</p>
        </div>
        <hr class="my-4">
    </div> <!-- /container -->
    </body>
    </html>

    resources/templates/error/404.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>

    resources/templates/error/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>500 오류 화면 스프링 부트 제공</h2>
        </div>
        <div>
            <p>오류 화면 입니다.</p>
        </div>
        <hr class="my-4">
    </div> <!-- /container -->
    </body>
    </html>

    등록한 오류 페이지

    • resources/templates/error/4xx.html
    • resources/templates/error/404.html
    • resources/templates/error/500.html

    테스트

    • http://localhost:8080/error-404 → 404.html
    • http://localhost:8080/error-400 → 4xx.html (400 오류 페이지가 없지만 4xx가 있음)
    • http://localhost:8080/error-500 → 500.html
    • http://localhost:8080/error-ex → 500.html (예외는 500으로 처리)

     

    BasicErrorController가 제공하는 기본 정보들

    • BasicErrorConroller는 다음 정보를 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`)

     

    오류 정보 추가 - resources/templates/error/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>500 오류 화면 스프링 부트 제공</h2>
        </div>
        <div>
            <p>오류 화면 입니다.</p>
        </div>
        <ul>
            <li>오류 정보</li>
            <ul>
                <li th:text="|timestamp: ${timestamp}|"></li>
                <li th:text="|path: ${path}|"></li>
                <li th:text="|status: ${status}|"></li>
                <li th:text="|message: ${message}|"></li>
                <li th:text="|error: ${error}|"></li>
                <li th:text="|exception: ${exception}|"></li>
                <li th:text="|errors: ${errors}|"></li>
                <li th:text="|trace: ${trace}|"></li>
            </ul>
            </li>
        </ul>
        <hr class="my-4">
    </div> <!-- /container -->
    </body>
    </html>

    실행

     

    오류 정보는 숨기는 것이 좋다!

    • 하지만 오류 관련 내부 정보들을 고객에게 노출하는 것은 좋지 않습니다.
    • 고객이 해당 정보를 읽는다면 혼란만 더 해지고 보안상의 문제도 발생할 수 있습니다.
    • 다음 설정을 통해 오류 정보를 model에 포함할지 여부를 설정할 수 있습니다.

    application.properties

    server.error.include-exception=true		//exception 포함 여부 (true, false)
    server.error.include-message=never		//message 포함 여부 (never, always, on_param)
    server.error.include-stacktrace=never		//trace 포함 여부 (never, always, on_param)
    server.error.include-binding-errors=never	//errors 포함 여부 (never, always, on_param)

     

    기본 값이 never인 부분은 다음 3가지 옵션을 사용할 수 있습니다.

    • never : 사용하지 않음
    • always : 항상 사용
    • on_param : 파라미터가 있을 때 사용

    on_param

    • on_param은 해당 파라미터가 있으면(ex)message=) 해당 정보를 노출합니다. 
    • 디버그 시 문제를 확인하기 위해 사용할 수 있습니다.
    • 하지만 개발 서버에서는 권장하지만, 운영 서버에서는 권장하지 않습니다.
    • 다음과 같이 오류 관련 정보가 쉽게 노출되기 때문입니다.

    테스트

    application.properties

    server.error.include-exception=true
    server.error.include-message=on_param
    server.error.include-stacktrace=on_param
    server.error.include-binding-errors=on_param

     

    http://localhost:8080/error-ex?message=&errors=&trace=

    주의

    • 실무에서는 이런 정보들을 노출하면 안 됩니다.
    • 사용자에게는 예쁘게 출력된 오류 화면과 고객이 이해할 수 있는 간단한 오류 메시지를 보여주고, 오류는 서버에 로그로 남겨서 확인해야 합니다. 
    참고 스프링 부트 관련 옵션
    server.error.whitelabel.enabled=true 	// 오류 처리 화면을 못찾을 시, 스프링 whitelabel 오류 페이지 적용
    server.error.path=/error 		// 오류 페이지의 경로. 스프링이 자동으로 등록하는 서블릿 글로벌 오류 페이지 경로와 BasicErrorController 오류 컨트롤러 경로에 함께 사용된다.​
    참고 에러 공통 처리 컨트롤러 기능 확장
    해당 기능을 변경하고 싶다면, ErrorController 인터페이스를 상속받아 구현하거나, BasicErrorController를 상속받아 기능을 추가하면 됩니다. 

     

     

    정리

    스프링 부트가 기본으로 제공하는 오류 페이지를 활용하면, 오류 페이지와 관련된 대부분의 문제는 손쉽게 해결할 수 있습니다.

     


    출처

    https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard

     

    스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

    웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

    www.inflearn.com