Spring

[SpringBoot] HTML 예외처리와 예외 페이지

꾸준함. 2021. 8. 3. 21:27

개요

실무 개발을 경험해보면 실제 개발하는 시간보다 예외처리 및 테스트 코드 작성하는데 시간이 더 많이 걸립니다.

따라서, 이번에는 예외처리와 예외 페이지에 대해 정리해보겠습니다.

 

1. 서블릿의 예외처리 과정

서블릿은 크게 두 가지 방식으로 예외처리 지원함

  • Exception을 던지거나
  • response.sendError(HTTP 상태 코드, 에러 메시지) 메서드 호출

 

1.1  Exception

  • 자바의 메인 메서드를 직접 실행할 경우 main이라는 쓰레드가 실행되며 실행 도중 예외를 잡지 못하고 실행한 main() 메서드를 넘어서 예외가 던져지면, 예외 정보를 남기고 해당 쓰레드 종료
  • 웹 애플리케이션의 경우 사용자 요청 별로 별도의 쓰레드가 할당되고, 서블릿 컨테이너 안에서 실행
    • 애플리케이션에서 exception이 발생했을 때, try catch문으로 예외를 처리하면 문제가 되지 않지만 예외를 잡지 못하고 예외가 전달되면 톰캣 같은 WAS까지 예외가 전달됨
  • Filter, Interceptor 관련 게시글(https://jaimemin.tistory.com/1887)을 참고하면 HTTP 요청이 들어올 경우 WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 순으로 호출되는 것을 알 수 있음
  • 따라서, 컨트롤러에서 예외가 발생할 경우 역순으로 컨트롤러 -> 인터셉터 -> 서블릿 -> 필터 -> WAS 순으로 예외가 전달
  • 예를 들자면, 정의되지 않은 URL로 갈 경우 404 에러가 발생하고 이를 별도로 처리하지 않을 경우 스프링 부트 혹은 톰캣에서 제공한 404 페이지가 호출되는 것을 확인할 수 있음

 

1.2 response.sendError 메서드

  • 에러가 발생했을 때 HttpServletResponse에서 제공하는 sendError 메서드 사용하는 방법도 있음
  • 호출한다고 해서 당장 exception이 발생하는 것은 아니지만 서블릿 컨테이너에 오류가 있다는 내용을 전달 가능
  • 메서드 파라미터로 HTTP 상태 코드와 에러 메시지도 전달 가능
  • 앞선 Exception과 마찬가지로 컨트롤러에서 sendError 메서드를 호출할 경우 컨트롤러 -> 인터셉터 -> 서블릿 -> 필터 -> WAS 순으로 호출
  • 메서드 호출 후 response는 내부에 오류 상태를 저장하고 서블릿 컨테이너는 고객에게 응답하기 전 response 내용을 확인 후 오류 코드에 적절한 기본 오류 페이지를 제공

 

2. 커스텀 에러 페이지

  • 톰캣이나 스프링 부트에서 디폴트 에러 페이지를 제공하기는 하지만 고객 친화적이지 않음
  • 또한, 톰캣 에러 페이지 같은 경우 어떤 파일에서 에러가 발생했는지를 다 보여주기 때문에 해커들의 타깃이 될 수 있음
  • 따라서, 개발자가 에러에 적합한 커스텀 에러 페이지를 직접 제작해서 보여줄 수 있도록 처리하는 것이 중요
  • 기존에는 서블릿 예외 발생 시 각 에러 코드마다 커스텀 에외 페이지를 매핑하는 내용을 web.xml에 등록했었음
  • 하지만, 요즘에는 xml을 지향하고 있으므로 WebServerFactoryCustomizer를 사용하여 에러 페이지를 등록하는 예시 코드를 작성해보겠습니다.
  • 에러 페이지를 등록했다면 에러 페이지를 관리하는 에러 페이지 전용 컨트롤러도 필요

 

ExampleController.java


 

WebServerCustomizer.java


 

ErrorPageController.java


 

* WebServerCustomizer에서 각 에러 코드에 대응하는 예외 페이지를 등록

* 에러 페이지는 exception을 다룰 때 해당 예외와 그 자식 타입의 오류까지 함께 처리 (예를 들자면, RuntimeException을 등록했을 경우 RuntimeException의 자식도 함께 처리)

 

3. 에러 페이지 작동 원리

  • 앞서 언급한대로 서블릿은 예외가 서블릿 밖으로 전달되거나 response.sendError() 메서드가 호출되었을 때 설정된 오류 페이지를 찾음
  • 컨트롤러에서 예외가 발생할 경우 두 케이스 모두 컨트롤러 -> 인터셉터 -> 서블릿 -> 필터 -> WAS 순으로 예외가 전달되며 WAS는 에러 페이지 정보를 WebServerCustomizer에서 확인한 뒤 다시 해당 에러 페이지를 출력하기 위해 재요청
  • 전체적인 흐름을 정리하자면 아래와 같음
    • HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러 - 예외 발생 -> 인터셉터 -> 서블릿 -> 필터 -> WAS - 예외 페이지 정보 확인 -> 필터 -> 서블릿 -> 인터셉터 -> 예외 페이지 컨트롤러 -> 예외 페이지 (View)
  • 위와 같은 흐름이 발생할 때, 웹 브라우저 즉, 클라이언트는 서버 내부에서 예외 페이지를 요청하기 위해 프로세스를 한번 더 타는 것을 모름
    • 오직 서버 내부에서만 에러 페이지를 찾기 위해 추가적인 호출을 함
  • 정리를 하자면, 예외가 발생할 경우 WAS까지 전파가 되고 WAS는 에러 페이지 경로를 찾아서 내부에서 에러 페이지를 호출하기 위해 필터, 서블릿, 인터셉터, 컨트롤러를 모두 재호출 (재호출 한다는 점은 클라이언트 측에서 알 수 없음)
  • 또한, WAS는 에러 페이지를 호출할 때 오류 정보를 request 내 attribute에 추가해서 넘겨주기 때문에 아래와 같이 오류 로그를 남기는 것이 가능

 

 

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

 

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

  • 앞서 설명한 에러 페이지 작동 흐름을 보면 서블릿이 필터를 총 두 번 호출하는 것을 확인할 수 있음
    • HTTP 요청을 받았을 때
    • exception이 발생해서 에러 페이지를 재호출할 때
  • 로그를 남기는 필터의 경우 두 번 다 호출되도 무관하겠지만 로그인 처리 필터 같은 경우 이미 HTTP 요청을 받았을 때 로그인 여부를 확인했으므로 두 번 호출되는 것은 비효율적
  • 결국 필터가 호출될 때 정상적인 요청에 의한 호출인지 에러 페이지 호출에 따른 호출인지 판별할 수 있어야 함
    • 서블릿은 이런 문제를 해결하기 위해서는 DispatcherType이라는 추가 정보 제공

 

DispatcherType

  • DispatcherType은 enum 타입이고 FORWARD, INCLUDE, REQUEST, ASYNC, ERROR 타입이 존재
  • 우리가 눈여겨봐야 할 타입은 REQUEST와 ERROR 타입
    • REQUEST: 정상적인 요청
    • ERROR: 에러 페이지 호출
  • 나머지 타입
    • FORWARD: 서블릿에서 다른 서블릿을 호출
    • INCLUDE: 서블릿에서 다른 서블릿의 결과를 포함
    • ASYNC: 서블릿 비동기 호출
  • 정리를 하자면, 필터는 DispatcherType을 보고 어떤 호출인지 판별하고 REQUEST일 경우 HTTP 요청이 들어온 것으로 판단하고 ERROR일 때는 에러페이지 호출로 판단
  • Filter, Interceptor 관련 게시글(https://jaimemin.tistory.com/1887)을 참고하면 알 수 있듯이 필터는 WebConfig에 등록하며 등록할 때 어떤 DispatcherType일 때만 호출할지를 정의해줄 수 있음

 

WebConfig.java


 

* 로그 필터 같은 경우 정상 요청과 에러 페이지 호출로 인한 재요청 모두 등록해도 무방

* 로그인 필터 같은 경우 DispatcherType.REQUEST일 때만 등록해주는 것을 추천

 

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

  • 인터셉터 또한 필터처럼 에러 페이지 작동 흐름 내 서블릿이 인터셉터를 총 두 번 호출하는 것을 확인할 수 있음
  • 필터는 서블릿에서 제공하는 기능이므로 필터 등록 시 어떤 DispatcherType인 경우에만 적용하도록 등록할 수 있음
  • 하지만 인터셉터는 스프링에서 제공하는 기능이므로 모든 DispatcherType에 대해 호출됨
    • 이는 excludePathPatterns를 통해 해결 가능
    • excludePathPatterns에 오류 페이지 경로 등록 시 오류 페이지 호출 시 호출되지 않음

 

WebConfig.java


 

6. 스프링 부트에서 기본으로 제공하는 에러 페이지

  • 별도 커스텀 에러 페이지를 등록하지 않을 경우, 스프링 부트는 ErrorPage를 자동으로 등록하며 경로는 /error로 기본 오류 페이지 등록
    • 서블릿 밖으로 예외가 발생하거나, response.sendError() 메서드가 호출될 경우 모든 에러는 /error를 호출
  • 스프링 부트는 BasicErrorController라는 스프링 컨트롤러를 자동으로 등록하며 해당 컨트롤러는 /error를 매핑해서 처리하는 컨트롤러
  • ErrorMvcAutoConfiguration 클래스가 에러 페이지를 자동으로 등록하는 역할
  • BasicErrorController는 기본적인 로직이 모두 개발되어 있기 때문에 개발자는 오류 페이지 화면만 등록해주면 됨
    • 에러 페이지가 정적 HTML이면 static 폴더 내 등록
    • 뷰 템플릿을 사용해서 동적으로 에러 페이지를 만들고 싶으면 뷰 템플릿 경로에 오류 페이지 파일을 생성해서 넣어주면 됨
    • resources/templates/error/500.html 처럼 에러코드를 구체적으로 등록할 수 있지만 모든 500대 에러에 대해 해당 에러 페이지를 호출하고 싶을 경우 resources/templates/error/5xx.html과 같이 등록해주면 됨

 

출처

인프런 스프링 MVC 2편 (김영한 강사님)

반응형