예외는 예외 상황에서만 써야 하지, 절대로 일상적인 제어 흐름용으로 쓰여서는 안된다. 또한, 이를 프로그래머에게 강요하는 API를 만들어서도 안된다.
try { // 성능이 2배 정도 느리다
int i = 0;
while(true)
range[i++].climb();
} catch (ArrayIndexOutOfBoundsException e) {
}
배열의 원소를 순회하는데, 무한루프를 돌다가 배열의 끝에 도달해 예외가 발생하면 끝을 내는 로직으로 작성한 코드이다. 직관적이지 않다는 점 하나만으로도 제어 흐름용으로 예외를 사용하면 안되는 이유는 충분하다.
for (Mountain m : range)
m.climb();
실제로 예외를 사용한 쪽이 표준 관용구보다 2배 정도 느리다.
잘 설계된 API는, 클라이언트가 정상적인 제어 흐름에서 예외를 사용할 일이 없어야 한다.
특정 상태에서만 호출할 수 있는 상태 의존적 메서드를 제공하는 클래스는, 상태 검사 메서드도 함께 제공한다면 예외를 사용하지 않을 수 있다.
Iterator
인터페이스의 next
는 상태 의존적 메서드, hasNext
는 상태 검사 메서드에 해당한다.
for (Iterator<Foo> i = collections.iterator(); i.hasNext(); ) {
Foo foo = i.next();
}
try {
Iterator<Foo> i = collection.iterator();
while(true) {
Foo foo = i.next();
...
} catch (NoSuchElementException e) {
}
이처럼 반복문에 예외를 사용하면 코드가 헷갈리고 성능도 좋지 않으며, 엉뚱한 곳에서 발생한 버그를 본의 아니게 숨기기도 한다.
상태 검사 메서드 이외에, 올바르지 않은 상태일 때 빈 Optional
혹은 null
같은 특수한 값을 반환하는 방법도 존재한다.
- 외부 동기화 없이 여러 스레드가 동시에 접근할 수 있거나 외부 요인으로 상태가 변할 수 있다면 옵셔널이나 특정 값을 사용한다. 상태 검사 메서드와 상태 의존적 메서드 호출 사이에 객체의 상태가 변할 수 있기 때문이다.
- 성능이 중요한 상황에서 상태 검사 메서드가 상태 의존적 메서드의 작업을 일부 중복 수행한다면 옵셔널이나 특정 값을 선택한다.
- 다른 모든 경우엔 상태 검사 메서드 방식이 좋다. 가독성이 좋고, 잘못 사용했을 때 발견하기 쉽기 때문이다.(상태 검사 메서드 호출을 잊었다면 메서드는 예외를 던질것이다.) 옵셔널은 괜찮지만, 특정 값은 검사하지 않고 지나쳐도 발견하기 어렵다.
예외는 예외 상황에서 쓸 의도로 설계되었다. 정상적인 제어 흐름에서 사용해서는 안 되며, 이를 프로그래머에게 강요하는 API를 만들어서도 안 된다.