[교재 EffectiveJava] 아이템 69. 예외는 진짜 예외 상황에만 사용하라

반응형
728x90
반응형

예외의 잘못된 사용

결론적으로 잘못된 코드. 느리다.
package com.java.effective.item68;

public class Main {
    public static void main(String[] args) {
        try {
            int i = 0;
            
            while(true) {
                range[i++].climb();
            }
        } catch (ArrayIndexOutOfBoundsException e) {

        }
    }
}

위 코드는 직관적이지 않다. 해당 코드는 배열을 무한루프로 순회하다가 배열의 끝에 도달하여 ArrayIndexOutOfBoundsException이 발생하면 끝을 낸다. 

 

표준적은 관용구 코드로 수정 
package com.java.effective.item68;

public class Main {
    public static void main(String[] args) {
        try {
            for (Mountain m : range) {
                m.climb();
            }
        } catch (ArrayIndexOutOfBoundsException e) {

        }
    }
}

이쯤되면, 예외를 사용해서 루프를 종료한 이유가 궁금하다. JVM은 배열에 접근할 때마다 경계를 넘지 않는지 검사하는데, 일반적인 반복문도 배열 경계에 도달하면 종료한다. 이 검사를 반복문에 명시하면 같은 일이 중복되므로 하나를 생략한 것이다. 이로써 성능을 높여보려는 속셈이였지만, 세가지 면에서 잘못된 추론이다.

 

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

실상은 예외를 사용한 쪽이 표준 관용구보다 훨씬 느리다. 반복문 안의 버그를 숨겨버리고, ArrayIndexOutOfBoundsException 에러가 다른 이유로 발생했을 경우에도 오류가 나지 않고 정상적으로 반복문을 종료시킨다. 

 

 

상태 검사 메서드 사용 

예외는 오직 예외 상황에서만 써야한다. 절대로 일상적인 제어 흐름용으로 쓰여선 안된다. 잘 설계된 API라면 클라이언트가 정상적인 제어 흐름에서 예외를 사용할 일이 없게 해야한다. 

 

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

 

Iterator.java
package java.util;

import java.util.function.Consumer;

public interface Iterator<E> {
    /**
     * Returns {@code true} if the iteration has more elements.
     * (In other words, returns {@code true} if {@link #next} would
     * return an element rather than throwing an exception.)
     *
     * @return {@code true} if the iteration has more elements
     */
    boolean hasNext();

    /**
     * Returns the next element in the iteration.
     *
     * @return the next element in the iteration
     * @throws NoSuchElementException if the iteration has no more elements
     */
    E next();

    ...
}

Iterator 인터페이스의 next와 hasNaxt가 각각 상태 의존적 메서드와 상태 검사 메서드에 해당한다. 

 

▶ Iterator가 hasNext를 제공하지 않았다면?

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

}

위 코드는 우리가 앞서 봤던, 잘못된 예외 사용의 예시와 동일한 단점을 가진다. 장황하고 헷갈리며, 속도도 느리고 엉뚱한 곳에서 발생한 버그를 숨겨버린다. 

 

 Iterator가 hasNext를 제공했기 때문에 가능한 코드

//Iterator 표준 관용구 사용
for (Iterator<Foo> i = collection.iterator(); i.hashNext();) {
  Foo foo = i.next();
  ...
}

 

 

또다른 방법

상태 검사 메서드 대신 사용할 수 있는 선택지도 있다. 올바르지 않은 상태일때 빈 옵셔널(Optional) 혹은 null 같은 특수한 값을 반환하는 방법이다.

 

상태 검사 메서드, 옵셔널, 특정 값 중 하나를 선택하는 지침은 아래와 같다.

 

1) 옵셔널이나 특정 값을 사용

- 외부 동기화 없이 여러 스레드가 접근할 수 있거나 외부 요인으로 상태가 변할 수 있다면 옵셔널이나 특정 값을 사용한다. 상태 검사 메서드와 상태 의존적 메서드 호출 사이에 객체의 상태가 변할 수도 있기 때문이다.

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

 

2) 상태 검사 메서드 방식 사용

위 1)번의 경우와 다른 모든 경우에 사용한다. 상태 검사 메서드 방식이 가독성이 살짝 더 좋고, 잘못 사용했을때 발견하기 쉽기 때문이다. 상태 검사 메서드 호출을 깜빡 잊었다면 상태 의존적 메서드가 예외를 던져 버그를 확실히 처리할 수 있다.  반면, 특정값은 검사하지 않고 지나쳐도 발견하기가 어렵다.

 

 

 

반응형

Designed by JB FACTORY