위 그림은 Spring에서 에러가 발생했을 때 처리되는 과정이다.
에러가 발생했을 때 try-catch문으로 Exception을 잡지 않으면 에러는 WAS까지 전달된다. WAS는 에러의 종류를 확인하고 그에 따른 요청을 서블릿에게 재요청한다. 서블릿은 요청과 매핑되는 컨트롤러를 호출하여 오류에 대한 응답을 클라이언트에게 전송한다.
이때 클라이언트가 HTML 응답을 원한다면 에러페이지를 렌더링하여 응답하면 된다. 그런데 클라이언트가 application/json과 같이, HTML이 아닌 다른 방식으로 에러를 응답하기를 바란다면 어떻게 해야 할까?
우선 오류가 발생했을 때, 오류 종류에 따른 WAS 설정을 해보자.
WebServerFactoryCustomizer 구현체
@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);
}
}
에러 종류에 따른 요청URL을 담은 에러페이지 객체를 생성하여 ConfigurableWebSereverFactory 객체에 등록하였다. WAS는 에러가 발생하면 등록된 에러페이지 객체 중에 에러 종류와 맞는 객체를 선택한다.
에러페이지 Controller
@Slf4j
@Controller
public class ErrorPageController {
// 500에러가 발생한 경우 : 에러페이지 응답
@RequestMapping("/error-page/500")
public String errorPage500(HttpServletRequest request, HttpServletResponse response) {
log.info("errorPage500");
return "error-page/500";
}
// 500에러가 발생한 경우 : JSON 응답
@RequestMapping(value = "/error-page/500", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Map<String,Object>> errorPage500Api(
HttpServletRequest request,
HttpServletResponse response
){
log.info("API errorPage500");
// 에러 정보를 담은 HashMap 객체 생성
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(RequestDispatcher.ERROR_STATUS_CODE);
//HashMap과 HTTP 오류상태코드 반환
return new ResponseEntity<>(result, HttpStatus.valueOf(statusCode) );
}
}
두 메소드는 @RequestMapping에 설정된 URL이 동일하다.
그러나 produces 설정이 다르다. produces는 HTTP Accept 헤더를 가리키는 설정이다. Accept 헤더는 클라이언트가 서버에게 요청하는 응답 데이터 형식이다. Accept 헤더가 text/html이면 응답으로 HTML 페이지를 요청한 것이다. application/json이면 응답으로 JSON데이터를 요청한 것이다. 그러므로 produces 설정을 사용하면, 설정된 URL이 같더라도 Accept 헤더에 따라 다른 메소드가 호출되도록 할 수 있다. 위 코드를 보면, errorPage500은 HTML을 요청한 경우, errorPageApi는 json 데이터를 요청한 경우 호출된다.
HttpEntity(ResponseEntity)에 Map이나 객체를 넣고 반환하면 Spring의 메시지 컨버터가 동작하여 HttpEntity 객체를 JSON 데이터로 변환하여 HTTP Body 영역에 저장한다. 자세한 내용은 지난 포스팅을 참고하면 된다.
그럼 테스트를 해보자.
500에러를 일으키는 URL로 요청을 하였다. 이때 Header의 Accept 영역은 application/json으로 설정해준다. 그럼 어떤 응답을 받을까?
HTML 오류페이지가 아닌 JSON 형식의 데이터를 응답받았다.
HTML 형식의 에러페이지가 아닌 JSON 형식의 데이터를 에러 데이터로 요청하려면 위와 같은 과정이 필요하다. 여기까지는 직접 손으로 만든 API 에러처리 과정이다. SpringBoot는 위 과정을 자동으로 구현해 놓았다.
BasicErrorController
SpringBoot는 이미 구현된 에러페이지 Controller를 가지고 있다.
그것이 BasicErrorController이다.
BasicErrorController
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
// 중략 ...
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
// 중략...
}
위에서 직접 만든 에러페이지 Controller는 produces를 application/json으로 구별했지만 BasicErrorController는 text/html로 구분한다. 동일한 URL로 에러 관련 요청이 들어오면 produces 설정에 따라 다른 메소드가 호출된다. HTML인 경우 ModelAndView가 반환되고 API인 경우 ResponseEntity가 반환된다.
그런데 이렇게 되면 어떤 데이터가 JSON에 추가되어야 할지 개발자가 조작하지 못한다. 이를 위해, SpringBoot는 application.properties에 설정이 가능하도록 지원한다.
application.properties
server.error.include-exception=true
server.error.include-message=always
exception정보와 오류 메시지를 JSON데이터에 담도록 설정해보았다. 그럼 Accept를 application/json으로 설정하여 PostMan에서 요청해보자.
이와 같이, 디폴트 데이터와 비롯하여 exception 정보와 오류메시지가 JSON 형태로 클라이언트에 전달되었다.
여기까지 살펴보면, SpringBoot가 굉장히 편리한 기능을 제공하는 것 같다.
그러나 사실이 아니다.
API 응답은 다양한 형태가 요구된다. exception 정보가 필요할 수도 필요하지 않을 수도 있다. 요청에 따라 응답은 가지각색으로 변하는데, 이를 application.properties로 핸들링한다는 것은 말이 안된다. 그러므로 BasicErrorController는 HTML 응답인 경우에만 사용하는 것이 적합하다. 404, 500, 403 에러와 같이, 몇가지 경우로 정해져있는 에러페이지에 적합하다. API와 같이, 응답의 형태가 여러가지인 API 응답에는 부적절하다.
다음 포스팅에서는 API 에러를 응답하기 위한 방법에 대해서 다루어 보겠다.
참고자료
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의
웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있
www.inflearn.com
'SPRING > Spring MVC' 카테고리의 다른 글
[SpringMVC] API 예외처리하기(3) - @ExceptionHandler, @ControllerAdvice (0) | 2023.09.13 |
---|---|
[SpringMVC] API 예외처리하기(2) - HandlerExceptionResolver (0) | 2023.09.12 |
[SpringMVC] SpringBoot에서 오류 페이지 띄우기 (0) | 2023.09.01 |
[SpringMVC] 예외처리 ( 필터, 인터셉터 ) (0) | 2023.09.01 |
[SpringMVC] 오류페이지 띄우기 (0) | 2023.09.01 |