Language/Java
[effective java] 아이템 69. 예외는 진짜 예외 상황에만 사용하라.
JOYERIM
2023. 7. 31. 15:13
예시 1
코드 1 - 예외 처리를 제어 흐름 방식으로 사용
// 예외를 완전히 잘못 사용한 예 - 훨씬 느리다.
try { // 성능이 2배 정도 느리다
int i = 0;
while(true)
range[i++].climb();
} catch (ArrayIndexOutOfBoundsException e) {
}
- 배열의 원소를 순회하는데, 무한루프를 돌다가 배열의 끝에 도달해 ArrayIndexOutOfBoundsException이 발생하면 끝을 내는 코드이다.
코드 2 - 표준적인 관용구
// 표준적인 관용구로 작성한 예
for (Mountain m : range)
m.climb();
코드 1의 문제점
- 예외 처리는 비용이 많이 드는 작업이다.
- 예외는 코드의 정상적인 실행 흐름을 제어하기 위한 수단이 아니라, 예외적인 상황에 대응하기 위한 수단이다.
- 그러므로 JVM 구현자들은 예외 처리에 대한 성능 최적화를 크게 고려하지 않는다.
- 따라서, 예외를 일반적은 제어 흐름 제어로 사용하면 성능 저하를 초래할 수 있다.
- 코드를 try-catch 블록 안에 넣으면 JVM이 적용할 수 있는 최적화가 제한된다.
- JVM은 코드를 실행하면서 자동으로 여러 가지 최적화를 수행한다.
- 그러나 예외를 던지는 코드는 JVM이 최적화하기 어렵다.
- 이는 예외를 던지는 작업 자체가 비용이 크고, 예외가 발생하면 실행 흐름이 예측하기 어려워지기 때문이다.
- 배열을 순회하는 표준 관용구는 범위 검사를 알아서 처리한다.
- for-each 루프는 JVM에서 특별히 최적화되어, 루프의 범위 검사를 알아서 처리한다.
- 따라서 따로 범위 검사를 추가할 필요가 없다.
- 흐름 제어를 위한 예외가 다른 버그를 숨겨 디버깅을 훨씬 어렵게 한다.
교훈
예외는 오직 예외 상황에서만 써야 한다. 절대로 일상적인 제어 흐름용으로 쓰면 안 된다.
표준적이고 쉽게 이해되는 관용구를 사용하고, 성능 개선을 목적으로 과하게 머리를 쓴 기법은 자제하라.
잘 설계된 API라면 클라이언트가 정상적인 제어 흐름에서 예외를 사용할 일이 없게 해야 한다.
: 특정 상태에서만 호출할 수 있는 "상태 의존적" 메소드를 제공하는 클래스는 "상태 검사" 메소드도 함께 제공해야 한다.
방법 1. 상태 검사 메소드
- ex. Iterator 인터페이스의 next(상태 의존적 메소드)와 hasNext(상태 검사 메소드)
- for-each도 내부적으로 hasNext 메소드를 사용한다.
for (Iterator<Foo> i = collections.iterator(); i.hasNext(); ) {
Foo foo = i.next();
}
- 만약 Iterator가 hasNext를 제공하지 않을 경우, 클라이언트가 대신해야만 한다.
- 앞서 다뤘듯, 반복문에 예외를 사용하면 가독성이 떨어지고, 속도도 느려지며, 버그가 숨겨져 디버깅이 어렵다.
try {
Iterator<Foo> i = collection.iterator();
while(true) {
Foo foo = i.next();
...
} catch (NoSuchElementException e) {
}
방법 2. 빈 옵셔널(item 55) 혹은 null같은 특수한 값
- 상태 검사 메소드 대신, 올바르지 않은 상태일 때 빈 옵셔널(item 55) 혹은 null같은 특수한 값을 반환하는 방법도 있다.
상태 검사 메소드, 옵셔널, 특정 값 중 하나를 선택하는 지침
1. 외부 동기화 없이 여러 스레드가 동시에 접근 가능하거나 상태가 변할 수 있을 때 👉 옵셔널 또는 특정 값
- 상태 검사와 상태 의존적 메소드 호출 사이에 객체의 상태가 변할 가능성이 있다.
- 따라서 옵셔널이나 특정 값을 사용한다.
// 상태 검사 메소드 사용 예
if (obj.isAvailable()) {
obj.doSomething(); // obj의 상태가 isAvailable() 호출 이후에 변경될 수 있다면 문제 발생
}
// 옵셔널 사용 예
Optional<ResultType> result = obj.tryToDoSomething();
result.ifPresent(r -> ...);
2. 성능이 중요한 상황에서 상태 검사 메소드가 중복 작업을 수행할 때 👉 옵셔널 또는 특정 값
- 상태 검사와 상태 의존적 메소드의 동작 사이에 중복된 계산이 발생할 수 있다.
- 이럴 경우 성능 최적화를 위해 옵셔널 또는 특정 값을 사용하는 게 좋다.
// 상태 검사 메소드 사용 예 (비효율적)
if (obj.computeValue() > 10) {
int value = obj.computeValue();
...
}
3. 다른 모든 경우 👉 상태 검사 메소드
- 대부분 상황에서는 상태 검사 메소드가 가독성과 안정성 면에서 더 좋다
상태 검사 메소드의 이점
- 가독성
- 상태 검사 메소드는 직관적이다.
- 상태를 먼저 확인하고 상태에 따라 동작을 수행한다.
if (stack.isEmpty()) {
System.out.println("Stack is empty!");
} else {
stack.pop();
}
- 버그 발견 용이성
- 상태 검사 메소드를 사용하면, 만약 개발자가 상태 검사를 누락하게 되면, 상태 의존적 메소드에서 예외가 발생하게 될 확률이 높다.
- 따라서 버그를 빨리 발견할 수 있다.
// 깜빡하고 isEmpty 검사를 빼먹었다면 pop()에서 예외 발생
stack.pop();
특정 값의 문제점
- 특정 값을 반환하는 메소드는 종종 오류 상황을 암묵적으로 나타낸다.
- ex. 리스트에서 특정 항목을 찾지 못했을 때 -1을 반환하는 메소드는 -1이 실제 의미하는 값인지, 오류를 나타내는 값인지 구분하기 어렵다.
int position = list.find("item");
if (position != -1) {
// 항목 발견
} else {
// 항목 없음
}