// 예외를 완전히 잘못 사용한 예 - 따라 하지 말 것!
try {
int i = 0;
while(true)
range[i++].climb();
} catch (ArrayIndexOutOfBoundsException e) {
}
해당 예시는 예외가 잘못 사용된 예시로 배열의 원소를 순회하는데, 무한루프를 돌다가 배열의 끝에 도달해 예외가 발생하면 끝을 내는 로직으로 작성한 코드이다.
직관적이지 않다는 점 하나만으로도 제어 흐름용으로 예외를 사용하면 안되는 이유는 충분하다. 표준적인 관용구대로 작성하자.
//표준적인 관용구 사용 - 올바른 방법
for (Mountain m : range)
m.climb();
의미도 알기 쉽고, 헷갈릴 일이 없다.
그런데 예외를 써서 루프를 종료한 이유가 뭘까? 잘못된 추론을 근거로 성능을 높여보려 한 것이다.
JVM은 배열에 접근할 때마다 경계를 넘지 않는지 검사하는데, 일반적인 반복문도 배열 경계에 도달하면 종료한다. 따라서 이 검사를 반복문에도 명시하면 같은 일이 중복되므로 하나를 생략한 것이다. 하지만 세 가지 면에서 잘못된 추론이다.
try-catch
블록 안에 넣으면 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
를 사용하는 것이 더 깔끔하다.
Iterator
가 hasNext()
를 제공하지 않았다면 그 일을 클라이언트가 대신해야만 했다.
//컬렉션을 이런 식으로 순회하지 말 것!
try {
Iterator<Foo> i = collection.iterator();
while (true) {
Foo foo = i.next();
...
}
} catch (NoSuchElementException e) {
}