김영한 강사님의 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술을 듣고 정리한 내용입니다. 자세한 내용은 강의를 참고해주세요
HTML의 페이지에 대한 경우, BasicErrorController가 자동으로 등록해주어서, 우리는 에러 페이지만 /templates/error경로에 만들어 주면 된다.저번시간에 만든 컨트롤러를 사용하면, API로 요청해도, 오류가 발생하면 우리가 만들었던 HTML 오류페이지가 전달된다
API통신을 위한 새로운 컨트롤러를 만든다!
@RequestMapping(value = "/error-page/500",
produces = MediaType.APPLICATION_JSON_VALUE)
produces 파라미터를 이용해, http 요청 헤더의 Accept 헤더가 json타입일 때, 이 컨트롤러를 사용하게 정해줄 수 있다.ResponseEntity를 사용해 응답하고일단 API처리도 스프링 부트가 제공하는 BasicErrorController을 이용할 수 있다
다음은 BasicErrorController의 기능이다

errorHtml : produces = MediaType.TEXT_HTML_VALUE 로 클라이언트 요청의 Accept헤더 값이 text/html경우에, ModelAndView를 반환해 view화면을 제공한다
error : html외의 경우에 호출되고 ResponseEntity로 HTTP body 메시지에, JSON데이터를 반환한다
/error 페이지로 기본으로 요청하고 server.error.path 경로에서 수정 가능하다
이렇게
BasicErrorController을 이용하면, HTML페이지를 제공하는 경우에는 매우 편리하다(/error페이지에 view파일만 추가하면 된다).
그런데API는 컨트롤러나 예외마다 다른 응답 결과를 출력해야 할 수 있다. API 오류처리는@ExceptionHandler을 사용한다
다시 정리하자면
HTML 오류처리 : BasicErrorController
API 오류처리 : @ExceptionHandler
DispatcherServlet을 넘어 WAS서버에 예외가 전달되면WAS서버는 -> 서버 오류네~~ -> 500 Error를 제공했다if
IllegalArgumentException-> 컨트롤러 밖으로 오류 발생
- 서버오류(500)이 아닌
- 클라이언트 문제인(bad reauest -> 400)오류를 만들고 싶다

DispatcherServlet에서 WAS서버로 오류가 전달되면WAS서버는 -> 서버 오류라고 정의하였다 -> 500 Error!!!
HandlerExceptionResolver 즉 ExceptionReosolver을 이용해 핸들러의 오류를 DispatcherServlet으로 와서WAS서버로 보내는게 아니라, ExceptionResolver에서 한번 해결을 시도하고WAS 서버에 정상응답을 보내고ExceptionResolver 모두 시도해보고, 실패했다면WAS 서버에 보내준다WAS 서버는 500오류가 날것이다..@Slf4j
public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try {
if (ex instanceof IllegalArgumentException) {
log.info("IllegalArgumentException resolver to 400");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
return new ModelAndView();
}
} catch (IOException e) {
log.error("resolver ex",e);
}
return null;
}
}
HandlerExceptionResolver을 구현하는 리졸버를 만들어보자!!!handler와 Exception을 받는다response에 sendError로 서버 에러(500)가 아닌, 내가 보내고 싶은 error을 DispatcherServlet에 보내준다ModelAndView이다ModelAndView를 왜 반환해야되지?DispathcerServlet의 처리 코드에서 자세히 볼 수 있다빈 ModelAndView: new ModelAndView() 처럼 빈 ModelAndView 를 반환하면 뷰를 렌더링 하지 않고, 정상 흐름으로 서블릿이 리턴된다.ModelAndView 지정: ModelAndView 에 View , Model 등의 정보를 지정해서 반환하면 뷰를 렌더링 한다.null: null 을 반환하면, 다음 ExceptionResolver 를 찾아서 실행한다 만약 처리할 수 있는ExceptionResolver 가 없으면 예외 처리가 안되고, 기존에 발생한 예외를 서블릿 밖으로 던진다. -> 즉 서버 오류(500)그러면 WAS 서버에서
sendError된 에러를 보고,BasicErrorController의 기본설정경로인/error를 호출해, 오류를 처리한다!!!
@Configuration이 있는 파일에 extendHandlerExceptionResolvers를 오버라이드하고
configureHandlerExceptionResolvers 를 사용하면 스프링이 기본으로 등록하는 ExceptionResolver 가 제거되므로 주의, extendHandlerExceptionResolvers 를 사용하자DispatcherServler으로 보내버리는 것이다!!!@Slf4j
public class UserHandlerExceptionResolver implements HandlerExceptionResolver {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try {
if (ex instanceof UserException){
log.info("UserException resolver to 400");
String acceptHeader = request.getHeader("accept");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
if ("application/json".equals(acceptHeader)){
Map<String, Object> errorResult = new HashMap<>();
errorResult.put("ex",ex.getClass());
errorResult.put("message", ex.getMessage());
String result = objectMapper.writeValueAsString(errorResult);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().write(result);
return new ModelAndView();
} else {
// TEXT/HTML
return new ModelAndView("error/500");
}
}
} catch (IOException e){
log.error("resolver ex", e);
}
return null;
}
}
ExceptionResolver 인터페이스를 바로 구현하려니 매우 복잡하다ExceptionResovler을 알아보자!!!ExceptionResolver 는 다음과 같다.HandlerExceptionResolver Composite 에 다음 순서로 등록
ExceptionHandlerExceptionResolver : 1순위, @ExceptionHandler 을 처리ResponseStatusExceptionResolver : 2순위, HTTP상태 코드를 지정한다DefaultHandlerExceptionResolver : 3순위, 스프링 내부 기본 예외를 처리한다ExceptionHandlerResolver이다!!!ResponseStatusExceptionResolver 는 예외에 따라서 HTTP 상태 코드를 지정해주는 역할을 한다.@ResponseStatus 가 달려있는 예외ResponseStatusException 예외
ResponseStatusExceptionResolver 예외가 해당 애노테이션을 확인해서HttpStatus.BAD_REQUEST (400)으로 변경하고, 메시지도 담는다.sendError(statusCode,resolvedReason)을 보내고ModelAndView를 반환해reason파라미터에 메시지 기능을 사용할 수 있다messages.propertioes를 만들고 거기에 key=value값을 넣는다다음은
ResponseStatusException이다
@ResponseStatus는 개발자가 직접 변경할 수 없는 예외는 적용할 수 없다
TypeMismatchException이 발생한다면>>TypeMismatchException는 클아이언트가 요청을 잘못해서 발생하는 문제가 아닐까???DefaultHandlerExceptionResolver을 이용해 미리 처리해 두었다sendError()로 서버에 오류를 보내고, 서버에서 다시 해결하는 것을 볼 수 있다
TypeMismatchException발생!
DefaultHandlerException을 이용해 해결한다!!!지금까지 HTTP 상태 코드를 변경하고, 스프링 내부 예외의 상태코드를 변경하는 기능도 알아보았다. 그런데
HandlerExceptionResolver를 직접 사용하기는 복잡하다. API 오류 응답의 경우 response 에 직접 데이터를 넣어야 해서 매우 불편하고 번거롭다. ModelAndView 를 반환해야 하는 것도 API에는 잘 맞지 않는다.
스프링은 이 문제를 해결하기 위해@ExceptionHandler라는 매우 혁신적인 예외 처리 기능을 제공한다.
그것이 아직 소개하지 않은ExceptionHandlerExceptionResolver이다
API 예외처리의 어려운 점
HandlerExceptionResolver 를 떠올려 보면 ModelAndView 를 반환해야 했다. 이것은 API 응답에는 필요하지 않다HttpServletResponse 에 직접 응답 데이터를 넣어주었다. 이것은 매우 불편하다@ExceptionHandler 라는 애노테이션을 사용하는 매우 편리한 예외ExceptionHandlerExceptionResolver 이다.ExceptionHandlerExceptionResolver 를 기본으로 제공하고, 기본으로 제공하는 ExceptionResolver중에 우선순위도 가장 높다. - 실무에서 API 예외 처리는 대부분 이 기능을 사용한다
@Slf4j
@RestController
public class ApiExceptionV2Controller {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandler(IllegalArgumentException e){
log.error("[exceptionHandler] ex",e);
return new ErrorResult("BAD", e.getMessage());
}
@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandler(UserException e){
log.error("[exceptionHandler] ex",e);
ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandler(Exception e){
log.error("[exceptionHandler] ex",e);
return new ErrorResult("EX", "내부 오류");
}
...
}
@RestController로 @Controller와, @ResponseBody기능을 쓴다@ExceptionHandler 다음은 로직 순서이다!!!
ExceptionResolver에서 예외 해결을 시도한다ExceptionHandlerExceptionResolver가 실행된다ExceptionHandlerExceptionResolver 는 컨트롤러에 @ExceptionHandler가 있는지 찾아본다ExceptionHandlerExceptionResolver가 , 그 밑에 메서드를 실행해준다ErrorResult를 정상흐름으로 바꿔서, @RestComtroller가 있으므로, json 객체로 바꿔서 반환해준다@ResponseStatus(HttpStatus.~~~)를 붙여서 상태코드를 수정해준다

@ExceptionHandler에서 예외를 생략하면, 파라미터로 받는 예외를 자동으로 지정한다@ExceptionHandler에는 스프링 컨트롤러의 응답처렁 다양한 파라미터와 응답을 지정할 수 있다
@ExceptionHandler을 사용해서 예외를 처리할 수 있게 되었다SRP를 지키기 위해 @ControllerAdvice, @RestControllerAdvice를 사용해 분리할 수 있다

@ControllerAdvice
@ExceptionHandler 와 @ControllerAdvice를 조합하면 예외를 깔끔하게 해결할 수 있다.
@ExceptionHandler를 이용해
@RestController이므로 JSON객체의 형태로 반환이 된다!