관리 메뉴

Rootable의 개발일기

예외 처리와 오류 페이지 본문

Spring

예외 처리와 오류 페이지

dev-rootable 2023. 7. 31. 15:58

📌 서블릿의 2가지 예외 처리 방식

 

서블릿 컨테이너에 예외를 전달하는 경우는 다음 2가지가 있다.

 

🔎 Exception(예외)

 

✔ 자바 직접 실행

 

자바의 메인 메서드를 직접 실행하는 경우 main이라는 이름의 쓰레드가 실행된다. 예외는 바로 try ~ catch로 처리하거나 실행 도중에 예외를 잡지 못하고 처음 실행한 main() 메서드를 넘어서 예외가 던져지면, 예외 정보를 남기고 해당 쓰레드는 종료된다.

 

✔ 웹 애플리케이션

 

애플리케이션에서 예외가 발생했는데, 애플리케이션에서 예외를 잡지 못하고 서블릿 밖으로 예외가 전달될 경우 다음과 같이 동작한다.

 

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

 

🔎 response.sendError

 

HttpServletResponse가 제공하는 sendError() 메서드는 서블릿 컨테이너에게 오류가 발생했다는 것을 전달한다.

 

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

 

💡 sendError 흐름

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

1. response.sendError()를 호출하면 response 내부에는 오류가 발생했다는 상태를 저장
2. 서블릿 컨테이너는 고객에게 응답하기 전에 response에 sendError()가 호출되었는지 확인
3. 호출되었다면 설정한 오류 코드에 맞추어 기본 오류 페이지를 보여준다.

 

📌 서블릿의 오류 화면 제공

 

서블릿에서 제공하는 기본 오류 페이지는 고객 친화적이지 않다.

 

서블릿이 제공하는 기본 오류 페이지

 

예외가 발생했을 때, 해당 예외와 컨트롤러를 매핑해 주는 클래스를 생성하여 최종적으로 오류 페이지를 리턴하는 컨트롤러가 실행되도록 한다.

 

🔎 예외 발생시키기

 

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

}

 

RuntimeException 및 sendError()를 통해 서블릿에 예외를 전달한다. RuntimeException의 경우, 500 코드(서버 내부 에러)로 인지한다.

 

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

}

 

서블릿은 위와 같이 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";
    }

}

 

위 코드는 404 및 500 에러가 들어왔을 때, 이를 처리하는 컨트롤러다.

 

최종적으로 템플릿에서 error-page라는 디렉터리 내에 있는 html 파일을 렌더링 한다.

 

500 에러에 대한 오류 페이지

 

📌 정리

 

🔎 Exception의 예외 발생 흐름

 

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

 

🔎 sendError의 예외 발생 흐름

 

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

 

위 예제에서는 WAS에서 오류 페이지를 찾기 위해 컨트롤러로 재 요청했다.

 

WAS(오류 페이지 요청) ➡ 필터 ➡ 서블릿 ➡ 인터셉터 ➡ 컨트롤러(뷰 요청) ➡ 뷰

 

📌 RequestDispatcher

 

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

 

RequestDispatcher에 정의된 상수를 통해 오류 정보를 조회할 수 있다. WAS는 여기에 정의된 정보를 request의 attribute에 담는다.

 

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

}

 

오류 정보 출력(/error-ex 호출)

 

📌 필터와 인터셉터의 예외와 일반 요청 구분

 

로그인 인증 체크의 경우, 오류 페이지에 대해서 수행할 필요가 없다. 이를 위해서 필터와 인터셉터는 해당 요청이 클라이언트로부터 발생한 정상 요청인지 오류 페이지를
출력하기 위한 내부 요청인지 구분할 필요가 있다.

 

🔎 필터 - DispatcherType

 

필터는 DispatcherType을 통해 구분한다.

 

✔ DispatcherType 종류

 

  • REQUEST : 클라이언트로부터 발생한 정상 요청
  • ERROR : 오류 페이지를 출력하기 위한 내부 요청
  • FORWARD : 서블릿에서 다른 서블릿이나 JSP를 호출
  • INCLUDE : 서블릿에서 다른 서블릿이나 JSP의 결과를 포함
  • ASYNC : 서블릿 비동기 호출

 

✔ 구현

 

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 {
            log.info("REQUEST [{}][{}][{}]", uuid, request.getDispatcherType(), requestURI);
            chain.doFilter(request, response);
        } catch (Exception e) {
            log.info("EXCEPTION {}", e.getMessage());
            throw e;
        } finally {
            log.info("REQUEST [{}][{}][{}]", uuid, request.getDispatcherType(), requestURI);
        }
    }

    @Override
    public void destroy() {
        log.info("log filter destroy");
    }
}

 

위 필터를 설정 클래스에 등록한다.

 

@Configuration
public class WebConfig {

    @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다. 이 메서드 안에 Dispatcher.ERROR가 담기면 오류 페이지 요청이 있을 때도 필터가 호출된다. 위 메서드가 생략되었을 때 기본 값은 Dispatcher.REQUEST다.

 

오류 페이지 요청에 대한 필터

 

🔎 인터셉터 - 항상 호출됨

 

인터셉터는 서블릿이 아닌 스프링에서 제공하는 기능이므로 DispatcherType과 무관하게 항상 호출된다.

 

인터셉터는 요청 경로에 따라서 추가하거나 제외하는 것이 자유롭다. excludePathPatterns를 통해 오류 페이지 요청에 대해서는 호출되지 않도록 할 수 있다.

 

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

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

 

 

위 코드는 모든 요청에 대해 로그를 찍는 인터셉터다.

 

해당 인터셉터를 아래처럼 등록할 수 있다.

 

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

}

 

 

 

스프링 부트에서 기본으로 매칭되는 에러 디렉터리인 "/error"와 앞에서 사용했던 "/error-page" 디렉터리를 excludePathPatterns에서 제외했다.

 

정상 요청

 

오류 정보

 

위 결과를 보면 필터 때와는 달리 오류 정보 출력 부분에 로그가 찍혀 있지 않다. 반면, 처음 "/error-ex"를 요청했던 클라이언트의 요청(REQUEST)은 로그가 찍혀 있다. 이처럼 excludePathPatterns를 통해 오류 페이지에 대해서는 인터셉터가 동작하지 않은 것을 알 수 있다.

 

📌 스프링 부트의 오류 페이지 등록

 

스프링 부트는 위 과정(예외 매핑, 처리할 컨트롤러 매핑 및 호출)을 모두 기본으로 제공한다.

 

서블릿 예외처리에서는 WebServerFactoryCustomizer를 구현하고 factory에 에러 페이지를 저장하고, 스프링 부트에서는 컨트롤러를 자동으로 등록해주는 BasicErrorController오류 페이지를 자동으로 등록해주는 ErrorMvcAutoConfiguration을 제공한다.

 

스프링 부트는 ErrorPage라는 객체를 자동으로 등록하고, "/error"라는 경로를 기본 오류 페이지로 설정한다. 그리고 BasicErrorController라는 스프링 부트가 자동으로 등록한 컨트롤러가 "/error" 디렉터리에서 파일 이름(HTTP 상태 코드)을 보고 해당 예외에 맞게 호출한다. 결론은 개발자는 오류 페이지를 생성하고 구성하는 일만 하면 된다.

 

스프링 부트에서 개발자는 BasicErrorController가 제공하는 룰과 우선순위에 따라 오류 페이지 화면을 등록하고
구성하는 일만 하면 된다.

 

WebServerFactoryCustomizer가 동작하면 BasicErrorController는 동작하지 않는다.

 

🔎 뷰 선택 우선순위 (숫자 작을수록 우선순위 높음)

 

1. 뷰 템플릿

 

더 자세한 것이 우선순위가 높다.

 

(1) resources/templates/error/500.html

(2) resources/templates/error/5xx.html (5xx는 500번대 의미)

 

2. 정적 리소스(static, public)

 

(1) resources/static/error/400.html

(2) resources/static/error/404.html

(3) resources/static/error/4xx.html

 

3. 적용 대상이 없을 때 뷰 이름(error)

 

(1) resources/templates/error.html

 

에러 공통 처리 컨트롤러의 기능을 변경하고 싶으면 ErrorController 인터페이스를 상속받아서 구현하거나 BasicErrorController를 상속 받아서
기능을 추가하면 된다.

 

Reference:

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

 

'Spring' 카테고리의 다른 글

스프링 파일 업로드/다운로드  (0) 2023.08.03
API 예외 처리  (0) 2023.08.01
필터와 인터셉터  (0) 2023.07.28
서블릿 HTTP 세션  (0) 2023.07.27
MessageCodesResolver  (0) 2023.07.25