@Getter
@RequiredArgsConstructor
public enum ErrorCode {
OK(0, HttpStatus.OK, "Ok"),
BAD_REQUEST(10000, HttpStatus.BAD_REQUEST, "Bad request"),
SPRING_BAD_REQUEST(10001, HttpStatus.BAD_REQUEST, "Spring-detected bad request"),
VALIDATION_ERROR(10002, HttpStatus.BAD_REQUEST, "Validation error"),
NOT_FOUND(10003, HttpStatus.NOT_FOUND, "Requested resource is not found"),
INTERNAL_ERROR(20000, HttpStatus.INTERNAL_SERVER_ERROR, "Internal error"),
SPRING_INTERNAL_ERROR(20001, HttpStatus.INTERNAL_SERVER_ERROR, "Spring-detected internal error"),
DATA_ACCESS_ERROR(20002, HttpStatus.INTERNAL_SERVER_ERROR, "Data access error")
;
private final Integer code;
private final HttpStatus httpStatus;
private final String message;
public static ErrorCode valueOf(HttpStatus httpStatus) {
if (httpStatus == null) { throw new GeneralException("HttpStatus is null."); }
return Arrays.stream(values())
.filter(errorCode -> errorCode.getHttpStatus() == httpStatus)
.findFirst()
.orElseGet(() -> {
if (httpStatus.is4xxClientError()) { return ErrorCode.BAD_REQUEST; }
else if (httpStatus.is5xxServerError()) { return ErrorCode.INTERNAL_ERROR; }
else { return ErrorCode.OK; }
});
}
public String getMessage(Throwable e) {
return this.getMessage(this.getMessage() + " - " + e.getMessage());
}
public String getMessage(String message) {
return Optional.ofNullable(message)
.filter(Predicate.not(String::isBlank))
.orElse(this.getMessage());
}
@Override
public String toString() {
return String.format("%s (%d)", this.name(), this.getCode());
}
}
에러 코드는 enum 타입으로 한 곳에서 관리하는게 좋다.
@Getter
public class GeneralException extends RuntimeException {
private final ErrorCode errorCode;
public GeneralException() {
super(ErrorCode.INTERNAL_ERROR.getMessage());
this.errorCode = ErrorCode.INTERNAL_ERROR;
}
public GeneralException(String message) {
super(ErrorCode.INTERNAL_ERROR.getMessage(message));
this.errorCode = ErrorCode.INTERNAL_ERROR;
}
public GeneralException(String message, Throwable cause) {
super(ErrorCode.INTERNAL_ERROR.getMessage(message), cause);
this.errorCode = ErrorCode.INTERNAL_ERROR;
}
public GeneralException(Throwable cause) {
super(ErrorCode.INTERNAL_ERROR.getMessage(cause));
this.errorCode = ErrorCode.INTERNAL_ERROR;
}
public GeneralException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public GeneralException(ErrorCode errorCode, String message) {
super(errorCode.getMessage(message));
this.errorCode = errorCode;
}
public GeneralException(ErrorCode errorCode, String message, Throwable cause) {
super(errorCode.getMessage(message), cause);
this.errorCode = errorCode;
}
public GeneralException(ErrorCode errorCode, Throwable cause) {
super(errorCode.getMessage(cause), cause);
this.errorCode = errorCode;
}
}
RuntimeException 을 상속받아 사용하려는 의도에 맞게 재정의한 코드이다.
@Controller
public class BaseErrorController implements ErrorController {
@RequestMapping(path = "/error", produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletResponse response) {
HttpStatus httpStatus = HttpStatus.valueOf(response.getStatus());
ErrorCode errorCode = httpStatus.is4xxClientError() ? ErrorCode.BAD_REQUEST : ErrorCode.INTERNAL_ERROR;
if (httpStatus == HttpStatus.OK) {
httpStatus = HttpStatus.FORBIDDEN;
errorCode = ErrorCode.BAD_REQUEST;
}
return new ModelAndView(
"error",
Map.of(
"statusCode", httpStatus.value(),
"errorCode", errorCode,
"message", errorCode.getMessage(httpStatus.getReasonPhrase())
),
httpStatus
);
}
@RequestMapping("/error")
public ResponseEntity<ApiErrorResponse> error(HttpServletResponse response) {
HttpStatus httpStatus = HttpStatus.valueOf(response.getStatus());
ErrorCode errorCode = httpStatus.is4xxClientError() ? ErrorCode.BAD_REQUEST : ErrorCode.INTERNAL_ERROR;
if (httpStatus == HttpStatus.OK) {
httpStatus = HttpStatus.FORBIDDEN;
errorCode = ErrorCode.BAD_REQUEST;
}
return ResponseEntity
.status(httpStatus)
.body(ApiErrorResponse.of(false, errorCode));
}
}
스프링부트는 오류가 발생하면 server.error.path
에 설정된 경로에서 요청을 처리한다. 스프링부트에서는 기본적으로 BasicErrorController가 등록이 되어 해당 요청을 처리한다.
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
private final ErrorProperties errorProperties;
public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {
this(errorAttributes, errorProperties, Collections.emptyList());
}
public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties,
List<ErrorViewResolver> errorViewResolvers) {
super(errorAttributes, errorViewResolvers);
Assert.notNull(errorProperties, "ErrorProperties must not be null");
this.errorProperties = errorProperties;
}
@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);
}
}
Spring 환경 내에 server.error.path
혹은 error.path
로 등록된 property의 값을 넣거나, 없는 경우 /error
를 사용한다.
HTML로 응답을 주는 겅우 errorHTML
메소드가 실행되며
위에 이미지와 같은 HTML 문서로 페이지가 보이게 된다.(produces = MediaType.TEXT_HTML_VALUE로 HTML로 구분된다.)
HTML 외의 응답은 error 메소드가 실행되고 처리된다.
@Controller
public class BaseErrorController implements ErrorController {
@RequestMapping(value = "/error", produces = MediaType.TEXT_HTML_VALUE)
public String errorHtml(HttpServletResponse response,Model model){
HttpStatus status = HttpStatus.valueOf(response.getStatus());
ErrorCode errorCode = ErrorCode.valueOf(status);
model.addAllAttributes(
Map.of(
"statusCode", status,
"errorCode", errorCode,
"message", errorCode.getMessage()
)
);
return "error/error";
}
@RequestMapping("/error")
public ResponseEntity<ApiErrorResponse> error(HttpServletResponse response){
HttpStatus status = HttpStatus.valueOf(response.getStatus());
ErrorCode errorCode = ErrorCode.valueOf(status);
return ResponseEntity
.status(status)
.body(
ApiErrorResponse.of(false, errorCode)
);
}
}
@ExceptionHandler
는 @Controller
, @RestController
, @ControllerAdvice
, @RestControllerAdvice
가 적용된 Bean 내에서 발생하는 예외를 잡아서 메서드에서 처리해주는 기능이다. (service, repository 같은 Bean에서는 처리가 안된다.)
@Controller
public class BaseController {
@ExceptionHandler(Exception.class)
public ResponseEntity<?> exception(Exception e){
HttpStatus httpStatus = HttpStatus.NOT_FOUND;
return ResponseEntity.status(httpStatus).body(null);
}
}
@ExceptionHandler
에는 @ExceptionHandler(Exception.class)
에 잡으려는 예외를 지정할 수 있다. 만약 지정하지 않을 경우 파라미터에 설정된 에러 클래스로 간주된다. (파라미터 보다 @ExceptionHandler에 예외를 지정하는 것을 권장한다.) 또한 @ExceptionHandler
는 HttpServletRequest
나 WebRequest
와 같은 파라미터를 받을 수 있다. 반환 타입으로는 ResponseEntity
, String
, void
등 자유롭게 활용할 수 있다.
@ControllerAdvice
는 모든 @Controller
즉, 전역에서 발생할 수 있는 예외를 잡아 처리하는 어노테이션이다. 즉 @ControllerAdvice
가 붙인 하나의 클래스에 @ExceptionHandler
를 모아 처리할 수 있다. @RestControllerAdvice
는 @ControllerAdvice
와 기능은 동일하며 @RestController
어노테이션이 붙은 클래스에 예외를 처리한다.
@ControllerAdvice
public class BaseExceptionHandler {
@ExceptionHandler
public ModelAndView general(GeneralException e) {
ErrorCode errorCode = e.getErrorCode();
return new ModelAndView(
"error",
Map.of(
"statusCode", errorCode.getHttpStatus().value(),
"errorCode", errorCode,
"message", errorCode.getMessage()
),
errorCode.getHttpStatus()
);
}
@ExceptionHandler
public ModelAndView exception(Exception e, HttpServletResponse response) {
HttpStatus httpStatus = HttpStatus.valueOf(response.getStatus());
ErrorCode errorCode = httpStatus.is4xxClientError() ? ErrorCode.BAD_REQUEST : ErrorCode.INTERNAL_ERROR;
if (httpStatus == HttpStatus.OK) {
httpStatus = HttpStatus.FORBIDDEN;
errorCode = ErrorCode.BAD_REQUEST;
}
return new ModelAndView(
"error",
Map.of(
"statusCode", httpStatus.value(),
"errorCode", errorCode,
"message", errorCode.getMessage(e)
),
httpStatus
);
}
}
@RestControllerAdvice(annotations = {RestController.class, RepositoryRestController.class})
public class ApiExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler
public ResponseEntity<Object> validation(ConstraintViolationException e, WebRequest request) {
return handleExceptionInternal(e, ErrorCode.VALIDATION_ERROR, request);
}
@ExceptionHandler
public ResponseEntity<Object> general(GeneralException e, WebRequest request) {
return handleExceptionInternal(e, e.getErrorCode(), request);
}
@ExceptionHandler
public ResponseEntity<Object> exception(Exception e, WebRequest request) {
return handleExceptionInternal(e, ErrorCode.INTERNAL_ERROR, request);
}
@Override
protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
return handleExceptionInternal(ex, ErrorCode.valueOf(status), headers, status, request);
}
private ResponseEntity<Object> handleExceptionInternal(Exception e, ErrorCode errorCode, WebRequest request) {
return handleExceptionInternal(e, errorCode, HttpHeaders.EMPTY, errorCode.getHttpStatus(), request);
}
private ResponseEntity<Object> handleExceptionInternal(Exception e, ErrorCode errorCode, HttpHeaders headers, HttpStatus status, WebRequest request) {
return super.handleExceptionInternal(
e,
ApiErrorResponse.of(false, errorCode.getCode(), errorCode.getMessage(e)),
headers,
status,
request
);
}
}
@RestControllerAdvice
클래스에서는 ResponseEntityExceptionHandler
를 상속 받는다.
@ExceptionHandler({
HttpRequestMethodNotSupportedException.class,
HttpMediaTypeNotSupportedException.class,
HttpMediaTypeNotAcceptableException.class,
MissingPathVariableException.class,
MissingServletRequestParameterException.class,
ServletRequestBindingException.class,
ConversionNotSupportedException.class,
TypeMismatchException.class,
HttpMessageNotReadableException.class,
HttpMessageNotWritableException.class,
MethodArgumentNotValidException.class,
MissingServletRequestPartException.class,
BindException.class,
NoHandlerFoundException.class,
AsyncRequestTimeoutException.class
})
ResponseEntityExceptionHandler
클래스에는 위 코드와 같이 스프링에서 발생할 수 있는 예외를 정리해 놓았기 때문에 따로 설정하지 않고
ResponseEntityExceptionHandler
클래스를 상속받으면 사용가능하다.
protected ResponseEntity<Object> handleExceptionInternal(
Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
if (HttpStatus.INTERNAL_SERVER_ERROR.equals(status)) {
request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST);
}
return new ResponseEntity<>(body, headers, status);
}
ResponseEntityExceptionHandler
클래스를 상속받아 사용할 경우 handleExceptionInternal
메소드를 재정의해서 사용할 수 있다. (handleExceptionInternal 메소드에 body는 전부 null을 받는다. 만약 데이터를 넣고 싶다면 따로 정의해서 사용해야 한다.)
@Getter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public class ApiErrorResponse {
private final Boolean success;
private final Integer errorCode;
private final String message;
public static ApiErrorResponse of(Boolean success, Integer errorCode, String message) {
return new ApiErrorResponse(success, errorCode, message);
}
public static ApiErrorResponse of(Boolean success, ErrorCode errorCode) {
return new ApiErrorResponse(success, errorCode.getCode(), errorCode.getMessage());
}
public static ApiErrorResponse of(Boolean success, ErrorCode errorCode, Exception e) {
return new ApiErrorResponse(success, errorCode.getCode(), errorCode.getMessage(e));
}
public static ApiErrorResponse of(Boolean success, ErrorCode errorCode, String message) {
return new ApiErrorResponse(success, errorCode.getCode(), errorCode.getMessage(message));
}
}
API 에러 데이터를 받기 위한 코드이다.