다음은 잘못된 코드의 예시입니다.
try {
int i = 0;
while(true)
range[i++].climb();
} catch (ArrayIndexOutOfBoundsException e) {
}
일단 전혀 직관적이지 않다는 사실 하나만으로도 코드를 이렇게 작성하면 안 되는 이유는 충분합니다.(Item 67).
다음과 같이 표준적인 관용구대로 작성했다면 이해하기 쉬웠을 것입니다.
for (Mountain m: range)
m.climb();
예외를 써서 루프를 종료한 이유는, 잘못된 추론을 근거로 성능을 높여보려고 한 것입니다. JVM은 배열에 접근할 때마다 경계를 넘지 않는지 검사를 하는데, 일반적인 반복문도 이 검사를 중복하므로 하나를 생략하기 위해 위와 같이 코드를 짠것입니다. 하지만 이는 세 가지 면에서 잘못된 추론입니다.
예외를 사용한 반복문은 심지어 제대로 동작하지 않을 수도 있습니다. 반복문 안에 버그가 숨어 있다면 흐름 제어에 쓰인 예외가 이 버그를 숨겨 디버깅을 훨씬 어렵게 만들겁니다.(m.climb() 메서드 내부에서 ArrayIndexOutOfBoundsException 발생하는 경우)
예외는 오직 예외 상황에서만 써야 합니다. 절대로 일상적인 제어 흐름용으로 쓰여선 안 됩니다. 표준적이고 쉽게 이해되는 관용구를 사용하고, 성능 개선을 목적으로 과하게 머리를 쓴 기법은 자제해야합니다. 실제로 성능이 좋아지더라도 자바 플랫폼이 꾸준히 개선되고 있으니 최적화로 얻은 상대적인 성능 우위가 오래가지 않을 수 있으며, 숨겨진 미묘한 버그의 폐해와 어려워진 유지보수 문제가 계속 발생하게 됩니다.
이 원칙은 API 설계에도 적용됩니다. 잘 설계된 API라면 클라이언트가 정상적인 제어 흐름에서 예외를 사용할 일이 없게 해야 합니다. 특정 상태에서만 호출할 수 있는 '상태 의존적' 메서드를 제공하는 클래스는 '상태 검사' 메서드도 함께 제공해야 합니다(Iterator 인터페이스의 next와 hasNext가 대표적).
for (Iterator<Foo> i = collection.iterator(); i.hasNext(); ) {
Foo foo = i.next();
...
}
만약 hasNext가 없었다면, 아래와 같이 클라이언트에서 예외 처리를 하게 됩니다.(나쁜 예시)
try {
Iterator<Foo> i = collection.iterator();
while(true) {
Foo foo = i.next();
...
}
} catch (NoSuchElementException e) {
}
상태 검사 메서드 대신 사용할 수 있는 선택지로 올바르지 않은 상태일 때 빈 옵셔널(Item 55)을 반환하는 방법도 있습니다. 상태 검사 메서드, 옵셔널, 특정 값 중 하나를 선택하는 지침은 아래와 같습니다.
핵심 정리
예외는 예외 상황에서 쓸 의도로 설계되었다. 정상적인 제어 흐름에서 사용해서는 안 되며, 이를 프로그래머에게 강요하는 API를 만들어서도 안 된다.
출처