Skip to content

Latest commit

 

History

History
135 lines (98 loc) · 5.13 KB

예외는_진짜_예외_상황에서만_사용하라.md

File metadata and controls

135 lines (98 loc) · 5.13 KB

이펙티브 자바 아이템 69 : 예외는 진짜 예외 상황에만 사용하라

→ 예외를 안써도 되는 상황에서는 예외를 쓰지마라

예외를 예외 상황에서 쓰지 다른 상황에 어떻게 쓰지?

Car[] cars = new Car[100];
try {
    int index = 0;
    while (true) {
        cars[index++].advance();
    }
} catch (ArrayIndexOutOfBoundsException e) {
}

작성한 근거

  • JVM이 배열에 접근할 때 경계를 검사하는데, 일반적인 반복문도 경계에 도달하면 종료한다.
  • 일반적인 반복문을 사용하면 일을 두 번 하니까 한가지 일을 생략했다.

위 코드의 문제점

  • 코드를 한눈에 파악하기 어렵다.
  • 예외는 예외 상황에서 쓸 용도로 만들어졌기 때문에 최적화를 신경 쓴 코드로 판단하기 힘들다.
  • 코드를 try-catch 안에 넣으면 JVM이 적용할 수 있는 최적화가 제한된다.
  • 배열을 순회하는 표준 관용구는 중복검사를 하지 않는다. JVM이 최적화를 했다.
for (Cars car : cars) {
    car.advance();
}

실제로 예외를 사용한 쪽이 표준 관용구보다 훨씬 느리다. (라고 말하지만)

자동차 5000만개를 생성해서 전진시키는 코드를 작성했다.

  • 코드 보기

    public class ExceptionTest {
    
        @Test
        void test1() {
            Car[] cars = new Car[50_000_000];
            for (int i = 0; i < 50_000_000; i++) {
                cars[i] = new Car(new Name("test"));
            }
    
            double before = System.currentTimeMillis();
            for (Car car : cars) {
                car.advance(RandomUtil.getNumbersInRange(10));
            }
            double after = System.currentTimeMillis();
            System.out.println("반복문 속도 : " + (after - before) + "(ms)");
        }
    
        @Test
        void test2() {
            Car[] cars = new Car[50_000_000];
            for (int i = 0; i < 50_000_000; i++) {
                cars[i] = new Car(new Name("test"));
            }
    
            double before = System.currentTimeMillis();
            try {
                int index = 0;
                while (true) {
                    cars[index++].advance(RandomUtil.getNumbersInRange(10));
                }
            } catch (ArrayIndexOutOfBoundsException e) {
            }
            double after = System.currentTimeMillis();
            System.out.println("예외처리한 속도 : " + (after - before)+ "(ms)");
        }
    
    }

책에서는 느리다고 하지만 생각보다 큰 차이는 없다.

느린 것 보다 더 큰 문제는 코드가 제대로 동작하지 않을 수도 있다.

반복문 안에 버그가 숨어있다면 흐름 제어에 쓰인 예외가 이 버그를 숨겨 디버깅을 어렵게 만든다.

반복문에서 호출한 메서드가 내부에서 관련 없는 배열을 사용하였다가 ArrayIndexOutOfBoundsException 을 일으켰다고 가정해보자.

표준 관용구

  • 예외를 잡지 않고 스택에 정보를 남기고 스레드를 즉각 종료시킨다.

예외를 사용한 반복문

  • 버그때문에 발생한 엉뚱한 예외를 정상적인 반복문 종료 상황으로 오해하고 넘어간다.

그래서 예외는 예외 상황에서만 써야한다. 일반적인 제어 흐름용으로 쓰면 안된다.

표준적인 관용구를 사용하고, 성능 개선을 목적으로 과하게 머리를 쓴 기법을 자제한다. 실제 성능이 좋아지더라도 자바가 발전하면서 최적화로 얻은 성능 우위가 오래가지 않을 수도 있다. 반면 버그의 폐해와 유지보수 문제는 계속 이어질 것이다.

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

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

Iterator 인터페이스의 nexthasNext 가 각각 상태 의존적 메서드와 상태 검사 메서드에 해당된다. 이러한 메서드 덕분에 for 관용구를 사용할 수 있다. for-each도 내부적으로 hasNext를 사용한다.

List<Car> cars = new ArrayList<>();
for (Iterator<Car> i = cars.iterator(); i.hasNext();) {
   ... 
}
List<Car> cars = new ArrayList<>();
try {
    Iterator<Car> i = cars.iterator();
    while (true) {
        Car car = i.next();
    }
    
} catch (NoSuchElementException e) {
}

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

null 또는 Optional과 같은 특수한 값을 반환하는 방법이다.

  1. 동기화 없이 여러 스레드가 동시에 접근할 수 있거나 외부 요인으로 상태가 변할 수 있다면 옵셔널이나 특정 값을 사용한다.
  2. 성능이 중요한 상황에서 상태 검사 메서드가 상태 의존적 메서드의 작업 일부를 중복 수행한다면 옵셔널이나 특정 값을 선택한다.
  3. 이외에는 상태 검사 메서드 방식이 더 낫다고 할 수 있다.