스프링 MVC-스프링 예외처리
예외 처리
웹 어플리케이션은 상정하지 않은 많은 예외 상황을 맞닥뜨릴 수 있고, 그 때마다 투박한 오류 메세지만을 브라우저에 띄운다면 서비스의 신뢰성이 의심받을 수 있을 것이다.
이번 포스팅에서는 예외가 어떤 식으로 작동하는지, 그러한 예외를 어떻게 처리하는 지에 대해 알아볼 것이다.
예외(Exception)
예외가 발생하면 어떤 일이 벌어질까? 자바 코드를 직접 실행할 때에는 예외가 main() 스레드를 넘어가면 예외정보를 띄우고 실행이 종료된다.
하지만 톰캣 등의 WAS 서버, 서블릿 등을 거쳐 실행되는 웹 어플리케이션에서는 상황이 조금 더 복잡해진다.
try, catch 등으로 예외를 처리하지 않고 방치한다면 예외는 아래와 같이 이동한다.
WAS까지 예외가 도달하면 톰캣은 오류 코드에 따라 기본 오류 화면을 응답으로 보낸다. 해보면 알겠지만 사실상 오류 메세지만 화면에 띄우는 셈이라 대단히 투박하다.
이러한 예외를 발생시키고 싶다면 잘못된 입력으로 실제 예외를 발생시킬 수도 있고, response의 sendError() 메서드를 사용하여 서블릿 컨테이너를 예외가 발생한 것처럼 작동시킬 수도 있다.
오류 페이지
기본 오류 화면만 띄우는 것은 서비스의 신뢰성을 저해하고 완성도를 떨어뜨리므로 적절한 오류 화면을 렌더링하여 띄우는 것이 좋다.
결론적으로는 스프링 부트가 제공하는 BasicErrorController를 사용하면 되지만 로직이 어떤 식으로 작동하는지를 알아보기 위해 서블릿에서부터 천천히 올라가보도록 하겠다.
서블릿에서의 오류 페이지 등록은 아래 코드와 같이 이루어진다.
@Component
public class WebServerCustomizer implements WebServerFactoryCustomizer<ConfigurableWebServerFactory> {
@Override
public void customize(ConfigurableWebServerFactory factory) {
ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error-page/404");
ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error-page/500");
ErrorPage errorPageEx = new ErrorPage(RuntimeException.class, "/error-page/500");
factory.addErrorPages(errorPage404,errorPage500,errorPageEx);
}
}
WebServerFactoryCustomizer
위의 코드는 오류 페이지를 띄워주는 역할은 하지 않는다. 해당 코드의 에러가 발생하면 특정 path로 리다이렉트 해주는 역할을 해 줄 뿐이다.
따라서 뷰를 렌더링해줄 컨트롤러가 필요하다.
@Slf4j
@Controller
public class ErrorPageController {
@RequestMapping("/error-page/404")
public String errorPage404(HttpServletResponse response, HttpServletRequest request) {
log.info("errorPage 404");
return "error-page/404";
}
@RequestMapping("/error-page/500")
public String errorPage500(HttpServletResponse response, HttpServletRequest request) {
log.info("errorPage 500");
return "error-page/500";
}
@RequestMapping(value = "/error-page/500",produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String, Object>> errorPage500Api(
HttpServletRequest request, HttpServletResponse response) {
log.info("api 에러페이지 500");
Map<String,Object> result=new HashMap<>();
Exception ex = (Exception) request.getAttribute(ERROR_EXCEPTION);
result.put("status", request.getAttribute(ERROR_STATUS_CODE));
result.put("message",ex.getMessage());
Integer statusCode = (Integer) request.getAttribute(ERROR_STATUS_CODE);
return new ResponseEntity<>(result, HttpStatus.valueOf(statusCode));
}
}
리다이렉트 된 path를 @RequestMapping으로 받아 뷰를 반환하는 역할을 해주는 컨트롤러이다.
제일 아래 메서드는 이후 설명할 내용인데 API의 예외 처리를 할 때에도 서블릿 방식을 사용하는 것이 가능은 하다. 매개변수로 JSON 응답을 바란다는 의미의 파라미터를 받고 ResponseEntity를 반환하면 되는데 코드가 매우 지저분하고 불편하다.
어디까지나 가능은 하다는 것만 알아두고 넘어가면 될 것 같다.
오류 페이지 처리 흐름
WAS까지 예외가 도달하면 WAS는 브라우저에 응답하는 대신 예외코드에 맞는 오류페이지를 서버에 다시 요청한다. 정확히는 아래와 같이 이루어진다.
그런데 문제는 오류 페이지 요청이 오면서 필터와 인터셉터를 한번 더 거친다는 것이다.
이미 통과한 필터와 인터셉터를 한번 더 검증하는 것은 비효율적이므로 서블릿은 요청에 DispatcherType이라는 변수를 넣어 실제 고객 요청인지, 오류 페이지 요청인지를 구분한다.
public enum DispatcherType {
FORWARD,
INCLUDE,
REQUEST,
ASYNC,
ERROR
}
DispatcherType은 위와 같이 이루어져 있고 각 파리미터의 의미는 다음과 같다.
- REQUEST : 클라이언트 요청
- ERROR : 오류 요청
- FORWARD : MVC에서 배웠던 서블릿에서 다른 서블릿이나 JSP를 호출할 때
- INCLUDE : 서블릿에서 다른 서블릿이나 JSP의 결과를 포함할 때
- ASYNC : 서블릿 비동기 호출
필터를 FilterRegistrationBean에서 등록할 때 어떤 DispatcherType을 처리할 것인지 설정할 수 있다.
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean=new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
return filterRegistrationBean;
}
기본 값은 setDispatcherTypes(DispatcherType.REQUEST)로, 클라이언트 요청만 처리하도록 되어있다.
필터는 서블릿에서 제공하는 서블릿 필터이기 때문에 DispatcherType으로 처리가 가능했다. 하지만 인터셉터는 서블릿과 무관한 스프링에서 제공하는 기능이기 때문에 기본값으로 두면 실제로 두 번 처리된다.
대신 인터셉터는 excludePath를 통한 강력한 요청 경로의 제외를 제공하기 때문에 아래와 같이 오류 페이지 경로를 제외시켜주면 해결이 가능하다.
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "*.ico", "/error", "/error-page/**"); //오류페이지 경로 제외
}
BasicErrorController
지금까지 오류페이지 객체를 만들고 등록하는 등의 모든 과정은 스프링 부트에서 기본적으로 제공한다. 스프링 부트에 내장된 BasicErrorController는 기본 경로인 ‘/error’에 있는 모든 오류 페이지들을 자동으로 등록하고, 오류를 매핑하여 처리하는 컨트롤러 역할을 한다.
개발자는 BasicErrorController의 룰을 숙지하고 에러 페이지 역할을 할 파일만 경로에 넣어두면 되는 것이다.
BasicErrorController의 룰은 다음과 같다.
- 뷰 템플릿
- resources/templates/error/500.html
- resources/templates/error/5xx.html
- 정적 리소스( static , public )
- resources/static/error/400.html
- resources/static/error/404.html
- resources/static/error/4xx.html
- 적용 대상이 없을 때 뷰 이름( error )
- resources/templates/error.html
위의 순서대로 처리되고, 더 자세한 것이 우위에 있는 스프링의 기본 규칙 상 404, 400 등이 4xx보다 우위로 처리된다.
또한 BasicErrorController는 예외에 대한 다양한 정보를 자동으로 모델에 담아 뷰에 전달한다. 뷰는 해당 정보들을 통해 오류 페이지를 동적으로 렌더링 할 수 있다.
하지만 오류에 대한 정보를 유저에게 보이는 것은 좋지 않으므로 application.properties에서 설정을 통해 정보의 노출을 조절하는 것이 좋다.
정리
스프링에서 예외가 어떤 식으로 작동하는지, 예외 페이지의 등록과 처리 등을 알아보았다. 결론적으로 웹에서의 예외 처리는 BasicErrorController를 사용하면 된다.
다음 포스팅에서는 API에서의 예외처리는 웹과 어떻게 다른지, 그러므로 어떻게 처리해야 하는지에 대해 알아보도록 하겠다.