Spring
[Spring] Global Exception Handler (Controller Advice)
sabeom
2024. 12. 4. 09:37
서론
에러메시지 일관성 유지
나중에 추가 작성하겠음.. 지금은 개발기한 및 감리로 인하여..
코드 구현
에러 핸들러
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* Handle all uncaught exceptions.
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
log.error("Unhandled exception occurred: {}", ex.getMessage(), ex);
ErrorResponse response = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"An unexpected error occurred.",
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
/**
* Handle custom not found exceptions.
*/
@ExceptionHandler(BizExceptionMessage.class)
public ResponseEntity<ErrorResponse> handleBizExceptionMessage(BizExceptionMessage ex) {
log.warn("Resource not found: {}", ex.getMessage());
ErrorResponse response = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
}
/**
* Handle validation errors from @Valid annotations.
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ValidationErrorResponse> handleValidationExceptions(MethodArgumentNotValidException ex) {
List<Violation> violations = ex.getBindingResult().getFieldErrors()
.stream()
.map(this::toViolation)
.collect(Collectors.toList());
ValidationErrorResponse response = new ValidationErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"Validation failed",
LocalDateTime.now(),
violations
);
log.debug("Validation errors: {}", violations);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
/**
* Handle validation errors from @Validated annotations or ConstraintViolation.
*/
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ValidationErrorResponse> handleConstraintViolation(ConstraintViolationException ex) {
List<Violation> violations = ex.getConstraintViolations()
.stream()
.map(cv -> new Violation(
cv.getPropertyPath().toString(),
cv.getInvalidValue(),
cv.getMessage()
))
.collect(Collectors.toList());
ValidationErrorResponse response = new ValidationErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"Validation failed",
LocalDateTime.now(),
violations
);
log.debug("Constraint violations: {}", violations);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
private Violation toViolation(FieldError fieldError) {
return new Violation(fieldError.getField(), fieldError.getRejectedValue(), fieldError.getDefaultMessage());
}
/**
* Response structure for general errors.
*/
public static class ErrorResponse {
private final int status;
private final String message;
private final LocalDateTime timestamp;
public ErrorResponse(int status, String message, LocalDateTime timestamp) {
this.status = status;
this.message = message;
this.timestamp = timestamp;
}
// Getters
public int getStatus() { return status; }
public String getMessage() { return message; }
public LocalDateTime getTimestamp() { return timestamp; }
}
/**
* Response structure for validation errors.
*/
public static class ValidationErrorResponse extends ErrorResponse {
private final List<Violation> violations;
public ValidationErrorResponse(int status, String message, LocalDateTime timestamp, List<Violation> violations) {
super(status, message, timestamp);
this.violations = violations;
}
// Getter
public List<Violation> getViolations() { return violations; }
}
/**
* Represents a single validation error.
*/
public static class Violation {
private final String fieldName;
private final Object rejectedValue;
private final String message;
public Violation(String fieldName, Object rejectedValue, String message) {
this.fieldName = fieldName;
this.rejectedValue = rejectedValue;
this.message = message;
}
// Getters
public String getFieldName() { return fieldName; }
public Object getRejectedValue() { return rejectedValue; }
public String getMessage() { return message; }
}
}
- 예외 유형별 처리:
- Exception: 예기치 못한 일반 오류 처리.
- CustomNotFoundException: 사용자 정의 예외 처리.
- MethodArgumentNotValidException: @Valid 유효성 검증 실패 처리.
- ConstraintViolationException: 유효성 검증 실패 (주로 @Validated에서 발생) 처리.
- 응답 구조:
- ErrorResponse: 일반 오류 응답 구조.
- ValidationErrorResponse: 검증 오류 응답 구조, 필드별 오류 세부 정보 포함.
- 로깅:
- 각 예외 처리 블록에서 적절한 로깅(log.error, log.warn, log.debug).
- 재사용 가능한 구조:
- Violation 클래스와 ValidationErrorResponse를 사용해 일관된 검증 오류 응답 제공.
- 에러 메시지 노출 최소화:
- 사용자에게 불필요한 시스템 정보를 숨기고 명확한 메시지를 제공.
커스텀 에러 메시지
public class BizExceptionMessage extends RuntimeException {
private int code;
private ErrorType errorType;
private String msgTypCod; //Q : Question, C : Critical, I : Information, E : Exclamation , B : system
private String msgCaption;
private Locale msgLocale;
public BizExceptionMessage(ErrorType errorType) {
super(errorType.getMessage());
this.errorType = errorType;
this.code = errorType.getCode();
this.msgTypCod = "";
this.msgCaption = "";
this.msgLocale = Locale.KOREA;
}
public BizExceptionMessage(ErrorType errorType, Locale localeInfo) {
super(errorType.getMessage());
this.errorType = errorType;
this.code = errorType.getCode();
this.msgTypCod = "";
this.msgCaption = "";
this.msgLocale = localeInfo;
}
public BizExceptionMessage(ErrorType errorType, String subMessage) {
super(errorType.getMessage() + " (" + subMessage + ")");
this.errorType = errorType;
this.code = errorType.getCode();
this.msgTypCod = "";
this.msgCaption = "";
this.msgLocale = Locale.KOREA;
}
public BizExceptionMessage(String msgTypCod, ErrorType errorType, String subMessage) {
super(errorType.getMessage() + " (" + subMessage + ")");
this.errorType = errorType;
this.code = errorType.getCode();
this.msgTypCod = msgTypCod;
this.msgCaption = "";
this.msgLocale = Locale.KOREA;
}
public BizExceptionMessage(String msgTypCod, ErrorType errorType, String subMessage, String msgCaption) {
super(errorType.getMessage() + " (" + subMessage + ")");
this.errorType = errorType;
this.code = errorType.getCode();
this.msgTypCod = msgTypCod;
this.msgCaption = msgCaption;
this.msgLocale = Locale.KOREA;
}
public int getCode() {
return code;
}
public String getMsgTypCod() {
return msgTypCod;
}
public String getMsgCaption() {
return msgCaption;
}
public ErrorType getErrorType() {
return this.errorType;
}
public Locale getMsgLocale() {
return msgLocale;
}
}
에러타입 정의
public enum ErrorType {
// 성공
SUCCESS(0, "Success", "Operation completed successfully", "SUCCESS"),
// 인증 및 권한 오류 (10000 ~ 10999)
INVALID_CREDENTIALS(10001, "Invalid credentials", "The provided username or password is incorrect", "INVALID_CREDENTIALS"),
UNAUTHORIZED_ACCESS(10002, "Unauthorized access", "You do not have the required permissions to access this resource", "UNAUTHORIZED_ACCESS"),
TOKEN_EXPIRED(10003, "Token expired", "The authentication token has expired", "TOKEN_EXPIRED"),
TOKEN_INVALID(10004, "Invalid token", "The provided token is invalid", "TOKEN_INVALID"),
ACCOUNT_LOCKED(10005, "Account locked", "This account is locked due to too many failed login attempts", "ACCOUNT_LOCKED"),
// 입력 값 오류 (20000 ~ 20999)
INVALID_INPUT(20001, "Invalid input", "One or more input values are invalid", "INVALID_INPUT"),
MISSING_REQUIRED_FIELD(20002, "Missing required field", "A required field is missing in the request", "MISSING_REQUIRED_FIELD"),
INVALID_FORMAT(20003, "Invalid format", "The input format is incorrect", "INVALID_FORMAT"),
DUPLICATE_ENTRY(20004, "Duplicate entry", "The provided value already exists", "DUPLICATE_ENTRY"),
// 데이터베이스 오류 (30000 ~ 30999)
DATABASE_ERROR(30001, "Database error", "An error occurred while interacting with the database", "DATABASE_ERROR"),
DATA_NOT_FOUND(30002, "Data not found", "The requested data could not be found in the database", "DATA_NOT_FOUND"),
CONSTRAINT_VIOLATION(30003, "Constraint violation", "A database constraint was violated", "CONSTRAINT_VIOLATION"),
QUERY_TIMEOUT(30004, "Query timeout", "The database query took too long to execute", "QUERY_TIMEOUT"),
// 파일 관련 오류 (40000 ~ 40999)
FILE_NOT_FOUND(40001, "File not found", "The requested file could not be located", "FILE_NOT_FOUND"),
FILE_UPLOAD_ERROR(40002, "File upload error", "An error occurred while uploading the file", "FILE_UPLOAD_ERROR"),
FILE_SIZE_EXCEEDED(40003, "File size exceeded", "The uploaded file exceeds the maximum allowed size", "FILE_SIZE_EXCEEDED"),
UNSUPPORTED_FILE_FORMAT(40004, "Unsupported file format", "The uploaded file format is not supported", "UNSUPPORTED_FILE_FORMAT"),
// 네트워크 및 통신 오류 (50000 ~ 50999)
NETWORK_ERROR(50001, "Network error", "An error occurred while communicating with the server", "NETWORK_ERROR"),
SERVICE_UNAVAILABLE(50002, "Service unavailable", "The requested service is temporarily unavailable", "SERVICE_UNAVAILABLE"),
TIMEOUT_ERROR(50003, "Timeout error", "The request timed out while waiting for a response", "TIMEOUT_ERROR"),
// 서버 내부 오류 (60000 ~ 60999)
INTERNAL_SERVER_ERROR(60001, "Internal server error", "An unexpected error occurred on the server", "INTERNAL_SERVER_ERROR"),
NULL_POINTER_EXCEPTION(60002, "Null pointer exception", "A null pointer exception occurred", "NULL_POINTER_EXCEPTION"),
ILLEGAL_STATE(60003, "Illegal state", "The application encountered an illegal state", "ILLEGAL_STATE"),
CONFIGURATION_ERROR(60004, "Configuration error", "A configuration issue was detected", "CONFIGURATION_ERROR"),
// 기본 및 알 수 없는 오류 (90000 ~ 90999)
UNKNOWN_ERROR(90001, "Unknown error", "An unknown error occurred", "UNKNOWN_ERROR");
private final int code;
private final String message;
private final String detailMessage;
private final String messageKey;
ErrorType(int code, String message, String detailMessage, String messageKey) {
this.code = code;
this.message = message;
this.detailMessage = detailMessage;
this.messageKey = messageKey;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
public String getDetailMessage() {
return detailMessage;
}
public String getMessageKey() {
return messageKey;
}
}
- 인증 및 권한 오류 (10000 ~ 10999)
- 로그인 실패, 토큰 문제, 접근 권한 부족 등.
- 입력 값 오류 (20000 ~ 20999)
- 입력 데이터 누락, 형식 문제, 중복 데이터 등.
- 데이터베이스 오류 (30000 ~ 30999)
- SQL 에러, 데이터 미존재, 제약 조건 위반 등.
- 파일 관련 오류 (40000 ~ 40999)
- 파일 업로드, 형식, 크기 제한 등.
- 네트워크 및 통신 오류 (50000 ~ 50999)
- 네트워크 연결 문제, 서비스 비가용성 등.
- 서버 내부 오류 (60000 ~ 60999)
- 서버의 예기치 못한 문제, 설정 오류 등.
- 알 수 없는 오류 (90000 ~ 90999)
- 모든 예상치 못한 문제.