📌 순수 서블릿 컨테이너 예외처리
Exception
자바 직접 실행
자바 메인 메소드를 직접 실행하는 경우 main이라는 이름의 쓰레드가 실행된다.
실행 도중 예외를 잡지 못하고 main() 메소드를 넘어서 예외가 던져지면 예외 정보를 남기고 해당 쓰레드는 종료된다.
웹 애플리케이션
웹 애플리케이션은 쓰레드 하나가 돌아가는 것이 아니라 사용자 요청별로 별도의 쓰레드가 할당되고 서블릿 컨테이너 안에서 실행된다.
애플리케이션 예외가 발생했는데 어디선가 try-catch로 예외를 잡아 처리하면 아무런 문제가 없지만 예외를 잡지 못하고 서블릿 밖까지 예외가 전달 된다면
WAS → 필터 → 서블릿 → 스프링 인터셉터 → 컨트롤러(예외 발생) 이렇게 WAS가 필터를 호출,, 호출,, 호출,,,,, 되다가
컨트롤러에서 예외가 발생하면 반대로 컨트롤러 → 인터셉터 → 서블릿 → 필터 → WAS 여기까지 전파가 된다.
response.sendError
오류가 발생했을 때 HttpServletResponse가 제공하는 sendError
라는 메소드를 사용해도 된다.
이걸 호출한다고 당장 예외가 발생하는 것은 아니지만 서블릿 컨테이너에게 오류가 발생했다는 것을 전달할 수 있다.
response.sendError(Http 상태코드)
response.sendError(Http 상태코드, 오류메시지)
sendError 흐름
WAS(sendError 호출 기록 확인) < 필터 < 서블릿 < 인터셉터 < 컨트롤러(response.sendError()
)
response.sendError()
를 호출하면 response 내부에 오류가 발생했다는 상태를 저장해둔다.
그리고 서블릿 컨테이너는 고객에게 응답 전 response에 sendError()가 호출되었는지 확인한다. 이때, 호출 되었다면 설정한 오류 코드에 맞춰 기본 오류 페이지를 보여준다.
서블릿은 Exception이 발생해서 서블릿 밖으로 전달되거나 response.sendError()가 호출되었을 때 상황에 맞게 오류 처리 기능을 제공한다.
오류 페이지는 예외를 다룰 때 해당 예외뿐 아니라 그 자식의 타입 오류까지 함께 처리한다.
서블릿은 Exception이 발생해서 서블릿 밖으로 전달되거나 response.sendError()가 호출되었을 때 설정된 오류 페이지를 찾는다.
sendError 흐름
WAS(sendError 호출 기록 확인) < 필터 < 서블릿 < 인터셉터 < 컨트롤러(response.sendError()
)
이렇게 WAS는 해당 예외를 처리하는 오류 페이지 정보를 확인한다. 예를 들어 런타임 예외라면 우리는 예제에서 오류 페이지로 /error-page/500
를 설정해두었다. 그러므로 WAS는 오류 페이지를 출력하기 위해 /error-page/500
를 다시 요청한다.
(http요청이 다시 온 것 마냥 동작하지만 실제로 다시 온 것은 아니다.)
오류 페이지 요청 흐름
WAS/error-page/500
다시 요청 → 필터 → 서블릿 → 인터셉터 → 컨트롤러(/error-page/ 500
) → View
예외 발생과 오류 페이지 요청 흐름
1. WAS(여기까지 전파) < 필터 < 서블릿 < 인터셉터 < 컨트롤러(예외발생)
2. WAS/error-page/500
다시 요청 → 필터 → 서블릿 → 인터셉터 → 컨트롤러(/error- page/500
) → View
클라이언트(웹 브라우저)는 서버 내부에 이러한 일이 일어나는지 전혀 모름. 오직 서버 내부에서 오류 페이지를 찾기 위한 추가적인 호출.
즉,
1. 예외 발생 > WAS까지 전파
2. WAS는 오류 페이지 경로를 찾아 내부에서 오류 페이지 호출.
이때 오류 페이지 경로로 필터, 서블릿, 인터셉터, 컨트롤러가 모두 다시 호출
WAS는 오류 페이지를 요청 뿐 아니라 request, attribute에 오츄 정보를 넘겨준다.
오류가 발생하면 오류 페이지를 출력하기 위해 WAS 내부에서 다시 호출한다고 했다. 이때, 필터, 서블릿, 인터셉터, 컨트롤러가 모두 다시 호출된다. 그런데 이미 인증된 경우인데도 오류 페이지를 위해 또 다시 인증 체크를 하는 것은 번거롭다.
그래서 클라이언트로부터 발생한 오류인지, 오류 페이지를 출력하기 위한 내부 요청인지 구분할 수 있어야 한다.
서블릿은 이런 문제를 해결하기 위해 ([4]에서 사용했던) DispatcherType이라는 추가 정보를 제공한다.
DispatcherType
[4]의 결과(사진)를 참고하면 dispatchType=ERROR의 결과를 확인할 수 있다.
고객이 처음 요청했다면 dispatchType=REQUEST이다.
이렇게 dispatchType을 이용하면 실제 고객 요청인지, 오류 페이지 요청인지 구분할 수 있게된다.
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
: 이렇게 두 가지를 모두 넣으면 클라이언트 요청뿐만 아니라 오류 페이지 요청에도 필터가 호출된다.
아무것도 넣지 않으면 DispatcherType.REQUEST
가 default값이다. (클라이언트 요청에만 호출)
필터의 경우 DispatcherType
으로 필터를 적용 할지 안 할지 선택할 수 있었다.
그런데 인터셉터는 서블릿이 제공하는 기능이 아니라 스프링이 제공하는 기능이기 때문에 DispatcherType
과 무관하게 항상 호출된다.
대신 인터셉터는 excludePathPatterns
가 있다.
정상요청 흐름
WAS(/hello,dispatchType=REQUEST
) → 필터 → 서블릿 → 인터셉터 → 컨트롤러 → View
오류요청 흐름
1. WAS(/error-ex,dispatchType=REQUES
) → 필터 → 서블릿 → 인터셉터 → 컨트롤러
2. WAS(여기까지 전파) < 필터 < 서블릿 < 인터셉터 < 컨트롤러(예외발생)
3. WAS 오류 페이지 확인
4. WAS(/error-page/500,dispatchType=ERROR
) → 필터(x) → 서블릿 → 인터셉터(x) → 컨트롤러(/error-page/500) → View필터는
DispatchType
으로 중복 호출 제거(dispatchType=REQUEST
)
인터셉터는 경로 정보로 중복 호출 제거(excludePathPatterns("/error-page/**")
)
지금까지
WebServerCustomizer
→ 예외 종류에 따라 ErrorPage
추가 → 예외 처리용 컨트롤러 ErrorPageController
생성
이렇게 복잡한 과정을 거쳤다.
그런데 스프링 부트는 이런 과정을 모두 기본으로 제공한다.
ErrorPage
자동 등록. 이때 /error
라는 경로로 기본 오류 페이지를 설정BasicErrorController
(기본 로직들이 모두 개발되어 있음)라는 스프링 컨트롤러를 자동 등록.개발자는 BasicErrorController
에 따라 오류 페이지 화면만 등록하면 된다.
📌 참고로 우선 순위는 500
> 5xx
이렇듯 더 구체적인 것이 더 높다.
하지만 오류 관련 내부 정보들을 고객에게 노출하는 것은 좋지 않다.
on-param
: 특정 파라미터가 있을 때만 노출
그만,,