72. 표준 예외를 사용하라

📝 아이템 72: 표준 예외를 사용하라

🔹 핵심 요약

✅ 자바 라이브러리는 대부분의 API에서 쓰기에 충분한 수의 예외를 제공함 ✅ 표준 예외를 재사용하면 API가 더 배우기 쉽고 사용하기 편리해짐 ✅ 예외 클래스 수가 적을수록 메모리 사용량도 줄고 클래스 적재 시간도 적게 걸림 ✅ 가장 흔히 재사용되는 예외로는 IllegalArgumentException, IllegalStateException, NullPointerException 등이 있음


📚 필수 개념 정리

🧩 예외(Exception)란?

예외(Exception)는 프로그램 실행 중에 발생할 수 있는 문제를 나타내는 객체로, 정상적인 흐름을 방해하고 이를 처리할 수 있도록 해주는 메커니즘이다. 예외를 통해 오류를 감지하고 처리하여, 프로그램이 갑작스럽게 종료되지 않도록 제어할 수 있다.

이 코드의 결과는?

public class ExceptionExample {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3));
        Iterator<Integer> iterator = numbers.iterator();

        while (iterator.hasNext()) {
            Integer number = iterator.next();
            if (number == 2) {
                numbers.remove(number); // ❌ 반복 중 컬렉션 수정
            }
        }
    }
}

→ 결과는 ConcurrentModificationException이 발생

👉 왜 이런 결과가 나올까?

🔄 동시 수정 문제

  • iterator로 컬렉션을 순회하면서,

  • 동시에 List.remove() 같은 방법으로 컬렉션을 직접 수정했기 때문

    • iterator는 순회 중에 컬렉션의 구조가 변경되면, 이를 동시 수정(Concurrent Modification) 으로 감지하고 예외를 던짐

⚙️ fail-fast 동작

  • 자바의 Iterator는 컬렉션이 변경되면 빠르게 실패하도록 설계

  • 내부적으로 modCount가 바뀌면, expectedModCount와 비교하여 ConcurrentModificationException 을 발생시킴

🔍 modCount란?

  • 컬렉션이 구조적으로 변경될 때마다 증가하는 값

  • Iterator는 자신이 생성될 때 modCount 값을 복사해 두고, 순회 중에 변경 여부를 감지함


올바른 방법은?

Iterator를 사용해서 순회 중에 요소를 제거하려면 iterator.remove() 를 사용해야 함 이 방법은 컬렉션의 변경을 관리할 수 있도록 설계되어 있어, ConcurrentModificationException을 피할 수 있음

while (iterator.hasNext()) {
    Integer number = iterator.next();
    if (number == 2) {
        iterator.remove(); // ✅ 안전하게 요소 제거
    }
}

💡 표준 예외 선택 기준

표준 예외를 선택할 때 아래 항목들을 고려하자:

  • 예외 이름이 가리키는 조건에 부합하는가?

  • API 문서에 명시된 예외의 용도에 부합하는가?

  • 필요한 정보를 충분히 제공할 수 있는가?

  • 더 구체적인 예외가 존재하지 않는가?


🚨 잘못된 예외 사용 예시

과도한 커스텀 예외 사용

/**
 * 커스텀 예외를 과도하게 만드는 안티 패턴
 * 표준 예외로 충분히 표현 가능한 경우들
 */
public class TooManyExceptions {
    // 불필요한 커스텀 예외 클래스
    class InvalidAgeException extends RuntimeException {
        public InvalidAgeException(String message) {
            super(message);
        }
    }

    // 불필요한 커스텀 예외 클래스
    class EmptyNameException extends RuntimeException {
        public EmptyNameException(String message) {
            super(message);
        }
    }

    public void setAge(int age) {
        if (age < 0 || age > 150) {
            // IllegalArgumentException으로 충분한 경우
            throw new InvalidAgeException("나이는 0-150 사이여야 합니다: " + age);
        }
    }

    public void setName(String name) {
        if (name == null || name.isEmpty()) {
            // IllegalArgumentException이나 NullPointerException으로 충분한 경우
            throw new EmptyNameException("이름은 비어있을 수 없습니다");
        }
    }
}

표준 예외를 사용한 개선된 버전

/**
 * 표준 예외를 적절히 활용한 개선된 버전
 * 코드가 간결해지고 API 사용자에게 익숙한 예외를 제공
 */
public class StandardExceptions {
    public void setAge(int age) {
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("나이는 0-150 사이여야 합니다: " + age);
        }
    }

    public void setName(String name) {
        if (name == null) {
            throw new NullPointerException("이름은 null일 수 없습니다");
        }
        if (name.isEmpty()) {
            throw new IllegalArgumentException("이름은 비어있을 수 없습니다");
        }
    }
}

⚠️ throw new NullPointerException() 대신 Objects.requireNonNull() 사용하기

// 직접 NullPointerException을 던지는 방식
if (name == null) {
    throw new NullPointerException("이름은 null일 수 없습니다");
}

// 더 간결하고 표준적인 방식
String name = Objects.requireNonNull(name, "이름은 null일 수 없습니다");

💼 IllegalArgumentException vs IllegalStateException

🔍 둘 중 어떤 것을 써야 할까?

  • IllegalArgumentException: 메서드에 잘못된 인수를 전달했을 때

  • IllegalStateException: 메서드 호출 당시 객체의 상태가 적절하지 않을 때

🧠 구분 기준

  • 호출자가 제공한 인수 값이 잘못된 것인가? → IllegalArgumentException

  • 메서드 호출 시점이 잘못된 것인가? (객체 상태) → IllegalStateException

▶️ 원래도 인수와 상관없이 실패했을 것인가?

  • 그렇다면 → IllegalStateException

  • 아니라면 → IllegalArgumentException


🧐 표준 예외 사용 시 주의사항

⚠️ 예외 사용 시 고려 사항

  • 예외 메시지는 상세하게: 예외가 발생한 상황을 자세히 설명해야 향후 디버깅이 용이함

  • 예외 연쇄 활용하기: 저수준 예외가 고수준 예외의 원인이라면, 예외 연쇄(exception chaining)를 사용

  • 적절한 예외 계층 선택하기: 과도하게 상세한 예외보다 더 일반적인 예외가 적합할 수 있음

/**
 * 예외 연쇄를 활용한 예시
 */
public class ExceptionChainingExample {
    public void processFile(String path) {
        try {
            // 파일을 읽고 처리하는 코드
            FileInputStream fis = new FileInputStream(path);
            // ...생략...
        } catch (IOException e) {
            // 저수준 예외를 원인으로 포함시켜 고수준 예외 발생
            throw new IllegalArgumentException("파일을 처리할 수 없습니다: " + path, e);
        }
    }
}

🚫 커스텀 예외를 만들어야 하는 경우

  • 표준 예외로 표현할 수 없는 추상화 계층에 맞는 예외가 필요할 때

  • 특별한 복구 조치나 액세스 방법이 필요한 경우

  • 모듈에 특화된 예외가 필요할 때

/**
 * 커스텀 예외가 필요한 상황 예시
 * - 특정 비즈니스 도메인에 관련된 예외
 * - 복구 전략이 다른 예외
 */
public class BankAccountException extends RuntimeException {
    private final String accountId;
    private final TransactionType transactionType;

    public BankAccountException(String message, String accountId,
                              TransactionType type, Throwable cause) {
        super(message, cause);
        this.accountId = accountId;
        this.transactionType = type;
    }

    // 복구를 위한 정보 제공
    public String getAccountId() {
        return accountId;
    }

    public TransactionType getTransactionType() {
        return transactionType;
    }

    // 예외 종류
    public enum TransactionType {
        DEPOSIT, WITHDRAWAL, TRANSFER
    }
}

🎯 언제 어떤 예외를 사용할까?

🧠 표준 예외 선택 가이드

상황
권장 예외
이유

메서드에 부적절한 인수 전달

IllegalArgumentException

인수 값 자체의 문제

인수로 null이 전달됨

NullPointerException

null 특화 예외, 의도 명확

인덱스가 범위를 벗어남

IndexOutOfBoundsException

인덱스 관련 특화 예외

객체의 상태가 메서드 수행에 부적절

IllegalStateException

객체 상태의 문제, 호출 시점이 잘못됨

반복자가 더 이상 원소 없음

NoSuchElementException

특정 상황에 대한 명확한 예외

지원하지 않는 메서드 호출

UnsupportedOperationException

선택적 메서드나 기능을 지원하지 않을 때

동시 수정 감지

ConcurrentModificationException

허용되지 않는 동시 수정

발생할 수 없는 상황 발생

AssertionError

프로그램 로직의 오류 (버그가 있다는 신호)

✅ 예외 처리 체크리스트


🎯 결론

📍 자바 라이브러리는 대부분의 상황에서 활용할 수 있는 다양한 표준 예외를 제공한다

📍 표준 예외를 사용하면 API의 일관성, 학습 곡선, 유지보수성이 좋아진다

📍 예외 계층을 과도하게 만들기보다 적절한 표준 예외를 선택하는 것이 중요하다

📍 예외 메시지와 예외 연쇄는 디버깅과 원인 추적에 큰 도움이 된다


💭 느낀 점

💡 예외 처리가 단순한 에러 케이스 처리가 아닌 API 설계의 중요한 부분임을 이해하게 됨

💡 이름과 상황이 잘 매칭되는 적절한 예외를 선택하는 것이 코드의 가독성과 일관성에 큰 영향을 미친다는 것을 알게 됨

💡 앞으로 커스텀 예외는 정말 필요할 때만 만들고, 표준 예외를 적극적으로 활용해야겠다고 생각함

Last updated