스프링 MVC 2 정리 - 8. 예외 처리와 오류 페이지 (22.8.12)

피아노과 개발자도전?·2022년 8월 12일
0

Today I learned

목록 보기
31/75
post-custom-banner

김영한 개발자님의 강의를 수강한 후 정리한 내용이다.

8. 예외 처리와 오류 페이지

자바는 메인 메소드 실행시 main의 쓰레드가 실행된다. 예외를 잡지 못하고 넘어서 예외가 던져지면, 예외 정보를 남기고 해당 쓰레드는 종료된다.

웹 어플리케이션

  • 사용자 요청별로 별도의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행된다.
  • 애플리케이션에서 예외가 발생했는데, 잡지 못하고 서블릿 밖까지 예외가 전달 되면 HTTP status 404같은 예외 페이지가 나타난다.

8.1. 서블릿 예외 처리

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

  • 서블릿 컨테이너에게 오류가 발생했다는 점을 전달할 수 있다.
  • Excpetion이 터지면 서블릿 컨테이너는 500으로 처리한다.
  • 직접 오류메시지 등을 담아서 처리하고 싶은 경우 사용한다.
@Slf4j
@Controller
public class ServletExController {
    @GetMapping("/error-ex")
    public void errorEx(){
        throw new RuntimeException("Exception occured");
    }

    @GetMapping("/error-404")
    public void error404(HttpServletResponse response) throws IOException{
        response.sendError(404, "404 Error");
    }

    @GetMapping("/error-500")
    public void error500(HttpServletResponse response) throws IOException {
        response.sendError(500);
    }
}

하지만 서블릿 컨테이너가 제공하는 기본 예외 처리 화면은 사용자가 보기에 불편하다.


서블릿이 제공하는 오류화면 기능

먼저 스프링 부트가 제공하는 기능을 사용해 서블릿 오류 페이지를 등록한다.

@Component // 스프링에 등록해주는 어노테이션 
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
    // 서블릿 컨테이너가 이렇게 사용하도록 지정함
    @Override
    public void customize(ConfigurableWebServerFactory factory) {
        ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
        // 낫파운드 에러가 뜨면 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);
        // 등록을 함
    }
}

해당 오류를 처리할 컨트롤러가 필요하다.

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

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

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

예를 들어서 RuntimeException 예외가 WAS까지 전달되면, WAS는 오류 페이지 정보를 확인한다.

확인해보니 RuntimeException 의 오류 페이지로 /error-page/500 이 지정되어 있다. WAS는 오류 페이지를 출력하기 위해 /error-page/500 를 다시 요청한다.


오류 정보 추가

WAS는 오류 페이지를 단순히 다시 요청만 하는 것이 아니라, 오류 정보를 requestattribute 에 추가해서 넘겨준다.

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));
    //클라이언트 요청 URI - ex의 경우 NestedServletException 스프링이 한번 감싸서 반환
    log.info("ERROR_REQUEST_URI: {}", request.getAttribute(ERROR_REQUEST_URI));
    //오류가 발생한 서블릿 이름
    log.info("ERROR_SERVLET_NAME: {}", request.getAttribute(ERROR_SERVLET_NAME));
    // HTTP 상태 코드
    log.info("ERROR_STATUS_CODE: {}", request.getAttribute(ERROR_STATUS_CODE));
    //디스패처 타입 - 클라이언트로 부터 발생한 정상 요청인지, 아니면 오류 페이지를 출력하기 위한 내부 요청인지 구분
    log.info("dispatchType={}", request.getDispatcherType());
    }

서블릿 예외 처리 - 필터

두번씩 호출되는게 비효율적이기 때문에 클라이언트로부터 발생된 정상 요청인지 오류 페이지 출력을 위한 내부 요청인지 구분해야하고, 이를 위해 DispatcherType을 사용한다.

  • 고객이 한 요청 : dispatcherType=REQUEST
  • 오류 요청인 경우 : dispatchType=ERROR
  • MVC에서 배웠던 서블릿에서 다른 서블릿이나 JSP를 호출할 때 : dispatchType=FORWARD
  • 서블릿에서 다른 서블릿이나 JSP의 결과를 포함할 때 : dispatchType= INCLUDE
  • 서블릿 비동기 호출 : dispatchType=ASYNC

WebConfig에 다음을 추가한다

filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);{
	return filterRegistrationBean;
}

이렇게 두 가지를 모두 넣으면 클라이언트 요청은 물론이고, 오류 페이지 요청에서도 필터가 호출된다.

아무것도 넣지 않으면 기본 값이 DispatcherType.REQUEST 이다.


예외처리 - 인터셉터

WebConfig

	@Override
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**", "/*.ico", "/error", "/error-page/**");
    }

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

대신에 인터셉터는 다음과 같이 요청 경로에 따라서 추가하거나 제외하기 쉽게 되어 있기 때문에, 이러한 설정을 사용해서 오류 페이지 경로를 excludePathPatterns 를 사용해서 빼주면 된다.


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

개발자는 오류 페이지 화면만 BasicErrorController 가 제공하는 룰과 우선순위에 따라서 등록하면 된다.

정적 HTML이면 정적 리소스, 뷰 템플릿을 사용해서 동적으로 오류 화면을 만들고 싶으면 뷰 템플릿 경로에 오류 페이지 파일을 만들어서 넣어두기만 하면 된다.

뷰 선택 우선순위

BasicErrorController 의 처리 순서
1. 뷰 템플릿

  • resources/templates/error/500.html
  • resources/templates/error/5xx.html
  1. 정적 리소스(static, public)
  • resources/static/error/400.html
  • resources/static/error/404.html
  • resources/static/error/4xx.html
  1. 적용 대상이 없을 때 뷰 이름(error)
  • resources/templates/error.html

BasicErrorController가 제공하는 기본 정보들

<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>
  • 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)

오류 관련 내부 정보들을 고객에게 노출하는 것은 좋지 않다. 고객이 해당 정보를 읽어도 혼란만 더해지고, 보안상 문제가 될 수도 있다.

//application.properties 각각의 포함여부
server.error.include-exception=true //예) exception 포함여부
server.error.include-message=on_param
server.error.include-stacktrace=on_param
server.error.include-binding-errors=on_param

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

  • never : 사용하지 않음
  • always :항상 사용
  • on_param : 파라미터가 있을 때 사용
profile
공부한 내용 정리
post-custom-banner

0개의 댓글