Spring Boot가 실행되면 내장 톰캣(Was)를 띄운다. 톰캣은 Web Server와 Web Container로 구성되어 있고 Web Container는 Servlet Container라고도 부른다. Servlet Container를 통해 동적인 데이터를 클라이언트에 반환할 수 있다.
사용자의 요청이 오면 흐름은 아래와 같다.
애플리케이션에서 예외가 발생할 때 우리는 흔히 try~catch~finally 문을 통해 예외 처리를 한다. 하지만 만약 애플리케이션에서 발생한 예외가 Servlet 밖으로 나오면 어떻게 될까? 아래 그림을 봐보자
만약 Srping Web을 사용하고 있다면 여기서 서블릿은 DispatcherServlet를 의미한다. 또한 Was는 위에서 설명한 내장 톰캣(tomcat)을 의미한다. 별도의 예외 처리를 하지 않으면 예외는 Was까지 전달된다. Was는 예외가 서블릿 밖으로 전달되었거나 HttpServletResponse가 제공해주는 sendError()메소드의 호출 기록을 확인하여 오류 페이지 경로를 찾아 오류 페이지를 내부적으로 호출하는데 이때 필터,서블릿,인터셉터,컨트롤러를 거친다. 전체 과정은 아래와 같다.
Was(내장 톰캣) > 필터 > 서블릿 > 인터셉터 > 컨트롤러
if(예외 발생)...
컨트롤러(예외 발생) > 인터셉터 > 서블릿 > 필터 > Was(내잠 톰캣,sendError()확인!)
Was가 오류 페이지 확인...
> Was(톰캣) > 필터 > 서블릿 > 인터셉터 > 컨트롤러
DispatcherType과 인터셉터의 excludePathPatterns를 통해 Was가 예외 페이지를 호출할 때 필터와 인터셉터를 거치지 않도록 설정할 수 있다. 이때 스프링의 경우 에러 처리를 위해 BasicErrorController를 빈으로 등록하였는데 오류 페이지 경로를 기본으로 /error로 설정하여 해당 경로를 Was가 호출한다. 즉! 별도의 설정이 없으면 /error 경로로 Was 예외 처리를 위해 Controller를 호출을 하고 BasicErrorController가 에러 페이지 또는 에러 메세지를 반환함으로써 이를 처리한다.
위와 같이 Exception을 발생하는 코드를 작성하고 Postman을 사용하여 해당 api를 호출했을 때 아래와 같은 결과를 확인할 수 있다.
웹 페이지 상에는 아래와 같은 에러 페이지를 확인할 수 있다.
해당 에러 메세지와 페이지는 좋은 응답이라고 말할 수 없다.
따라서 클라이언트 입장에서는 에러 파악을 하기 정말 힘들다. 그러면 협업 능률이 떨어질 것이다. 팀 분위기도,,ㅎㅎ
해당 Postman에서의 api 요청을 디버깅하였다.
위 코드는 빈으로 등록된 BasicErrorController의 내부 코드이다. 직접 작성한 TestController에서 발생한 RuntimeException이 Was에 전달되고 스프링은 별도의 설정을 하지 않았기 때문에 /error 경로로 에러 처리를 요청한다. 위 화면에서 requestURI = "/error"를 통해 해당 사실을 확인할 수 있다. BasicErrorController는 들어온 요청을 처리하기 위해 error() 메소드를 실행하는 것을 확인할 수 있다. 반환된 결과는 위에서 확인했던 Json 형태의 응답 값들이다.
위에서 설명했듯이 윗 방식으로 에러 처리를 하고 응답하면
처리되지 않는 에러가 Was에 전달되기 때문에 클라이언트 요청이 잘못 됐어도 Status 500으로 응답하게 된다.
Restful api 환경에서 각각의 api마다, 하나의 api에서 서로 다른 원인으로 에러가 발생하는데 이때마다 세밀한 처리를 통해 프론트측으로 Json 응답을 보내주기 까다롭다.
스프링에서 이에 대한 해결책으로 HandlerExceptionResolver 인터페이스를 만들고 이것의 구현체들은 빈으로 등록하였다. ExceptionResolver라고도 부른다. 공식 문서의 설명은 다음과 같다.
Interface to be implemented by objects that can resolve exceptions thrown during handler mapping or execution, in the typical case to error views. Implementors are typically registered as beans in the application context.
HandlerExceptionResolver는 Handler 즉, Controller 실행 도중 발생한 예외를 처리하기 위한 인터페이스이다. ExceptionResolver에서 예외를 처리하고 ModelAndView를 반환하는데 이것은 Was가 예외가 아닌 정상 응답을 받았다고 생각하게 하기 위함이다.
내부 구현 코드를 뜯어보자! resolveException()메소드를 통해 예외를 처리하는데 매개 변수 중 handler는 예외를 발생시킨 Controller를 의미하고 ex는 발생된 예외를 의미한다. 메소드의 반환 값으로 ModelAndView를 반환하는데 빈 ModelAndView를 반환하면 view를 렌더링하지 않고 정상흐름으로 DispatcherServlet으로 반환되고 Model과 View를 담아서 반환하면 View를 렌더링한다. 하지만 null를 반환하는 경우, 해당 ExceptionResolver의 구현체가 예외를 처리할 수 없음을 뜻하고 다음 순위의 ExceptionResolver를 찾는다. 만약, 처리할 ExceptionResolver가 존재하지 않는다면 에러는 처리되지 않은 채로 Was에게 전달된다! 이 경우, 처음 설명했던 것처럼 Spring은 /error 경로로 BasicErrorController를 호출할 것이다.
If an exception occurs during request mapping or is thrown from a request handler (such as a @Controller), the DispatcherServlet delegates to a chain of HandlerExceptionResolver beans to resolve the exception and provide alternative handling, which is typically an error response.
스프링 공식 문서에 나와 있듯이 우리가 일일히 발생한 예외마다 처리하는 ExceptionHandler 구현체를 구현하기엔 너무 복잡하고 번거롭다. 위에서도 말했듯이 스프링에서 이미 몇 가지의 구현체를 만들어 빈으로 등록하였고 DispathcerServlet은 @Controller에서 던져진 예외 처리를 HandlerExceptionResolver Chain에 위임한다. 우리는 그것을 이용하면 된다.
스프링 내부적으로 예외처리하는 과정이 이해가 잘 되네요 :) 감사합니다