Post

Item 69 - 예외는 진짜 예외 상황에만 사용하라

예외 처리를 제어 흐름 방식으로 사용했을 때 🙁

1
2
3
4
5
6
7
// 예외를 완전히 잘못 사용한 예
try {
    int i = 0;
    while(true)
        range[i++].climb();
} catch (ArrayIndexOutOfBoundsException e) {
}
  • 배열의 원소를 순회하면서 무한루프를 돌다가, 배열의 끝에 도달해ArrayIndexOutOfBoundsException이 발생하면 끝을 내는 코드이다.
  • 예외를 써서 루프를 종료한 이유? - 이 코드를 작성한 개발자의 의도
    • 예외를 통한 검사가 표준 관용구보다 빠를 것이다.
    • try-catch 블록 안에 넣어도 JVM이 적용할 수 있는 최적화가 제한되지 않는다.
    • 배열을 순회하는 표준 관용구는 배열 경계 검사를 수행한다. → 중복 검사를 줄여보자는 의도
    • But, 모두 잘못된 추론이다.

위 추론이 잘못된 이유 🤔

  • JVM 구현자 입장에서는 위 예시 코드가 빠르게 돌아갈지에 대해선 전혀 고려하지 않을 것이다.
    • 예외는 예외적인 상황에 대응하기 위한 수단이지, 코드의 실행 흐름을 제어하기 위한 수단이 아니다.
    • 그러므로, JVM 구현자들은 예외 처리에 대한 성능 최적화를 크게 고려하지 않는다.
    • 따라서, 예외를 로직에 이용하면 오히려 성능 저하를 초래할 수 있다.
  • 코드를 try-catch 블록 안에 넣으면 JVM이 적용할 수 있는 최적화가 제한된다.
  • 배열을 순회하는 표준 관용구의 배열 경계 검사는 JVM이 알아서 최적화해 이를 중복 수행하지 않는다.

정리하면, 제어 흐름용으로 예외 처리를 사용하는 게 표준 관용구를 사용하는 것보다 빠를 거라는 추론에서 위와 같은 코드를 작성했을 것이다.

하지만 실상은 예외를 사용한 쪽이 표준 관용구보다 훨씬 느리다.

표준 관용구

1
2
3
// 표준 관용구로 작성한 예
for (Mountain m : range)
    m.climb();
  • 의미 파악도 쉽고, 헷갈리지 않는다.
  • 성능 저하도 없다.

교훈

예외는 오직 예외 상황에서만 쓰자.

  • 절대 일상적인 제어 흐름용으로 쓰지 말자.
  • 실제로 성능이 상승하더라도, 언제든 JVM의 발전에 의해 따라잡힐 수 있다.
  • 어려운 유지보수 문제는 덤이다.

이러한 원칙은 API 설계에도 적용된다.

예외 관점에서 잘 설계된 API 란? 👌

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

  • ‘상태 의존적’ 메서드를 제공하는 클래스는 ‘상태 검사’ 메서드도 함께 제공해야 한다.
  • ex) Iterator 인터페이스의 next(상태 의존적 메서드)와 hasNext(상태 검사 메서드)
1
2
3
4
5
// 두 메서드 덕분에 표준 for 관용구를 사용할 수 있음
for (Iterator<Foo> i = collections.iterator(); i.hasNext(); ) {
    Foo foo = i.next();
    ...
}

Iterator가 상태 검사 메서드를 제공하지 않았다면, 클라이언트가 그 일을 대신해야 한다.

  • 이러한 형태의 코드는 가독성이 떨어지고, 속도도 느린데다, 버그를 숨기기까지 한다.
1
2
3
4
5
6
7
try {
    Iterator<Foo> i = collection.iterator();
    while(true) {
        Foo foo = i.next();
        ...
} catch (NoSuchElementException e) {
}

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

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

언제 무엇을 쓰는 것이 좋을까?

(1) 외부 동기화 없이 여러 스레드가 동시에 접근 가능 하거나 외부 요인으로 상태가 변할 수 있을 때

👉 옵셔널 또는 특정 값

1
2
3
4
5
6
7
8
// 상태 검사 메소드 사용 예
if (obj.isAvailable()) {
    obj.doSomething(); // obj의 상태가 isAvailable() 호출 이후에 변경될 수 있다면 문제 발생
}

// 옵셔널 사용 예
Optional<ResultType> result = obj.tryToDoSomething();
result.ifPresent(r -> ...);
  • 상태 검사 메서드와 상태 의존적 메서드 호출 사이에 객체가 변할 가능성이 있기 때문이다.
(2) 성능이 중요한 상황에서 상태 검사 메서드가 상태 의존적 메서드의 작업 일부를 중복 수행할 때

👉 옵셔널 또는 특정 값

1
2
3
4
5
// 상태 검사 메소드 사용 예 (비효율적)
if (obj.computeValue() > 10) {
    int value = obj.computeValue();
    ...
}

(3) 다른 모든 경우엔 상태 검사 메서드가 낫다.

  • 가독성이 좋고, 잘못 사용했을 때 발견하기가 쉽다.
  • 상태 검사를 누락할 경우, 예외를 던져 버그를 확실히 드러낼 것이다.

💡 핵심 정리

  • 정상적인 제어 흐름에서 예외를 사용하지 말자.
  • 이를 프로그래머에게 강요하는 API도 만들지 말자.
This post is licensed under CC BY 4.0 by the author.