Item59 - 예외는 진짜 예외 상황에만 사용하라

2024. 1. 11. 20:06Book/이펙티브 자바

예외를 잘못 사용한 예를 보자

public static void main(String[] args) {
    Mountain [] range = new Mountain[3];
    range[0] = new Mountain();
    range[1] = new Mountain();
    range[2] = new Mountain();

    try {
        int i = 0;
        while (true) {
            range[i++].climb();
        }
    } catch (ArrayIndexOutOfBoundsException e) {
        e.printStackTrace();
    }
}

=>
등산!
등산!
등산!
java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
	at effective.study.chapter10.Item59.WrongUsedError.main(WrongUsedError.java:14)

 

첫 번째로 코드가 직관적이지 않다.

이 코드는 배열의 원소를 순회하는데, 배열의 끝에 도달해 에러가 발생하면 끝을 낸다. 이 코들르 표준적인 관용구대로 작성하면 쉽게 이해할 수 있을 것이다.

 

public static void main(String[] args) {
    Mountain [] range = new Mountain[3];
    range[0] = new Mountain();
    range[1] = new Mountain();
    range[2] = new Mountain();

    for (Mountain m : range) {
        m.climb();
    }
}

=>
등산!
등산!
등산!

 

 

예외를 써서 루프를 종료한 이유는 잘못된 추론으로 성능을 높이려고 한 것이다.

JVM은 배열에 접근할 때마다 경계를 넘지 않는지 검사하는데, 일반적인 반복문도 배열 경계에 도달하면 종료한다. 그래서 검사가 중복되므로 하나를 생략한 것이다. 하지만 잘못된 생각이다.

 

1. 예외는 예외 상황에 쓸 용도로 설계되었으므로  JVM 구현자 입장에서는 명확한 검사만큼 빠르게 만들어야 할 동기가 약하다

 

 2. 코드를 try-catch 블록 안에 넣으면 JVM이 적용할 수 있는 최적화가 제한된다.

 

 3. 배열을 순회하는 표준 관용구는 앞서 걱정한 중복 검사를 수행하지 않는다.

 

사실은 예외를 사용한 쪽이 더 느리다.

예외를 사용한 반복문은 코드를 헷갈리게 하고 성능도 좋지 않다.

 

따라서 예외는 일상적인 제어 흐름용으로 쓰여선 안되고 예외 상황에만 사용되어야 한다.

 

이 원칙은 API 설계에도 적용된다. 잘 설계된 API라면 클라이언트가 정상적인 제어 흐름에서 예외를 사용할 일이 없게 해야 한다. 특정 상태에서만 호출할 수 있는 '상태 의존적 ' 메서드를 제공하는 클래스는 '상태 검사' 메서드도 함께 제공해야 한다. Iterator 인터페이스의 next와 hasNext가 각각 상태 의존적 메서드와 상태 검사 메서드이다.

Mountain [] range = new Mountain[3];
range[0] = new Mountain();
range[1] = new Mountain();
range[2] = new Mountain();

Collection<Mountain> collection = Arrays.stream(range).toList();

for (Iterator<Mountain> i = collection.iterator(); i.hasNext();) {
    Mountain m = i.next();
}

 

Iterator가 hasNext를 제공하지 않았다면 그 일을 클라이언트가 대신해야 한다.

Mountain [] range = new Mountain[3];
range[0] = new Mountain();
range[1] = new Mountain();
range[2] = new Mountain();

Collection<Mountain> collection = Arrays.stream(range).toList();

try {
    Iterator<Mountain> i = collection.iterator();
    while (true) {
        Mountain m = i.next();
        m.climb();
    }
} catch (NoSuchElementException e) {}

 

상태 검사 메서드 대신 사용할 수 있는 선택지도 있다. 올바르지 않은 상태일 때 빈 옵셔널 혹은 null 같은 특수한 값을 반환하는 방법이다.

 

상태 검사 메서드, 옵셔널, 특정 값 중 하나를 선택하는 지침을 몇 개 소개하겠다.

 

1. 외부 동기화 없이 여러 스레드가 동시에 접근할 수 있거나 외부 요인으로 상태가 변할 수 있다면 옵셔널이나 특정 값을 사용한다. (상태 검사 메서드와 상태 의존적 메서드 호출 사잉에 객체의 상태가 변할 수 있음)

 

2. 성능이 중요한 상황에서 상태 검사 메서드가 상태 의존적 메서드의 작업 일부를 중복 수행한다면 옵셔널이나 특정 값을 반환한다.

 

3. 다른 모든 경우엔 상태 검사 메서드 방식이 조금 더 낫다고 할 수 있다. 가독성이 좋고, 잘못 사용했을 대 발견하기 쉽다.