애플리케이션 전체 예외 처리 시스템 개선

뚜우웅이·2025년 4월 21일

캡스톤 디자인

목록 보기
16/35
  • 현재 애플리케이션에서는 다양한 종류의 표준 예외(RuntimeException, BadRequestException 등)를 직접 던지고 있어 예외 처리가 일관되지 않고, 프론트엔드에 유의미한 에러 메시지를 전달하기 어려운 상황이다.
  • 모든 예외를 커스텀 예외로 통합하여 일관된 예외 처리 체계를 구축하고자 한다.

Auth

Exception

public class AuthException extends BaseException {

    // 이메일 중복 예외
    public static class EmailDuplicateException extends AuthException {
        public EmailDuplicateException() {
            super("이미 사용 중인 이메일입니다.", HttpStatus.CONFLICT, "AUTH_EMAIL_DUPLICATE");
        }
    }

    // 인증 실패 예외
    public static class AuthenticationFailedException extends AuthException {
        public AuthenticationFailedException() {
            super("인증에 실패했습니다.", HttpStatus.UNAUTHORIZED, "AUTH_FAILED");
        }
    }

    // 리프레시 토큰 만료 예외
    public static class RefreshTokenExpiredException extends AuthException {
        public RefreshTokenExpiredException() {
            super("리프레시 토큰이 만료되었습니다.", HttpStatus.UNAUTHORIZED, "REFRESH_TOKEN_EXPIRED");
        }
    }

    // 리프레시 토큰 없음 예외
    public static class RefreshTokenNotFoundException extends AuthException {
        public RefreshTokenNotFoundException() {
            super("리프레시 토큰을 찾을 수 없습니다.", HttpStatus.NOT_FOUND, "REFRESH_TOKEN_NOT_FOUND");
        }
    }

    public static class PasswordResetTokenNotFoundException extends AuthException {
        public PasswordResetTokenNotFoundException() {
            super("비밀번호 재설정 토큰을 찾을 수 없습니다.", HttpStatus.NOT_FOUND, "PASSWORD_RESET_TOKEN_NOT_FOUND");
        }
    }

    public static class PasswordResetTokenExpiredException extends AuthException {
        public PasswordResetTokenExpiredException() {
            super("비밀번호 재설정 토큰이 만료되었습니다.", HttpStatus.BAD_REQUEST, "PASSWORD_RESET_TOKEN_EXPIRED");
        }
    }

    public static class InvalidRefreshTokenException extends AuthException {
        public InvalidRefreshTokenException() {
            super("유효하지 않은 토큰입니다.", HttpStatus.UNAUTHORIZED, "INVALID_REFRESH_TOKEN");
        }
    }

    public static class ExpiredRefreshTokenException extends AuthException {
        public ExpiredRefreshTokenException() {
            super("토큰이 만료되었습니다.", HttpStatus.UNAUTHORIZED, "EXPIRED_REFRESH_TOKEN");
        }
    }

    public AuthException(String message, HttpStatus status, String errorCode) {
        super(message, status, errorCode);
    }
}

Service 예시

// 토큰 검증
        PasswordResetToken resetToken = passwordResetTokenRepository.findByToken(token)
                .orElseThrow(() -> new AuthException.PasswordResetTokenNotFoundException());

Email

Exception

public class EmailVerificationException extends BaseException {

    public static class EmailNotVerifiedException extends EmailVerificationException {
        public EmailNotVerifiedException() {
            super("학교 이메일 인증이 필요합니다.", HttpStatus.FORBIDDEN, "EMAIL_NOT_VERIFIED");
        }
    }
    public static class VerificationNotFoundException extends EmailVerificationException {
        public VerificationNotFoundException(String email) {
            super("인증 정보를 찾을 수 없습니다: " + email, HttpStatus.NOT_FOUND, "VERIFICATION_NOT_FOUND");
        }
    }

    public static class InvalidEmailDomainException extends EmailVerificationException {
        public InvalidEmailDomainException(String email) {
            super("학교 이메일만 사용 가능합니다: " + email, HttpStatus.BAD_REQUEST, "INVALID_EMAIL_DOMAIN");
        }
    }

    public static class VerificationCodeExpiredException extends EmailVerificationException {
        public VerificationCodeExpiredException() {
            super("인증 코드가 만료되었습니다.", HttpStatus.BAD_REQUEST, "VERIFICATION_CODE_EXPIRED");
        }
    }

    public static class InvalidVerificationCodeException extends EmailVerificationException {
        public InvalidVerificationCodeException() {
            super("인증 코드가 일치하지 않습니다.", HttpStatus.BAD_REQUEST, "INVALID_VERIFICATION_CODE");
        }
    }

    public EmailVerificationException(String message, HttpStatus status, String errorCode) {
        super(message, status, errorCode);
    }
}

Service 예시

        if (!verification.getVerificationCode().equals(code)) {
            throw new EmailVerificationException.InvalidVerificationCodeException();
        }

File

Exception

public class FileException extends BaseException {

    public static class FileUploadException extends FileException {
        public FileUploadException(String fileName) {
            super("파일 업로드에 실패했습니다: " + fileName, HttpStatus.INTERNAL_SERVER_ERROR, "FILE_UPLOAD_FAILED");
        }

        public FileUploadException(String fileName, Throwable cause) {
            super("파일 업로드에 실패했습니다: " + fileName, HttpStatus.INTERNAL_SERVER_ERROR, "FILE_UPLOAD_FAILED");
        }
    }

    public static class ThumbnailCreationException extends FileException {
        public ThumbnailCreationException(String fileName) {
            super("썸네일 생성에 실패했습니다: " + fileName, HttpStatus.INTERNAL_SERVER_ERROR, "THUMBNAIL_CREATION_FAILED");
        }

        public ThumbnailCreationException(String fileName, Throwable cause) {
            super("썸네일 생성에 실패했습니다: " + fileName, HttpStatus.INTERNAL_SERVER_ERROR, "THUMBNAIL_CREATION_FAILED");
        }
    }

    public static class FileNotFoundException extends FileException {
        public FileNotFoundException(String fileName) {
            super("파일을 찾을 수 없습니다: " + fileName, HttpStatus.NOT_FOUND, "FILE_NOT_FOUND");
        }
    }

    public static class DirectoryCreationException extends FileException {
        public DirectoryCreationException(String path) {
            super("디렉토리 생성에 실패했습니다: " + path, HttpStatus.INTERNAL_SERVER_ERROR, "DIRECTORY_CREATION_FAILED");
        }

        public DirectoryCreationException(String path, Throwable cause) {
            super("디렉토리 생성에 실패했습니다: " + path, HttpStatus.INTERNAL_SERVER_ERROR, "DIRECTORY_CREATION_FAILED");
        }
    }

    public static class FileDeletionException extends FileException {
        public FileDeletionException(String path) {
            super("파일 삭제에 실패했습니다: " + path, HttpStatus.INTERNAL_SERVER_ERROR, "FILE_DELETION_FAILED");
        }

        public FileDeletionException(String path, Throwable cause) {
            super("파일 삭제에 실패했습니다: " + path, HttpStatus.INTERNAL_SERVER_ERROR, "FILE_DELETION_FAILED");
        }
    }

    public static class EmptyFileException extends FileException {
        public EmptyFileException() {
            super("업로드할 파일이 없습니다.", HttpStatus.BAD_REQUEST, "EMPTY_FILE");
        }
    }

    public FileException(String message, HttpStatus status, String errorCode) {
        super(message, status, errorCode);
    }
}

Service 예시

    private void deleteFileIfExists(Path filePath) {
        try {
            if (Files.exists(filePath)) Files.delete(filePath);
        } catch (IOException e) {
            log.error("파일 삭제 실패: {}", filePath, e);
            throw new FileException.FileDeletionException(filePath.toString(), e);
        }
    }

OAuth2

Exception

public class OAuth2Exception extends BaseException {

    public static class UnsupportedProviderException extends OAuth2Exception {
        public UnsupportedProviderException(String provider) {
            super("지원하지 않는 소셜 로그인입니다: " + provider, HttpStatus.BAD_REQUEST, "UNSUPPORTED_OAUTH_PROVIDER");
        }
    }

    public static class OAuth2AttributeParsingException extends OAuth2Exception {
        public OAuth2AttributeParsingException(String provider) {
            super(provider + " 사용자 정보를 파싱하는데 실패했습니다,", HttpStatus.INTERNAL_SERVER_ERROR, "OAUTH_ATTRIBUTE_PARSING_FAILED");
        }
    }

    public static class OAuth2ProcessingException extends OAuth2Exception {
        public OAuth2ProcessingException(String message) {
            super(message, HttpStatus.INTERNAL_SERVER_ERROR, "OAUTH_PROCESSING_FAILED");
        }
    }

    public static class EmailAlreadyExistsException extends OAuth2Exception {
        public EmailAlreadyExistsException(String email) {
            super("이미 가입된 이메일입니다: " + email, HttpStatus.CONFLICT, "EMAIL_ALREADY_EXISTS");
        }
    }

    public static class MissingAttributeException extends OAuth2Exception {
        public MissingAttributeException(String provider, String attribute) {
            super(provider + " 응답에서 필수 속성(" + attribute + ")을 찾을 수 없습니다.", HttpStatus.BAD_REQUEST, "MISSING_OAUTH_ATTRIBUTE");
        }
    }

    public static class InvalidResponseException extends OAuth2Exception {
        public InvalidResponseException(String provider) {
            super(provider + " 응답 형식이 올바르지 않습니다.", HttpStatus.BAD_REQUEST, "INVALID_OAUTH_RESPONSE");
        }
    }

    public OAuth2Exception(String message, HttpStatus status, String errorCode) {
        super(message, status, errorCode);
    }
}

Service 예시

        if (responseData == null || !responseData.containsKey("id")) {
            log.error("네이버 응답에서 사용자 정보를 찾을 수 없습니다. attributes: {}", attributes);
            throw new OAuth2Exception.MissingAttributeException("naver", "id");
        }

Product

public class ProductException extends BaseException {

    public static class ProductNotFoundException extends ProductException {
        public ProductNotFoundException(Long productId) {
            super("해당 ID의 상품을 찾을 수 없습니다: " + productId, HttpStatus.NOT_FOUND, "PRODUCT_NOT_FOUND");
        }
    }

    public static class SellerMismatchException extends ProductException {
        public SellerMismatchException() {
            super("해당 상품의 판매자가 아닙니다.", HttpStatus.FORBIDDEN, "SELLER_MISMATCH");
        }
    }

    public static class ProductCreationException extends ProductException {
        public ProductCreationException(String message) {
            super("상품 등록 중 오류가 발생했습니다: " + message, HttpStatus.INTERNAL_SERVER_ERROR, "PRODUCT_CREATION_FAILED");
        }
    }

    public static class ProductUpdateException extends ProductException {
        public ProductUpdateException(String message) {
            super("상품 수정 중 오류가 발생했습니다: " + message, HttpStatus.INTERNAL_SERVER_ERROR, "PRODUCT_UPDATE_FAILED");
        }
    }
    public static class AlreadySoldProductException extends ProductException {
        public AlreadySoldProductException() {
            super("이미 판매 완료된 상품입니다.", HttpStatus.BAD_REQUEST, "ALREADY_SOLD_PRODUCT");
        }
    }

    public static class NotSoldProductException extends ProductException {
        public NotSoldProductException() {
            super("판매 완료 상태가 아닌 상품은 취소할 수 없습니다.", HttpStatus.BAD_REQUEST, "NOT_SOLD_PRODUCT");
        }
    }

    public static class ProductNotAvailableException extends ProductException {
        public ProductNotAvailableException() {
            super("해당 상품은 구매할 수 없는 상태입니다.", HttpStatus.BAD_REQUEST, "PRODUCT_NOT_AVAILABLE");
        }
    }

    public static class ProductAccessDeniedException extends ProductException {
        public ProductAccessDeniedException() {
            super("해당 상품에 대한 권한이 없습니다.", HttpStatus.FORBIDDEN, "PRODUCT_ACCESS_DENIED");
        }
    }

    public static class ProductStockException extends ProductException {
        public ProductStockException() {
            super("상품 재고가 부족합니다.", HttpStatus.BAD_REQUEST, "PRODUCT_STOCK_INSUFFICIENT");
        }
    }

    public ProductException(String message, HttpStatus status, String errorCode) {
        super(message, status, errorCode);
    }
}

Service 예시

    private static void emailVerification(User seller) {
        if (!seller.isEmailVerified()) {
            throw new EmailVerificationException.EmailNotVerifiedException();
        }
    }

예시 이미지


ProductException

  • 상품 조회, 등록, 수정, 삭제 과정의 예외 처리
  • 판매자 권한 검증, 상품 상태 변경 관련 예외 포함

FileException

  • 파일 업로드, 다운로드, 삭제 과정의 예외 처리
  • 디렉토리 생성, 썸네일 생성 관련 예외 포함

OAuth2Exception

  • 소셜 로그인 과정에서 발생하는 예외 처리
  • 제공자별 속성 파싱, 이메일 중복 관련 예외 포함

EmailVerificationException

  • 이메일 인증 과정의 예외 처리
  • 인증 코드 검증, 만료 관련 예외 포함

AuthException

  • 인증, 권한 관련 예외 처리
  • 토큰 검증, 만료 관련 예외 포함
profile
공부하는 초보 개발자

0개의 댓글