스프링 API 에러처리
API에서 에러가 발생하면 HTML 페이지가 아닌 JSON
형태의 에러 데이터를 클라이언트에게 보내야 한다.
스프링 부트는 HTML뿐만 아니라 API 예외처리도 기본적으로 제공한다.
스프링부트 기본 오류처리
BaseErrorController
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE) // text/html
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {}
@RequestMapping // 그 외(applcation/json 등)
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {}
API 예외처리
- 예외처리는
ExceptionResolver
가 처리한다.
API 예외처리 로직
- 컨트롤러 이하에서 로직 수행 중 예외처리가 발생하면
DispatcherServlet
까지 예외가 던져진다 -
processHandlerExcpetion()
이 호출되어 적절한HandlerExceptionResolver
를 가져온다 - 호출된 ExceptionResolver가 예외를 처리한다
public interface HandlerExceptionResolver {
@Nullable
ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}
-
Object handler
: 핸들러 정보 -
Exception ex
: 해당 핸들러에서 발생한 Exception 정보 -
ModelAndView
를 return 함으로써 클라이언트에게 정상 흐름으로 반환된다
public class DispatcherServlet extends FrameworkServlet {
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
....
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
....
}
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
....
// Exception 발생시
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
...
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
....
}
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {
...
// 등록된 HandlerExceptionResolvers 탐색
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
....
throw ex;
}
}
스프링이 제공하는 ExceptionResolver
- 우선순위
ExceptionHandlerExceptionResolver
ResponseStatusExceptionResolver
DefaultHandlerExceptionResolver
★ 1. ExceptionHandlerExceptionResolver
-
@ExceptionHandler
로 처리한 exception - 세밀한 에러처리가 가능하다
ExceptionAdvice
-
@ExceptionHandler
: 설정한 예외가 던져지면 해당 핸들러가 실행되어 처리한다 -
@ControllerAdvice
- 여러 컨트롤러에서 발생한 예외를 한 곳에서 관리하고 처리할 수 있게 도와주는 어노테이션
- 대상으로 지정한 여러 Controller에
@ExceptionHandler
,@InitBinder
기능을 부여한다 - 대상을 지정하지 않으면 모든 Controller에 적용된다
@Log4j2
@RestControllerAdvice
public class ExceptionAdvice {
/*
* Custom Exception
*/
@ExceptionHandler(ApiException.class)
protected ResponseEntity<ErrorResponse> apiExceptionHandler(final ApiException e) {
log.error("ApiException : [" + e.getErrorCode().getCode() + "] " + e.getErrorCode().getMessage());
final ErrorCode errorCode = e.getErrorCode();
final ErrorResponse response = ErrorResponse.of(errorCode);
return ResponseEntity.status(errorCode.getStatus()).body(response);
}
/*
* @Validated Binding Exception
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
protected ResponseEntity<ErrorResponse> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
log.error("handleMethodArgumentNotValidException", e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
/*
* @ModelAttribute Binding Exception
*/
@ExceptionHandler(BindException.class)
protected ResponseEntity<ErrorResponse> bindExceptionHandler(BindException e) {
log.error("handleBindException", e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
/*
* HTTP method not allowed
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
protected ResponseEntity<ErrorResponse> httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException e) {
log.error("handleHttpRequestMethodNotSupportedException", e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.METHOD_NOT_ALLOWED);
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).body(response);
}
@ExceptionHandler(Exception.class)
protected ResponseEntity<ErrorResponse> exceptionHandler(Exception e) {
log.error("handleEntityNotFoundException", e);
final ErrorResponse response = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
}
ApiException
- 커스텀 Exception
@Getter
public class ApiException extends RuntimeException {
private ErrorCode errorCode;
public ApiException(ErrorCode e) {
super(e.getMessage());
this.errorCode = e;
}
public ApiException(ErrorCode e, String message) {
super(message);
this.errorCode = e;
}
}
ErrorResponse (DTO)
- HttpStatus code, error code, error message를 전달할 객체
- errors를 List로 바인딩 할 수 있게 작성
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ErrorResponse {
private int status;
private String code;
private String message;
private List<CustomFieldError> errors;
private ErrorResponse(final ErrorCode code) {
this.status = code.getStatus().value();
this.code = code.getCode();
this.message = code.getMessage();
this.errors = new ArrayList<>();
}
private ErrorResponse(final ErrorCode code, final List<CustomFieldError> errors) {
this.status = code.getStatus().value();
this.code = code.getCode();
this.message = code.getMessage();
this.errors = errors;
}
public static ErrorResponse of(final ErrorCode code) {
return new ErrorResponse(code);
}
public static ErrorResponse of(final ErrorCode code, final List<CustomFieldError> errors) {
return new ErrorResponse(code, errors);
}
public static ErrorResponse of(final ErrorCode code, final BindingResult bindingResult) {
return new ErrorResponse(code, CustomFieldError.of(bindingResult));
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class CustomFieldError {
private String field;
private String value;
private String message;
public static List<CustomFieldError> of(final String field, final String value, final String message) {
List<CustomFieldError> fieldErrors = new ArrayList<>();
fieldErrors.add(new CustomFieldError(field, value, message));
return fieldErrors;
}
private static List<CustomFieldError> of(final BindingResult bindingResult) {
final List<FieldError> fieldErrors = bindingResult.getFieldErrors();
return fieldErrors.stream().map(error -> new CustomFieldError(
error.getField(),
error.getRejectedValue() == null ? "" : error.getRejectedValue().toString(),
error.getDefaultMessage()))
.collect(Collectors.toList());
}
}
}
ErrorCode (enum)
- 커스텀 에러코드를 enum으로 관리한다
@Getter
@ToString
public enum ErrorCode {
// COMMON
INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "CO01", "잘못된 입력값 입니다."),
NO_AUTHENTICATION(HttpStatus.UNAUTHORIZED, "CO02", "권한이 없습니다."),
NO_AUTHORIZATION(HttpStatus.FORBIDDEN, "CO03", "권한이 없습니다."),
NOT_FOUND(HttpStatus.NOT_FOUND, "CO04", "잘못된 요청입니다."),
METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "CO05", "잘못된 요청입니다."),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "CO06", "서버 에러입니다."),
// USER
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "US01", "존재하지 않는 회원입니다."),
FOUND_NAME(HttpStatus.CONFLICT, "US02", "이미 존재하는 닉네임입니다."),
// SUPPORT
UNOPENED_CONTENT(HttpStatus.FORBIDDEN, "SU01", "비공개 게시글입니다."),
NOT_FOUND_CONTENT(HttpStatus.NOT_FOUND, "SU02", "존재하지 않는 게시글입니다.");
private final HttpStatus status;
private final String code;
private String message;
ErrorCode(HttpStatus status, String code) {
this.status = status;
this.code = code;
}
ErrorCode(HttpStatus status, String code, String message) {
this.status = status;
this.code = code;
this.message = message;
}
}
2. ResponseStatusExceptionResolver
-
@ResponseStatus
로 처리한 exception - 해당 커스텀 Exception이 발생했을 때 설정한 Response Status가 반환된다
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "잘못된 오청 오류")
public class BadRequestException extends RuntimeException {
public BadRequestException(String message) {
super(message);
}
}
- 만약에 message가 null로 비어서 표출된다면 다음 설정을 추가한다
server:
error:
include-message: always
include-binding-errors: always
3. DefaultHandlerExceptionResolver
- 스프링 내부 기본 예외를 처리하는 exception
- 다양한 Exception 경우를 스프링에서 기본으로 구현해 놓았다