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)
    • 모든 예상치 못한 문제.