웹 서비스에서는 잘못된 요청이 서버로 들어왔을 때, 서비스를 사용하는 사용자에게 요청이 잘못되었다는 것을 알려야한다. 하지만, 요청이 잘못되었다는 것을 어떤 방식으로 알려야할까?
스프링이 없는 순수 서블릿 컨테이너, 즉 아파치 톰캣같은 WAS는 서블릿을 이용하기 때문에, 서블릿이 지원하는 예외처리 방식을 사용한다. 서블릿은 다음 2가지 방식으로 예외 처리를 지원한다.
자바에서 예외(exception)란 사용자의 잘못된 조작이나 개발자의 코딩 실수로 인해 발생하는 프로그램 오류를 말한다.
일반적인 자바 프로그램과 웹 애플리케이션에서 예외가 발생하는 경우를 나누어서 생각해보자.
웹 애플리케이션의 경우, 발생한 예외를 애플리케이션 내에서 적절히 처리하지 않으면 예외가 WAS까지 전달된다.
톰캣은 예외가 전달되면 오류 페이지를 기본으로 제공하는 로직을 내부에 갖고 있기에, 상황에 맞는 기본 오류 페이지를 사용자에게 반환한다.
또다른 예외를 처리하는 방법으로는 sendError() 메서드가 있다. 이 메서드를 호출한다고 당장 예외가 발생하는 것은 아니지만, 이 메서드를 이용하면 서블릿 컨테이너에게 오류가 발생했다는 점을 전달할 수 있다.
response.sendError()를 호출하면 response 내부에는 오류가 발생했다는 상태가 저장되고, 서블릿 컨테이너는 사용자에게 응답하기 전에 response 내부에 sendError()가 호출되었는지를 확인한다. 그리고 호출되었다면 설정한 오류 코드에 맞게 기본 오류 페이지를 사용자에게 반환한다.
참고.
sendError() 메서드를 이용하면 WAS에게 HTTP 상태 코드를 통해 어떤 종류의 오류(400번대, 500번대)가 발생했는지에 대한 정보를 전달할 수 있다. 하지만, 단순히 WAS에 예외가 던져지면, WAS(톰캣)는 기본적으로 모든 예외를 HTTP 상태코드 500(Internal Server Error)로 인식한다.
서블릿은 예외가 발생해서 서블릿 밖으로 전달되거나 response.sendError()가 호출되었을 때 각각의 상황에 맞는 오류 페이지를 반환하는 기능을 제공한다. 그리고 스프링 부트를 이용하면 서블릿이 제공하는 오류 페이지를 쉽게 등록할 수 있다.
참고.
WebServerFactoryCustomizer를 이용하면, 스프링 부트에서 내장하고 있는 WAS에 상황에 맞는 오류 페이지들을 등록해준다.
오류 페이지가 사용자에게 반환되기까지의 과정과 원리에 대해서 좀 더 자세히 알아보자.
서블릿은 예외가 발생해서 서블릿 밖으로 전달되거나 response.sendError()가 호출되었을 때 설정된 오류 페이지를 찾는다. 각각의 경우에 따른 흐름은 아래와 같다. 예외는 컨트롤러에서 발생했다고 가정하겠다.
WAS는 예외가 던져지거나 sendError()가 호출된 기록이 있다면, 오류 페이지 정보를 확인하고, 지정된 오류 페이지 경로로 재요청한다. 따라서 아래와 같은 흐름이 이어진다.
여기서 중요한 점은 위의 오류 페이지 재요청과정이 서버 내부에서만 일어나는 추가 호출이기 때문에,
사용자(클라이언트, 즉 웹 브라우저)는 이런 일이 일어나는지 전혀 모른다는 것이다.
정리하면 다음과 같다.
WAS는 오류 페이지 경로로 재요청할 때 단순히 다시 요청만 하는 것이 아니라, 발생한 오류 정보를 request의 attribute에 추가해서 넘겨준다. 이 오류 정보는 컨트롤러에서 사용할 수 있다.
위에서 재요청시에 오류페이지 경로로 필터, 서블릿, 인터셉터, 컨트롤러가 모두 다시 호출된다고 했다. 그런데 생각해보면 서버 내부에서 오류 페이지를 호출한다고 해서 필터나 인터셉터가 한번 더 호출되는 것은 매우 비효율적이다. 이를 방지하기 위해서 서블릿은 DispatcherType이라는 추가 정보를 제공한다. 이를 이용하여 해당 요청이 클라이언트로부터 발생한 정상 요청인지, 아니면 서버내부에서 발생한 오류 페이지 경로로의 재요청인지 구분한다.
참고.
해당 요청이 클라이언트로부터 발생한 정상 요청이라면 DispatcherType은 REQUEST이고,
서버내부에서 발생한 오류 페이지 경로로의 재요청이라면 DispatcherType은 ERROR이다.
DispatcherType을 이용하면, 설정 파일에서 어떤 DispatcherType의 요청에 필터를 적용할지 설정할 수 있다. 인터셉터의 경우 서블릿이 제공하는 기능이 아니라 스프링이 제공하는 기능이기 때문에 DispatcherType와 무관하게 항상 호출된다. 그러므로, 인터셉터는 적용을 원하지 않을 경우 경로 설정을 통해 오류 페이지 경로를 제외시켜주면 된다.
스프링 부트 없이 오류 페이지를 보여주려면 다음과 같은 복잡한 과정이 필요하다.
스프링 부트는 이런 과정을 모두 기본으로 제공한다.
스프링부트는 '/error' 라는 경로로 기본 오류 페이지를 설정하고, BasicErrorController라는 스프링 컨트롤러를 자동으로 등록하여, '/error' 경로로 들어오는 오류 페이지 재요청을 처리한다. 개발자는 오류 페이지 파일(HTML 파일)만 BasicErrorController가 제공하는 룰과 우선순위에 따라서 등록하면 된다.
참고.
스프링 부트는 오류 페이지 파일을 선택할 때, 다으모가 같은 우선순위로 찾는다.
1. 뷰 템플릿 - 구체적인 오류 (ex. resources/templates/error/500.html)
2. 뷰 템플릿 - 포괄적인 오류 (ex. resources/templates/error/5xx.html)
3. 정적 리소스 - 구체적인 오류 (ex. resources/static/error/400.html)
4. 정적 리소스 - 포괄적인 오류 (ex. resources/static/error/4xx.html)
5. 적용 대상이 없을 때 (ex. resources/static/error.html)
BasicErrorController는 model에 오류와 관련된 정보를 담아서 뷰에 전달한다. 하지만, 오류와 관련된 내부 정보들은 사용자에게 노출하지 않아야한다. 고객이 해당 정보를 읽어도 알아볼 수 없고, 보안상 문제가 될 수도 있기 때문이다.
사용자에게는 가독성이 좋은 메시지와 이쁜 오류 화면을 보여주고, 오류는 서버 로그에 남겨서 로그로 확인해야한다.