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.