단순히 에러 및 오류가 났을 때는 BasicErrorController가 제공하는 기능을 사용하여 사용자에게 해당 번호대 오류 페이지를 보여주기만 하면 되었습니다.
하지만 API의 경우 사용자에게 각각 상황에 맞는 오류에 대하여 JSON으로 응답을 해야하기 때문에 HTML페이지를 보여주는 것 이상으로 세밀하게 JSON을 제공해야 합니다.
BasicErrorController는 오류 페이지 뿐만 아니라 API 예외 처리 기능도 제공합니다!
잠깐 BasicErrorController의 코드 일부를 먼저 보시죠!
@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);
}
여기서 중요히 보실 부분은 RequestMapping에 produces입니다.
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response)
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request)
이렇게 두 부분으로 나뉘어 진 것을 보실 수 있습니다.
errorHtml()의 produces = MediaType.TEXT_HTML_VALUE 는 클라이언트의 requset Accept Header값이 text/html일 때를 의미합니다.
즉 text/html형식으로 요청이 오면 오류 페이지를 제공한다고 보시면 됩니다.
그리고, error()는 text/html이외의 요청에 호출 되고 반환 값은 코드에서 보시는 데로 ResponseEntoty로 Json 데이터를 반환하게 됩니다.
BasicErrorController는 Json 메시지도 변경할 수 있다고 합니다. 하지만 물론 BasicErrorControlelr는 오류 페이지를 제공하는데는 매우 효율적이지만 API에서 일어나는 오류에 대한 처리는 정말 세세하게 이뤄져야 하므로 잘 사용하지는 않는다고 합니다( ex IllegalArgumentException의 경우 입력값을 잘못 넣은 사용자의 잘못이라고 볼 수 있는데 에러 코드는 500으로 나감. 이 에러코드를 4xx대의 코드로 바꿔서 보내줘야한다.. 이런!).
그 대신 HandlerExceptionResolver를 사용한다고 합니다!
기본적으로 제공하는 ExceptionResolver
ExceptionHandlerExceptioinResolver
@ExceptionHandler annotaion처리
ResponseStatusExceptionResolver
@ResponseStatus(value=HttpStatus.BAD_REQUEST) ... Http 상태 코드를 지정
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 요청 오류") public class BadRequestException extends IllegalArgumentException
이런 식으로 사용 가능 합니다. BadRequestException 예외가 발생하게 되면 오류 코드를 HttpStatus.BAD_REQUEST로 바꾸고 메시지를 reason에 담아 응답하게 됩니다.
DefaultHandlerExceptionResolver
스프링 내부에서 발생하는 예외를 해결 합니다.
ex) TypeMismatchException 과 같은 경우 대부분의 클라이언트 잘못. 이와 같은 경우 스프링에서 InternalServerError가 아닌 BAD_REQUEST의 에러로 반환
우선 순위 순 입니다.
상황 상황 마다 세밀한 에러 처리를 하고 싶을 때 ExceptionHandlerExceptioinResolver를 사용합니다.
CODE 🙋♀️를 보시죠~
ErrorResult
@Data
@AllArgsConstructor
public class ErrorResult {
private String code;
private String message;
}
ApiExceptionController
@Slf4j
@RestController
public class ApiExceptionController {
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult badHandler(IllegalArgumentException e){
return new ErrorResult("BAD",e.getMessage() );
}
@GetMapping("/test/{code}")
public String test(@PathVariable("code")String code){
if(code.equals("bad")){
throw new IllegalArgumentException("잘못된 값");
}
return "ok";
}
}
http://localhost:8080/test/bad 로 요청이 오면
ErrorResult에 "BAD"라는 code와 "잘못된 값"이라는 message가 담겨서 반환 될 것 입니다!
그외에 다른 요청은 "OK"를 반환하게 될 것입니다.
한번 확인 해보죠!
이번에는 IllegalArgument를 상속 받은 예외 클래스를 추가한 후 실행해 봅시다!
UserException
public class UserException extends IllegalArgumentException{
public UserException() {
super();
}
public UserException(String s) {
super(s);
}
public UserException(String message, Throwable cause) {
super(message, cause);
}
public UserException(Throwable cause) {
super(cause);
}
}
ApiExceptionController
@Slf4j
@RestController
public class ApiExceptionController {
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult badHandler(IllegalArgumentException e){
return new ErrorResult("BAD",e.getMessage() );
}
@ExceptionHandler(UserException.class)
public ErrorResult userHandler(UserException e){
return new ErrorResult("USER-ERROR",e.getMessage());
}
@GetMapping("/test/{code}")
public String test(@PathVariable("code")String code){
if(code.equals("bad")){
throw new IllegalArgumentException("잘못된 값");
}
if(code.equals("user-bad")){
throw new UserException("USER 잘못!");
}
return "ok";
}
}
http://localhost:8080/test/user-bad 로 요청
결과
보셨다시피 해당 컨트롤러에서 예외가 발생하면 @ExceptionHandler가 지정된 메서드가 실행 됩니다.
IllegalArgumentException을 상속받은 UserException을 처리한 것을 보셨다 시피 다양하게 상황 상황 맞게 예외를 처리할 수 있는 것을 보실 수 있습니다!
매번 Controller마다 @ExceptionHandler를 지정하기 귀찮죠!
그래서 @ControllerAdvice & @RestControllerAdvice 기능을 제공한다고 합니다!
이들은 대상으로 지정한 여러 컨트롤러에 @ExceptionHandler 및 @InitBinder 기능을 부여 해주며 대상을 지정해주지 않으면 모든 컨트롤러에 지정된다고 합니다!
ex)
@Slf4j
@RestControllerAdvice
public class ExControllerAdvice {
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult badHandler(IllegalArgumentException e){
return new ErrorResult("BAD",e.getMessage() );
}
@ExceptionHandler(UserException.class)
public ErrorResult userHandler(UserException e){
return new ErrorResult("USER-ERROR",e.getMessage());
}
}
이 글은 인프런 김영한님의 '스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술'을 수강하고 작성합니다.
출처:https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard