예외는 오직 예외 상황에만 사용할 것

예외를 제어 흐름용으로 사용하지 말자

• 예외는 오직 예외 상황에서만 사용해야 한다.

• 일반적인 제어 흐름용으로 사용해서는 안됨

    ◦  코드를 헷갈리게 한다(코드가 직관적이지 못하다).

    ◦  성능 저하의 원인이 될 수 있다.

    ◦  엉뚱한 곳에서 발생한 버그를 숨겨버릴 수 있다.

 

예외를 제어 흐름용으로 사용한 예시

try {
	int i = 0;
	while(true) { mountainList[i++].climb(); }
} catch(ArrayIndexOutOfBoundsException e) {...}
for(Mountain m : mountainList) { m.climb(); }

• 두 코드 모두 mountainList 배열의 원소를 순회하는 목적으로 쓰여졌다.

• 예외를 제어흐름용으로 사용하니 코드가 전혀 직관적이지 못하다.

• 이 작자는 코드를 왜 이렇게 짰을까?

    ◦  JVM 은 배열에 접근할 때 마다 경계를 넘지 않는지 검사한다.

    ◦  for 문은 배열의 경계에 도달하면 반복문을 종료한다.

    ◦  for 문 안에서 배열에 접근하고 있으니, 배열의 경계에 대한 검사를 2번 하고 있다고 생각할 수 있다.

    ◦  중복 검사를 피하면 성능을 향상시킬 수 있지 않을까? → 예외를 사용에 제어 흐름을 구현하면 되겠다!

 

    ◦  이 추론이 잘못된 이유

          -  예외는 예외 상황에 쓸 용도로 설계된 것이다. JVM 개발자가 최적화에 신경쓰지 않았을 가능성이 크다.

          -  코드를 try-catch 블록 안에 넣으면 JVM 이 적용할 수 있는 최적화가 제한된다.

          -  배열을 순회하는 for 문은 앞서 걱정한 중복 검사를 수행하지 않는다. JVM 이 알아서 최적화해주기 때문.

          -  결과적으로 성능이 개선되지 않는다! (실제로 코드를 돌려보면 예외를 사용한 쪽이 2배 이상 느리다)

 

    ◦  뿐만 아니라 반복문 안에 버그가 숨어 있다면 흐름 제어에 쓰인 예외가 이 버그를 숨겨 디버깅을 훨씬 어렵게 할 수 있다.

          -  반복문의 몸체 내부에서 다른 배열을 사용하는 경우, 해당 배열에서 ArrayIndexOutOfBound 예외가 발생할 수 있다.

          -  표준 관용구

              -  예외처리 하지 않고 스택 추적 정보를 남긴 다음 해당 스레드를 종료시킨다.

          -  예외를 사용한 반복문

              -  ArrayIndexOutOfBound 예외 발생 = 배열 순회 종료 라고 간주하고 있음

              -  즉 실제 버그 상황에서 발생한 예외를 반복문 종료 상황으로 오해하고 넘어간다.

 

코드를 짤 때는 꼼수를 쓰지 말자

• 표준적이고 쉽게 이해되는 관용구를 사용하라

• 성능 개선을 목적으로 과하게 머리를 쓴 기법은 자제하라.

    ◦  꼼수로 성능이 개선되더라도 자바 플랫폼은 꾸준히 업데이트 되기 때문에 상대적인 성능 우위는 오래가지 않는다.

    ◦  반면 꼼수에 숨겨진 미묘한 버그와 어려워진 유지보수 문제는 계속된다.


상태 검사 메소드 / 옵셔널 / 특정 값

상태 의존적 메소드와 상태 검사 메소드

• 지금까지 살펴본 원칙은 API 설계 시에도 유의해야하는 부분이다.

• 특히 특정 상태에서만 호출할 수 있는 상태 의존적 메소드를 제공하는 클래스의 경우, 상태 검사 메소드를 함께 제공해 사용자가

    예외 처리를 제어 흐름에 사용하지 않도록 해줘야 한다.

    ◦  상태 의존적 메소드 예시 : Iterator 의 next

          -  Iterator 에 다음 원소가 있어야 next 메소드 호출이 가능하므로

    ◦  상태 검사 메소드 예시 : Iterator 의 hasNext

          -  Iterator 에 다음 원소가 있는지 여부를 hasNext 메소드를 통해 검사할 수 있다.

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

• 상태 검사 메소드 없이 상태 의존 메소드만 있었다면, 사용자는 아래와 같이 코드를 작성할 수 밖에 없을 것이다.

try {
	Iterator<Foo> i = collection.iterator();
	while(true) { Foo foo = i.next(); }
} catch (NoSuchElementException e) {...}

 

 

상태 검사 메소드 / 옵셔널 / 특정 값

• 상태 검사 메소드 대신 올바르지 않은 상태일 때 빈 옵셔널을 반환하거나, null 같이 사전에 정의해둔 특정 값을 반환하는 방법도 있다.

• 아래는 이 세가지 방식 중 하나를 선택하는 것에 대해 책에서 소개하는 지침이다.

    ◦  옵셔널 혹은 특정 값을 반환하는 방식

          -  외부 동기화 없이 여러 스레드가 동시에 접근할 수 있는 경우

          -  외부 요인으로 상태가 변할 수 있는 경우

              → 상태 검사 메소드와 상태 의존적 메소드를 호출하는 사이에 객체의 상태가 변할 수 있기 때문

          -  성능이 중요한 상황, 상태 검사 메서드가 상태 의존적 메서드의 작업 일부를 중복 수행하는 경우

    ◦  상태 검사 메서드를 사용하는 방식

          -  위의 상황을 제외한 다른 모든 경우

              → 가독성이 더 좋다 + 상태 검사 메서드 호출이 누락된 경우 상태 의존적 메소드가 이를 잡아주기 때문에 버그를 잡기 쉽다.

          -  단, 특정 값은 검사하지 않고 지나쳐도 발견하기 어렵다는 단점이 있다.