공통으로 사용할 ErrorCode 및 ErrorResponse 작성하기
@Getter
public enum ErrorCode {
// Client
BAD_REQUEST(HttpStatus.BAD_REQUEST.value(), "G001", "잘못된 요청 입니다."),
INVALID_INPUT_HEADER(HttpStatus.BAD_REQUEST.value(),"G002", " 값은 필수 입니다."),
FORBIDDEN_ERROR(HttpStatus.FORBIDDEN.value(), "G003","권한이 없습니다."),
NO_HANDLER_FOUND(HttpStatus.NOT_FOUND.value(),"G004", "존재하지 않는 API"),
NULL_POINT_ERROR(HttpStatus.NOT_FOUND.value(),"G005", "Null Point Exception"),
NO_MATCHES_FOUND(HttpStatus.NOT_FOUND.value(),"G006", "데이터가 존재하지 않습니다.");
private final int value;
private final String status;
private final String message;
ErrorCode(int value, String status, String message) {
this.value = value;
this.status = status;
this.message = message;
}
}
ErrorResponse 빌더를 ErrorCode로만 받다가 에러 핸들러를 작성하면서 예외처리시점에 별도의 메세지를 주고 싶어 status, error, message를 받는 형식으로 변경
@Data
public class ErrorResponse {
@Schema(description = "에러 발생 시간")
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private final LocalDateTime dateTime = LocalDateTime.now();
@Schema(description = "HTTP 상태 코드")
private final int status;
@Schema(description = "에러 코드")
private final String error;
@Schema(description = "에러 메세지")
private final String message;
@Builder
public ErrorResponse(int status, String error, String message ) {
this.status = status;
this.error = error;
this.message = message;
}
}
BusinessExceptionHandelr
비즈니스 로직중 에러가 나왔을 경우 사용할 RuntimeException을 상속 받은 클래스이다.
public class BusinessExceptionHandler extends RuntimeException {
@Getter
private final ErrorCode errorCode;
@Builder
public BusinessExceptionHandler(String message, ErrorCode errorCode) {
super(message);
this.errorCode = errorCode;
}
@Builder
public BusinessExceptionHandler(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
전역 예외 처리는 @RestContorllerAdvice를 사용하여 진행하였다.
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* [Exception] API 호출 시 '객체' 혹은 '파라미터' 데이터 값이 유효하지 않은 경우
*
* @param ex MethodArgumentNotValidException
* @return ResponseEntity<responseBody, HttpStatus>
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
protected ResponseEntity<ErrorResponse> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
log.error("MethodArgumentNotValidException", ex);
ErrorCode errorCode = ErrorCode.BAD_REQUEST;
String message = ex.getBindingResult().getFieldErrors()
.stream()
.map(x -> x.getDefaultMessage())
.collect(Collectors.toList())
.get(0);
final ErrorResponse response = getErrorResponse (errorCode,message);
return new ResponseEntity<>(response, HttpStatus.OK);
}
/**
* [Exception] API 호출 시 'Header' 내에 누락된 필드가 존재할 경우
*
* @param ex MissingRequestHeaderException
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(MissingRequestHeaderException.class)
protected ResponseEntity<ErrorResponse> handleMissingRequestHeaderException(MissingRequestHeaderException ex) {
log.error("MissingRequestHeaderException", ex);
String headerName = ex.getHeaderName();
ErrorCode errorCode = ErrorCode.INVALID_INPUT_HEADER;
String message = headerName + errorCode.getMessage();
final ErrorResponse response = getErrorResponse (errorCode,message);
return new ResponseEntity<>(response, HttpStatus.OK);
}
/**
* [Exception] API 호출 시 'Header' 내에 유효하지 않은 필드가 있을 경우
*
* @param ex MissingRequestHeaderException
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(ConstraintViolationException.class)
protected ResponseEntity<ErrorResponse> handleConstraintViolationException(ConstraintViolationException ex) {
log.error("ConstraintViolationException", ex);
ErrorCode errorCode = ErrorCode.INVALID_INPUT_HEADER;
//propertyPath 값 가져오기
String property = ex.getConstraintViolations()
.stream()
.map(x -> x.getPropertyPath().toString()) // 수정된 부분
.findFirst() // 첫 번째 프로퍼티 경로만 사용
.orElse("");
String message = ex.getMessage();
if (property.equals("addPost.userId")) {
message = "X-UERID는 3자에서 10자 사이여야 합니다.";
}
final ErrorResponse response = getErrorResponse (errorCode,message);
return new ResponseEntity<>(response, HttpStatus.OK);
}
/**
* [Exception] API 호출 시 누락된 값이 있을 경우
*
* @param ex IllegalArgumentException
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException ex) {
log.error("IllegalArgumentException", ex);
String message = ex.getMessage();
ErrorCode errorCode = ErrorCode.NULL_POINT_ERROR;
final ErrorResponse response = getErrorResponse (errorCode,message);
return new ResponseEntity<>(response, HttpStatus.OK);
}
/**
* [Exception] API 호출 시 누락된 값이 있을 경우
*
* @param ex IllegalArgumentException
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(NoResultException.class)
public ResponseEntity<ErrorResponse> handleNoResultException(NoResultException ex) {
log.error("NoResultException", ex);
ErrorCode errorCode = ErrorCode.NO_MATCHES_FOUND;
String message = errorCode.getMessage();
final ErrorResponse response = getErrorResponse (errorCode,message);
return new ResponseEntity<>(response, HttpStatus.OK);
}
/**
* [Exception] 비즈니스로직에서 에서 발생한 에러
*
* @param ex BusinessExceptionHandler
* @return ResponseEntity<ErrorResponse>
*/
@ExceptionHandler(BusinessExceptionHandler.class)
public ResponseEntity<ErrorResponse> handleCustomException(BusinessExceptionHandler ex) {
log.error("BusinessExceptionHandler", ex);
ErrorCode errorCode = ex.getErrorCode();
String message = ex.getMessage();
if ("".equals(message)) {
message = errorCode.getMessage();
}
final ErrorResponse response = getErrorResponse (errorCode,message);
return new ResponseEntity<>(response, HttpStatus.OK);
}
/**
* ErrorResponse 로 반환해 주는 메서드
*
* @param errorCode ErrorCode
* @param message String
*
* @return ErrorResponse
*/
private ErrorResponse getErrorResponse(ErrorCode errorCode, String message) {
return ErrorResponse.builder()
.status(errorCode.getValue())
.error(errorCode.getStatus())
.message(message)
.build();
}
}
@RestControllerAdvice
@controllerAdvice가 반환 값을 View로 준다면 @RestControllerAdvice는
RESTful API를 처리하는 컨트롤러에 사용되며, 메소드가 반환하는 데이터는 주로 JSON 또는XML형식
@ExceptionHandler
특정 예외가 발생했을 때 해당 예외를 처리하는 메서드를 지정하는데 사용된다.
핸들러에서 처리하는 예외의 경우 http 상태값을 200으로 리턴하고, 바디 부분에 실패에 대한 상세 내용으 리턴했다.(이 부분에 대해선 찾아보니 200 으로 리턴하는건 잘못된 방식이라고 한다. 비즈니스로직의 경우를 제외하곤 BadRequest 코드 값으로 리턴하게 변경하였다.)
실제 응답 값은 아래와 같다