69] 예외는 진짜 예외 상황에만 사용하라

// 예외를 완전히 잘못 사용한 예 - 따라 하지 말 것!
try {
		int i = 0;
		while(true)
				range[i++].climb();
} catch (ArrayIndexOutOfBoundsException e) {
}

해당 예시는 예외가 잘못 사용된 예시로 배열의 원소를 순회하는데, 무한루프를 돌다가 배열의 끝에 도달해 예외가 발생하면 끝을 내는 로직으로 작성한 코드이다.

직관적이지 않다는 점 하나만으로도 제어 흐름용으로 예외를 사용하면 안되는 이유는 충분하다. 표준적인 관용구대로 작성하자.

//표준적인 관용구 사용 - 올바른 방법
for (Mountain m : range)
		m.climb();

의미도 알기 쉽고, 헷갈릴 일이 없다.

그런데 예외를 써서 루프를 종료한 이유가 뭘까? 잘못된 추론을 근거로 성능을 높여보려 한 것이다.

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

  1. 예외는 예외 상황에 쓸 용도로 설계되었으므로, JVM 구현자 입장에서는 위 같은 코드가 빠르게 돌아갈지에 대해서는 전혀 고려하지 않았을 것이다.
  2. 코드를 try-catch 블록 안에 넣으면 JVM이 적용할 수 있는 최적화가 제한 된다.
  3. 배열을 순회하는 표준 관용구는 앞서 걱정한 중복 검사를 수행하지 않는다. JVM이 알아서 최적화해 없애준다.

하지만 실상은 예외를 사용한 쪽이 표준 관용구보다 훨씬 느리고, 디버깅에 어려움을 겪을 수도 있다.

그래서 결론은 예외는 오직 예외 상황에서만 써야 한다. 절대로 일상적인 제어 흐름용으로 쓰여선 안 된다.

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

이러한 상태 검사 메서드 덕분에 다음과 같은 표준 for 관용구를 사용할 수 있다.

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

hasNext()를 이용하여 올바르게 순회하고 있다. for-each를 사용해도 내부적으로 hasNext()를 사용하기 때문에 사실 for-each를 사용하는 것이 더 깔끔하다.

IteratorhasNext()를 제공하지 않았다면 그 일을 클라이언트가 대신해야만 했다.

//컬렉션을 이런 식으로 순회하지 말 것!
try {
    Iterator<Foo> i = collection.iterator();
    while (true) {
        Foo foo = i.next();
        ...
    }
} catch (NoSuchElementException e) {
}