[Servlet] 예외 처리, 오류 페이지

hi·2022년 12월 16일
0

스프링이 아닌 순수 서블릿 컨테이너의 예외 처리 방식
(예외가 있다고 알려주는 두 가지 방식)

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

Exception 예외

자바 직접 실행

  • 메인 메서드를 직접 실행하면 main 이라는 이름의 쓰레드가 실행
  • main() 메서드를 넘어 예외가 터지면 해당 쓰레드는 종료

웹 애플리케이션

  • 사용자 요청별로 별도의 쓰레드가 할당, 서블릿 컨테이너 안에서 실행

  • 만약 애플리케이션에서 예외를 잡지 못해 서블릿 밖으로 예외가 전달된다면?
    WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
    톰캣 같은 WAS 까지 예외가 전달

  • 서버 내부에서 처리할 수 없는 오류가 발생한 것으로 생각하여 오류 화면을 띄움

톰캣 기본 오류 화면


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

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

  • HttpServletResponse 가 제공하는 sendError 메서드
  • 호출 즉시 예외가 발생하는 것은 아니지만, 서블릿 컨테이너에게 오류가 발생했다는 점을 전달
  • HTTP 상태 코드와 오류 메시지 추가 가능

sendError 흐름

WAS(sendError 호출 기록 확인) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러
(response.sendError())
  • response.sendError() 호출 시 response 내부에 오류가 발생했다는 상태를 저장
  • 서블릿 컨테이너는 응답 전에 response에 sendError() 가 호출되었는지 확인
  • 호출되었다면 설정한 오류 코드에 맞추어 기본 오류 페이지를 보여줌

서블릿 - 오류 화면

서블릿이 제공하는 기능을 사용하여 오류 페이지를 변경할 수 있다

  • 과거에는 web.xml 파일에 직접 오류 화면을 등록하여 사용
  • 현재는 스프링 부트를 통하여 서블릿 컨테이너를 실행하기 때문에,
    스프링 부트가 제공하는 기능을 사용하여 서블릿 오류 페이지 등록 가능
@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"); //자식 타입 예외도 errorPageEx 호출

        factory.addErrorPages(errorPage404, errorPage500, errorPageEx);
    }
}

response.sendError(404) : errorPage404 호출
response.sendError(500) : errorPage500 호출
RuntimeException 또는 그 자식 타입의 예외: errorPageEx 호출

처리할 컨트롤러도(오류 화면을 보여줄) 필요

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

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

1. WAS(여기까지 전파) <- 필터 <- 서블릿 <- 인터셉터 <- 컨트롤러(예외발생)
2. WAS `/error-page/500` 다시 요청 -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러(/error-page/500) -> View
  1. 예외가 발생하여 WAS까지 전파
  2. WAS는 오류 페이지 경로를 찾아 호출
    이때 오류 페이지 경로로 필터, 서블릿, 인터셉터, 컨트롤러가 모두 다시 호출

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


🔎오류 발생 -> 오류 페이지를 출력하기 위해 필터, 서블릿, 인터셉터가 다시 호출 되는데 이는 비효율적

클라이언트로부터 발생한 정상 요청인지, 오류 페이지를 출력하기 위한 내부 요청인지 구분하기 위해 서블릿은 DispatcherType 이라는 추가 정보를 제공한다


서블릿 예외 처리 - 필터

DispatcherType

log.info("dispatchType={}", request.getDispatcherType())

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

로그를 출력하는 부분에 추가

request.getDispatcherType()

필터 등록

filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
//두 가지의 경우에 필터 호출

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

  • 인터셉터는 서블릿이 아닌, 스프링이 제공
  • 따라서 DispatcherType 과 무관하게 항상 호출

excludePathPatterns

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

스프링 부트 - 오류 페이지

스프링은 앞의 과정들을 모두 기본으로 제공한다
따라서 오류 페이지만 등록하면 된다

  • ErrorPage 자동으로 등록하고 /error 라는 경로로 기본 오류 페이지 설정

    • new ErrorPage("/error"), 상태코드와 예외를 설정하지 않으면 기본 오류 페이지로 사용
    • 서블릿 밖으로 예외가 발생하거나, response.sendError(...) 가 호출되면 모든 오류는 /error 호출
  • BasicErrorController 라는 스프링 컨트롤러를 자동으로 등록

    • ErrorPage 에서 등록한 /error 를 매핑해서 처리하는 컨트롤러

🔎 ErrorMvcAutoConfiguration 이라는 클래스가 오류 페이지를 자동으로 등록하는 역할을 함


뷰 선택 우선순위

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

but, 내부 정보들을 노출하는 것은 좋지 않음
다음 오류 정보들을 model 에 포함할지 여부를 선택 가능

application.properties

server.error.include-exception=false : exception 포함 여부( true , false )
server.error.include-message=never : message 포함 여부
server.error.include-stacktrace=never : trace 포함 여부
server.error.include-binding-errors=never : errors 포함 여부
  • 기본 값이 never 인 부분은 3가지 옵션을 사용 가능
    never : 사용하지 않음
    always : 항상 사용
    on_param : 파라미터가 있을 때 사용

  • on_param은 파라미터가 있으면 해당 정보를 노출. 디버그 시 문제를 확인하기 위해 사용할 수 있지만 운영 서버에서는 권장하지 않음

  • on_param 으로 설정 후, 다음과 같이 HTTP 요청시 파라미터를 전달하면 해당 정보들이 model 에 담겨서 뷰 템플릿에서 출력된다
    message=&errors=&trace=

스프링 부트 오류 관련 옵션

server.error.whitelabel.enabled=true : 스프링 부트 제공 기본 오류 페이지
server.error.path=/error : 오류 페이지 경로, 스프링이 자동 등록하는 서블릿 글로벌 오류 페이지 경로와 BasicErrorController 오류 컨트롤러 경로에 함께 사용됨

확장 포인트

에러 공통 처리 컨트롤러의 기능을 변경하고 싶으면

  • ErrorController 인터페이스를 상속 받아 구현하거나
  • BasicErrorController 상속 받아 기능을 추가

0개의 댓글