스프링 MVC 2편 - 백엔드 웹 개발 활용 기술을 공부하고 정리하는 포스트입니다.


서블릿 예외 처리

스프링이 아닌 순수 서블릿 컨테이너의 예외 처리

  • 2가지 방식을 지원
    • Exception(예외)
    • response.sendError(HTTP status code, error message)

Exception 예외

  • 자바 직접 실행
    • 메인 메서드를 직접 실행하는 경우 main 쓰레드가 실행
    • 실행 도중에 예외를 잡지 못하고 처음 실행한 main() 메서드를 넘어서 예외가 던져지면 → 예외정보를 남기고 쓰레드 종료
  • 웹 애플리케이션
    • 사용자 요청별로 별도의 쓰레드가 할당, 서블릿 컨테이너 안에서 실행
    • try ~ catch로 예외를 잡아서 처리하면 → No problem!
    • 서블릿 밖까지 예외가 전달되면? → 톰캣 같은 WAS 까지 예외가 전달
WAS ← 필터 ← 서블릿 ← 스프링 인터셉터 ← 컨트롤러(예외 발생)

오류 처리 페이지 흐름

WAS(예외 처리 경로) → 필터 → 서블릿 → 인터셉터 → 컨트롤러(예외 처리 경로)
  • 컨트롤러에서 발생한 예외가 WAS까지 전파 → 서버 내부에서 처리할 수 없는 오류가 발생한 것으로 판단 → HTTP 상태 코드 500 반환
  • WAS에서 오류 처리 페이지 경로로 다시 수행 → 페이지 경로를 컨트롤러에서 호출하여 오류 페이지 출력
@Slf4j
@Controller
public class ServletExController {

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

}
  • 리소스가 없는 아무 페이지나 호출하면, 톰캣이 기본적으로 제공하는 404 - Not Found가 출력

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

  • HttpServletResponse가 제공하는 sendError라는 메서드를 사용해도 됨
    → 바로 예외가 발생하는 것은 X, 서블릿 컨테이너에게 오류가 발생했다는 점을 전달할 수 있음
  • HTTP 상태 코드와 오류 메시지 추가 가능

  • response.sendError(HTTP 상태 코드)
  • response.sendError(HTTP 상태 코드, 오류 메시지)
WAS(response.sendError 호출 기록 확인) ← 필터 ← 서블릿 ← 스프링 인터셉터 ← 컨트롤러(response.sndError())

오류 처리 페이지 흐름

WAS(예외 처리 경로) → 필터 → 서블릿 → 인터셉터 → 컨트롤러(예외 처리 경로)
  • 컨트롤러에서 예외 발생 → response.sendError()로 예외 저장
  • 저장된 에러를 WAS에서 확인 → 설정된 오류 코드에 맞춰 예외 처리
  • sendError(Http 상태 코드, 오류 메시지)에서 Http 상태 코드로 예외 설정
@Slf4j
@Controller
public class 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, "500 오류!");
  }

}
  • 설정한 예외 코드에 맞춰 기본 오류 페이지 출력

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

  • 서블릿 컨테이너가 제공하는 기본 예외 처리화면은 불친절함 → 서블릿이 제공하는 오류 처리 기능 사용

  • Exception이 발생해서 서블릿 밖으로 전달되거나 response.sendError()가 호출되었을 때
    → 각각 상황에 맞춘 오류 처리 기능을 서블릿이 제공

서블릿 오류 페이지 등록

@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);
  }
}
  • 오류 페이지는 예외를 다룰 때 해당 예외와 그 자식 타입의 오류를 함께 처리
    → 예를 들어 RuntimeExceptionRuntimeException의 자식도 함께 처리

  • 오류 발생 처리 컨트롤러

@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";
  }
}
  • 오류 처리할 error-page 디렉터리 내부에 404.html, 500.html 생성

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

  • Exception이 발생해서 서블릿 밖으로 전달되거나 response.sendError()가 호출되었을 때 설정된 오류페이지 탐색

  • 예외 발생 흐름

WAS(여기까지 전파) ← 필터 ← 서블릿 ← 인터셉터 ← 컨트롤러(예외 발생)
  • sendError 흐름
WAS(sendError 호출 기록 확인) ← 필터 ← 서블릿 ← 인터셉터 ← 컨트롤러(`response.sendError()`)
  • WAS는 해당 예외를 처리하는 오류 페이지 정보를 확인
    • RuntimeException 예외가 WAS에 전달 → 오류 페이지 정보 확인 → /error-page/500으로 지정되어 있음 → WAS는 /error-page/500 요청
  • 오류 페이지 요청 흐름
WAS `/error-page/500` 다시 요청 → 필터 → 서블릿 → 인터셉터 → 컨트롤러(/error-page/500) → View
  • 예외 발생과 오류 페이지 요청 흐름
1. WAS(여기까지 전파) ← 필터 ← 서블릿 ← 인터셉터 ← 컨트롤러(예외발생)
2. WAS `/error-page/500` 다시 요청 → 필터 → 서블릿 → 인터셉터 → 컨트롤러(`/errorpage/500`) -> View

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

📌 참고

  • 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. WAS(여기까지 전파) ← 필터 ← 서블릿 ← 인터셉터 ← 컨트롤러(예외발생)
2. WAS `/error-page/500` 다시 요청 → 필터 → 서블릿 → 인터셉터 → 컨트롤러(`/errorpage/500`) -> View
  • 오류 발생 → 서버 내부에서 URI를 재요청
    • 이 때 필터, 서블릿, 인터셉터도 호출 → 매우 비효율적
    • 따라서, 정상요청과 내부요청을 구분해야함 → 이를 해결하기 위해 서블릿은 DispatcherType이라는 정보를 제공

DispatcherType

  • 이런 경우를 위해서 필터는 dispatcherTypes라는 옵션을 제공

  • 다양한 DispatcherType이 존재

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

filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR)

  • 두 가지를 모두 넣으면 클라이언트 요청과 오류 페이지 요청 모두에서 필터가 호출됨
    • 아무것도 넣지 않으면 기본 값은 DispatcherType.REQUEST, 클라이언트 요청만 필터가 적용

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

  • 필터는 DispatcherType에 따라 필터를 적용할지 선택 가능
  • 인터셉터는 서블릿이 아닌 스프링이 제공 → 즉, DispatcherType과 무관하게 항상 호출
    • 대신 요청 경로에 따라 추가하거나 제외하기 쉬움 → 오류페이지 경로를 excludePathPatterns를 사용해서 변경 가능
@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/**"); //오류 페이지 경로
  }

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

}

스프링 부트 오류 페이지 처리

  • 스프링 부트를 통해 서블릿 컨테이너 실행 → 스프링 부트에서 제공하는 기능을 사용해서 서블릿 오류 페이지 등록
    • ErrorPage 자동 등록 → /error 경로로 기본 오류페이지 설정
    • BasicController라는 스프링 컨트롤러 자동 등록 → ErrorPage에서 등록한 /error를 매핑 후 처리함
  • BasicErrorController의 처리 순서 (우선 순위)
    • 뷰 템플릿
      • resource/templates/error/500.html
      • resource/templates/error/5xx.html
    • 정적 리소스 (static, public)
      • resource/static/error/400.html
      • resource/static/error/404.html
      • resource/static/error/4xx.html
    • 적용 대상이 없을 시 뷰 이름 (error)
      • resource/templates/error.html
  • 해당 경로 위치에 HTTP 상태 코드 이름의 뷰 파일을 넣으면 끝
<!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>

📌 참고

  • BasicController는 오류 정보를 model에 담아 뷰에 전달함