개요
실무 개발을 경험해보면 실제 개발하는 시간보다 예외처리 및 테스트 코드 작성하는데 시간이 더 많이 걸립니다.
따라서, 이번에는 예외처리와 예외 페이지에 대해 정리해보겠습니다.
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편 (김영한 강사님)
반응형
'Spring' 카테고리의 다른 글
[SpringBoot] 스프링 TypeConverter 정리 (1) | 2021.08.11 |
---|---|
[SpringBoot] API 예외처리 (1) | 2021.08.08 |
[SpringBoot] Filter, Interceptor 개념 정리 및 로그인 처리 (4) | 2021.08.01 |
[SpringBoot] 쿠키, 세션을 이용한 로그인 처리 (2) | 2021.07.31 |
[SpringBoot] 쿠키, 세션 개념 정리 (0) | 2021.07.29 |