본문 바로가기
Web/spring study

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

by 장인이 2024. 3. 25.

1. 개요

 이번 게시물에서는 예외 발생시 우선 순수 서블릿으로 예외를 처리해보고, 오류 화면을 제공해 볼 것이다. 그 후, 스프링부트는 어떻게 예외를 처리하고 오류 페이지를 작동시키는지에 대해 알아보자.

 

 

2. 서블릿 예외 처리

 서블릿에서 예외처리를 하는 경우는 크게 2가지이다. Exception(예외)가 발생하거나, response.sendError(HTTP 상태 코드, 오류 메시지)를 담을 경우이다.

 

1) Exception(예외)

 자바가 메인 메서드를 실행하는 경우를 생각해보자. 이 경우 main이라는 쓰레드가 실행되며, 메서드 실행 도중 main()메서드 내부에서 예외처리를 하지 못한다면 예외 정보를 남기고, main() 메서드가 종료된다.

 

 이에 반해, 웹 애플리케이션은 사용자의 요청별로 쓰레드가 생성되며, 서블릿 컨테이너에서 실행된다. 당연히, try catch문으로 예외를 잡았을 경우에는 아무런 문제가 되지 않는다. 하지만 만약, 컨트롤러에서 예외를 잡지 못하면 어떻게 될까? 흐름을 한번 살펴보자.

WAS(여기까지 전달됨)  <-  필터  <-  서블릿  <-  인터셉터  <-  컨트롤러 (예외 발생)

 

 결국 WAS까지 전달되며, 이를 한번 살펴보자.

 

 우선 서블릿 예외 처리를 살펴보기 위해 스프링 부트가 제공하는 기본 예외 페이지를 꺼두자.

application.properties

server.error.whitelabel.enabled=false

 

ServletExController

@Controller
public class ServletExController {

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

 

실행해보면, tomcat이 기본으로 제공하는 오류화면이 뜰것이다.

HTTP Status 500 - Internal Server Error

 

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

 오류 상황이 일어났을 때, 서블릿이 제공하는 sendError() 메서드를 사용해도 된다. 해당 메서드를 호출한다고 해서 예외가 당장 발생하지는 않지만, 서블릿 컨테이너에게 무언가 오류가 발생했다는 사실을 알릴 수 있다.

 해당 방식의 장점으로는 HTTP 상태 코드와 오류 메시지도 추가할 수 있다는 점이다.

 

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

 

sendError() 흐름

WAS (sendError() 호출 확인)  <-  필터  <-  서블릿  <-  인터셉터  <-  컨트롤러

 

 

 서블릿 컨테이너는 고객에게 응답 전, sendError() 호출 여부를 확인한다. 만일에 호출되었다면, 해당 오류 코드에 맞추어 서블릿 컨테이너가 제공하는 기본 오류 페이지를 보여준다.

 

 

3. 서블릿 오류 페이지

 이번에는 서블릿 컨테이너가 기본으로 제공해주는 오류 페이지가 아닌, 직접 오류페이지를 만들어 등록해보자.

 

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

 

 HttpStatus.NOT_FOUND는 404 예외, HttpStatus.INTERNAL_SERVER_ERROR는 500 예외를 나타낸다. 이렇게 등록을 해두면, 다음과 같은 흐름으로 진행이 된다.

 

response.sendError(404)  ->  errorPage404 호출  ->  "/error-page/404" 호출

 

 따라서 해당 오류 주소를 처리할 컨트롤러가 필요하다.

 

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

 

 그 후, /templates/error-page/ 위치에 404.html, 500.html을 작성하고 실행하면, 설정한 오류 페이지가 잘 노출되는 것을 확인할 수 있다.

 

 그러면 예외가 발생하고, 오류 페이지가 요청될때까지 흐름을 한번 살펴보자.

WAS  <-  필터  <-  서블릿  <-  인터셉터  <-  컨트롤러 (예외발생)
WAS "/error-page/500 다시 요청  ->  필터  ->  서블릿  ->  인터셉터  ->  컨트롤러  ->  view

 

 결국 WAS에서 서블릿을 거쳐 컨트롤러까지 갔다가, 예외가 발생하면 WAS까지 다시 돌아오고, 다시 오류페이지로 이동하는 컨트롤러가 호출되어 view가 실행된다.

 여기서 포인트는, 클라이언트(웹 브라우저)는 서버 내부에서 어떤 일이 벌어지는지 모른다는 점이다!

 

 그런데 한번 생각해보자. 오류가 발생하면, 오류 페이지를 출력하기 위해 WAS 내부에서 요청이 다시 발생한다. 이때, 필터와 인터셉터가 한번 더 호출된다! 이미 한번 체크가 완료되었으므로, 한번 더 호출되는 것은 매우 비효율적이다. 그러면 필터와 인터셉터가 어떻게 중복호출을 방지하고 있는지 살펴보자.

 

1) 오류 페이지 요청 시 서블릿 필터

 우선 서블릿은 이런 요청을 구분하기 위해 DispatcherType이라는 정보를 제공한다. 이를 통해 실제 클라이언트가 요청한 내용인지, 서버가 내부에서 요청한 내용인지를 구분할 수 있다! enum값들을 한번 살펴보자.

 

- REQUEST : 클라이언트 요청

- ERROR : 오류 요청

- FORWARD : 서블릿에서 다른 서블릿 혹은 JSP를 호출할 때

( RequestDispatcher.forward(request, response); )

- INCLUDE : 서블릿에서 다른 서블릿이나 JSP의 결과를 포함할 때

( RequestDispatcher.include(request, response); )

 

 이 값을 확인하여, 해당 요청이 고객에 의한건지, 오류인지를 구분할 수 있다!

 

LogFilter

request.getDispatcherType();

 

 만들어둔 필터에 해당 메서드를 호출하여, 타입을 확인할 수 있다.

 

WebConfig

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

 

 이렇게 두 가지 값을 모두 넣으면 클라이언트 요청 뿐만 아니라, 오류 페이지 요청에서도 필터가 호출된다. 근데, 아무것도 넣지 않는다면 기본 값은 DispatcherType.REQUEST이다. 따라서 특별한 경우가 아니라면, 기본 값을 그대로 사용하면 된다.

 

2) 오류 페이지 요청 시 인터셉터

 인터셉터는 서블릿이 아닌, 스프링이 제공하는 기능이다.. 따라서 DispatcherType과 무관하게 호출된다. 대신, 인터셉터는 특정 요청 경로를 제외하는 강력한 기능을 제공하기 때문에, 이를 활용하여 오류페이지 경로를 빼주자.

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

 

 

4. 스프링 부트 오류 페이지

 우리는 위에서 오류 페이지를 추가하기 위해 WebServerCustomizer를 만들고, 예외에 따른 ErrorPage를 등록한 후, 해당 경로를 처리하기 위한 ErrorPageController를 만들었다.

 이때, 스프링 부트는 이런 과정을 기본으로 제공해준다!! /error라는 경로로 기본 오류 페이지를 설정하고, BasicErrorController라는 스프링 컨트롤러를 자동 등록한다.

 

 자, 결국 개발자는 정해진 규칙에 의해 오류 페이지만 등록하면 된다! 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. 적용 대상이 없을 때

- resources/templates/error.html

 

 

5. 정리

 결국 개발자는 오류 페이지만 등록하면 되는건데, 굳이 왜 알아보았을까? 핵심은 기능을 변형, 혹은 확장해야 할 경우이다. 예외 공통 처리 컨트롤러의 기능을 변경하고 싶으면 ErrorController 인터페이스, 혹은 BasicErrorController를 상속받아서 기능을 추가하면 되는데, 앞의 내용을 모른다면 이런 유연한 변형이 불가능할 것이다.

 

 

 


위 내용은 김영한 님의 인프런 강의 "스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술"의 내용과 강의자료를 토대로 작성된 게시글입니다.

강의 링크:

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2

 

댓글