온라인 강의/Spring Framework

[강의] 스프링 MVC 2편 - 예외 처리와 오류 페이지(섹션8)

코드몬스터 2024. 6. 15. 18:56
728x90

 

1. 서블릿 예외 처리

두 가지 방식으로 예외 처리를 지원한다.

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

 

1) Exception(예외)

자바 직접 실행

  • 자바의 메인 메서드를 직접 실행하는 main 이라는 쓰레드가 실행된다.
  • 실행 도중에 예외를 잡지 못하고 처음 실행한 main 메서드로 넘어가면 예외를 던지고, 쓰레드는 종료된다.

 

웹 애플리케이션

  • 웹 애플리케이션은 사용자 요청별로 별도의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행된다.
  • 예외가 발생했을 때, try ~ catch로 처리하면 아무런 문제가 없다.
  • 만약, 예외를 잡지 못하고 서블릿 밖으로 전달되면 어떻게 될까?
WAS(여기까지 전파) ← 필터 ← 서블릿 ← 인터셉터 ← 컨트롤러(예외발생)

  

@Slf4j
public class ServletExController {

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

 

톰캣이 제공하는 오류화면을 볼 수 있다.

HTTP Status 500 – Internal Server Error

 

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

 

오류가 발생했을 때, HttpServletResponse가 제공하는 sendError 메서드를 사용해도 된다.

@Slf4j
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, "500 오류");
    }
}

 

sendError 흐름

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

 

 

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

서블릿 컨테이너가 제공하는 기본 예외 처리 화면은 고객 친화적이지 않다.

 

web.xml

<web-app>
    <error-page>
        <error-code>404</error-code>
        <location>/error-page/404.html</location>
    </error-page>
    
    <error-page>
        <error-code>500</error-code>
        <location>/error-page/500.html</location>
    </error-page>
    
    <error-page>
        <exception-type>java.lang.RuntimeException</exception-type>
        <location>/error-page/500.html</location>
    </error-page>
</web-app>

 

서블릿 오류페이지 등록

@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {

    @Override
    public void customize(ConfigurableWebServerFactory factory) {
        ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
        ErrorPage errorPag500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");
        ErrorPage errorPagEx = new ErrorPage(RuntimeException.class, "/error-page/500");

        factory.addErrorPages(errorPage404, errorPag500, errorPagEx);
    }
}

 

오류 컨트롤러

@Slf4j
@Controller
public class ErrorPageController {

    @RequestMapping("/error-page/404")
    public String errorPage404(HttpServletResponse response, HttpServletRequest request) {
        log.info("errorPage 404");

        return "error-page/404";
    }

    @RequestMapping("/error-page/500")
    public String errorPage500(HttpServletResponse response, HttpServletRequest request) {
        log.info("errorPage 500");

        return "error-page/500";
    }

}

 

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

1. 오류가 발생하면 WAS로 보내지고

2. WAS는 해당 예외를 처리하는 오류 페이지 정보를 확인하여 다시 요청한다.
   → 이때 오류 페이지 경로로 필터, 서블릿, 인터셉터, 컨트롤러가 모두 다시 호출된다.

 

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

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

 

@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(HttpServletResponse response, HttpServletRequest request) {
        log.info("errorPage 404");
        printErrorInfo(request);
        
        return "error-page/404";
    }

    @RequestMapping("/error-page/500")
    public String errorPage500(HttpServletResponse response, HttpServletRequest request) {
        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());
    }

}

 

4. 서블릿 예외 처리 - 필터

오류페이지를 호출한다고 해서 해당 필터나 인터셉터가 한 번 더 호출되는 것은 비효율적이다.

오류 페이지를 출력하기 위한 내부 요청인지 구분할 수 있도록, 서블릿은 DispatcherType  이라는 추가 정보를 제공한다.

 

DispatcherType

출력해보면 오류 페이지에서 dispatcherType=ERROR 로 나오는 것을 확인할 수 있다.

log.info("dispatchType={}", request.getDispatcherType());
  • REQUEST: 클라이언트 요청
  • ERROR: 오류 요청
  • FORWARD: 서블리셍서 다른 서블릿이나 JSP를 호출할 때
  • INCLUDE: 서블릿에서 다른 서블릿이나 JSP의 경과를 포함할 때
  • ASYNC: 서블릿 비동기 호출
public enum DispatcherType {
    FORWARD,
    INCLUDE,
    REQUEST,
    ASYNC,
    ERROR
}

 

 

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

}

5. 서블릿 예외 처리 - 인터셉터

인터셉터는 경로 정보로 중복 호출 제거(excludePathPatterns("/error-page/**")

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

 

6. 스프링 부트 - 오류 페이지