예외를 완전히 잘못 사용한 예.
try {
int i = 0;
while(true) {
range[i++].climb();
}
} catch(ArrayIndexOutOfBoundsException e) {}
전혀 직관적이지 않다는 사실만으로도 코드를 이렇게 작성하면 안된다!
무한루프를 돌다가 배열의 끝에 도달해 ArrayIndexOutOfBoundsException
를 던지며 끝을 낸다.
for(Mountain m : range)
m.climb();
같은 동작을 유도한 코드이지만 직관적으로 코드가 하는 일을 파악할 수 있다.
위의 잘못된 예시를 사용한 이유는 무엇일까?
잘못된 추론을 근거로 성능을 높이보려 한 것이다. JVM은 배열에 접글할 때마다 경계를 넘지 않는지 검사를 하는데,
일반적인 반복문도 배열 경계에 도달하면 종료한다.
즉, 해당 검사의 반복을 줄인 것이다.
하지만 역시 잘못된 추론인데, 그 이유는 다음과 같다.
실상은 예외를 사용한 쪽이 표준 관용구보다 훨씬 느리다. (조선생님 컴퓨터에서 테스트해보니 2배 차이 났다고 한다.)
심지어 생각한대로 동작하지 않을 수 있다. 반복문 안에 버그가 숨어 있다면 흐름 제어에 쓰인 예외가 이 버그를 숨겨 디버깅을 훨씬 어렵게 할 것이다.
관련 없는 배열을 사용하다가 ArrayIndexOutOfBoundsException
예외를 만나는 경우, 정상적인 반복문 종료 상황으로 오해하고 넘어갈 수 있다.
표준적이고 쉽게 이해되는 관용구를 사용하고, 성능 개선을 목적으로 과하게 머리를 쓴 기법은 자제하라.
과하게 영리한 기법에 숨겨진 미묘한 버그의 폐해와 어려워진 유지보수 문제는 계속 이어질 것이다.
이러한 원칙은 API 설계에도 적용이 되는데, 잘 설계된 API라면 클라이언트가 정상적인 제어 흐름에서 예외를 사용할 일이 없게 해야 한다.
특정 상태에서만 호출할 수 있는 상태 의존적
메서드를 제공하는 클래스는 상태 검사
메서드도 함께 제공해야 한다.
예를 들면 Iterator
인터페이스의 next
와 hasNext
가 각각 상태 의존적 메서드와 상태 검사 메서드에 해당된다.
for-each
도 내부적으로 hasNext
를 사용하는데, 이러한 별도의 상태 검사 메서드 덕분에 표준 for 관용구를 사용할 수 있다.
for (Iterator<Foo> i = collection.iterator(); i.hasNext(); ) {
Foo foo = i.next();
...
}
Iterator
가 hasNext
를 제공하지 않았다면 처음의 잘못 사용된 예처럼 클라이언트가 대신 그 일을 해야만 했다.
반복문에 예외를 사용하면 장황하고 헷갈리며 속도도 느리고, 엉뚱한 곳에서 발생한 버그를 숨기기도 한다.
올바르지 않은 상태일 때 빈 옵셔늘 혹은 null 같은 특수한 값을 반환하는 방법이다.
상태 검사 메서드, 옵셔널, 특정 값 중 하나를 선택하는 지침은 아래와 같다.
정리
예외는 예외 상황에서 쓸 의도로 설계되었다. 정상적인 제어 흐름에서 사용해서는 안되며, 이를 프로그래머에게 강요하는 API를 만들어서도 안된다.