DEV ℧ Developer Diary

[EffectiveJava] item69 - 예외는 진짜 예외 상황에만 사용하라

다음과 같은 코드를 마주치게 된다고 하자.

try {
    int i =0;
    while(true)
        range[i++].climb();
} catch (ArrayIndexOutOfBoundsException e) { }

무슨 일을 하는 코드인지 한번에 알 수 있을까? 전혀 직관적이지 않다는 사실 하나만으로도 코드를 이렇게 작성하면 안 되는 이유는 충분하다.

이 코드는 배열의 원소를 순회하는데, 끔찍한 방법으로 돌고 있다. 무한루프를 돌다가 배열의 끝에 도달해 ArrayIndexOutOfBoundsException이 발생하면 끝을 내는 것이다.

만약 다음과 같이 표준적인 관용구대로 작성했다면 모든 자바 프로그래머가 곧바로 이해했을 것이다.

for (Mountain m : range)
    m.climb();

예외를 써서 루프를 종료한 이유는 무엇일까? 잘못된 추론을 근거로 성능을 높여보려 한 것이다.

JVM은 배열에 접근할 때마다 경계를 넘지 않는지 검사하는데, 일반적인 반복문도 배열 경계에 도달하면 종료된다.

이 검사를 반복문에도 명시하면 같은 일이 중복되므로 하나를 생략한다.

하지만 이는 잘못된 추론이다.

잘못된 추론으로 인한 예외 처리

  1. 예외는 예외 상황에 쓸 용도로 설계되었으므로 JVM 구현자 입장에서는 명확한 검사만큼 빠르게 만들어야 할 동기가 약하다.
  2. 코드를 try-catch 블록 안에 넣으면 JVM이 적용할 수 있는 최적화가 제한 된다.
  3. 배열을 순회하는 표준 관용구는 앞서 걱정한 중복검사를 수행하지 않는다. JVM이 알아서 최적화해서 없애준다.

또 예외를 사용한 쪽이 표준 관용구보다 훨씬 느리다. 원소 100개짜리 배열로 테스트해보니 2배 정도 느려졌다.

예외를 사용한 반복문의 해악

이러한 잘못된 코드는 헷갈리게만 하고 성능을 떨어뜨리는데서 끝나지 않는다.

제대로 동작하지 않을 수도 있다. 예를 들어 ArrayIndexOutOfBoundsException 예외가 발생되었다고 해보자.

for문만을 사용한 일반 관용구 였다면 이 버그는 예외를 잡지 않고 해당 스레드를 즉각 종료시킬 것이다. 하지만 예외를 사용한 반복문은 버그 때문에 발생한 엉뚱한 예외를 정상적인 반복문 종료 상황으로 오해하고 넘어간다.

이 이야기의 교훈은 간단하다. 예외는 (그 이름이 말해주듯) 오직 예외 상황에서만 써야 한다. 절대로 일상적인 제어 흐름용으로 쓰여선 안된다.

특정 상태에서만 호출 하는 클래스

잘 설계된 API라면 클라이언트가 정상적인 제어 흐름에서 예외를 사용할 일이 없게 해야 한다.

특정 상태에서만 호출할 수 있는 ‘상태 의존적’ 메서드를 제공하는 클래스는 ‘상태 검사’ 메서드도 함께 제공해야 한다.

Iterator

Iterator 인터페이스의 next와 hasNext가 각각 상태 의존적 메서드와 상태 검사 메서드에 해당한다. 그리고 별도의 상태 검사 메서드 덕분에 다음과 같은 for 표준 관용구를 사용할 수 있다. (for-each 내부적으로 hasNext를 사용한다.)

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

만약 IteratorhasNext를 제공하지 않았다면 그 일을 클라이언트가 대신해야만 했다.

/** 컬렉션을 이런 식으로 순회하면 안된다! */
try {
    Iterator<Foo> i = collection.iterator();
    while(true) {
        Foo foo = i.next();
        ...
    }
}

이 코드는 처음에 소개했던 예외를 사용했던 반복문과 상당히 비슷하다.

반복문에 예외를 사용하면 장황하고 헷갈리며 속도도 느리고, 엉뚱한 곳에서 발생한 버그를 숨기기도 한다.

상태 검사 메서드 외 방법들

상태 검사 메서드 대신 사용할 수 있는 선택지도 있다.

올바르지 않은 상태일때 빈 옵셔널 혹은 null 같은 특수한 값을 반환하는 방법이다.

  1. 외부 동기화 없이 여러 스레드가 동시에 접근할 수 있거나 외부 요인으로 상태가 변할 수 있다면 옵셔널이나 특정 값을 사용한다. 상태 검사 메서드와 상태 의존적 메서드 호출 사이에 객체의 상태가 변할 수 있기 때문이다.
  2. 성능이 중요한 상황에서 상태 검사 메서드가 상태 의존적 메서드의 작업 일부를 중복 수행한다면 옵셔널이나 특정 값을 선택한다.
  3. 다른 모든 경우엔 상태 검사 메서드 방식이 조금 더 낫다고 할 수 있다. 가독성이 살짝 더 좋고, 잘못 사용했을 때 발견하기가 쉽다.