예외를 안 써도 되는 상황에서는 예외를 사용하지 말자
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
try { | |
int i = 0; | |
while (true) { | |
range[i++].climb(); | |
} | |
} catch (ArrayIndexOutOfBoundsException e) { | |
log.error("" + e); | |
} |
코드가 달성하고자 한 목표
- JVM은 배열에 접근할 때마다 인덱스가 범위를 넘지 않는지 검사
- 일반적인 반복문도 배열 범위의 경계에 도달하면 종료
- 일반적인 반복문을 사용하면 검사를 두 번하므로 두 검사 중 하나를 생략함으로써 성능 최적화를 목표함
위 코드의 문제점
- 코드 가독성 저하
- 잘못된 추론을 근거로 성능 최적화를 노렸고 잘못되었다는 근거는 다음과 같음
- 예외는 예외 상황에 사용할 용도로 설계되었으므로 JVM 구현자 입장에서는 최적화에 별로 신경 쓰지 않았을 가능성이 큼
- try-catch 블록 내 코드는 JVM이 적용할 수 있는 최적화가 제한됨
- 배열을 순회하는 표준 관용구는 앞서 걱정한 중복 검사를 수행하지 않음 (JVM이 알아서 최적화)
1. 예외를 사용한 쪽이 표준 관용구보다 훨씬 느림
실제로 테스트해 본 결과 예외를 사용한 쪽이 표준 관용구를 사용한 반복문보다 훨씬 느렸습니다.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Slf4j | |
public class Example { | |
private static int MAX = 10_000; | |
public static void main(String[] args) { | |
Range[] range = new Range[MAX]; | |
for (int i = 0; i < MAX; i++) { | |
range[i] = new Range(i); | |
} | |
long start = System.currentTimeMillis(); | |
try { | |
int i = 0; | |
while (true) { | |
range[i++].climb(); | |
} | |
} catch (ArrayIndexOutOfBoundsException e) { | |
log.error("" + e); | |
} | |
long end = System.currentTimeMillis(); | |
System.out.println(String.format("잘못된 최적화 실행시간: %d", end - start)); | |
start = System.currentTimeMillis(); | |
for (int i = 0; i < MAX; i++) { | |
range[i].climb(); | |
} | |
end = System.currentTimeMillis(); | |
System.out.println(String.format("반복문 실행시간: %d", end - start)); | |
} | |
static class Range { | |
private int height; | |
public Range(int height) { | |
this.height = height; | |
} | |
public void climb() { | |
// System.out.println("climb() 메서드 호출"); | |
} | |
} | |
} |

2. 예외를 사용한 쪽이 느릴 뿐만 아니라 코드가 의도한 대로 동작하지 않을 수 있음
- 반복문 내 버그가 숨어있을 경우 흐름 제어에 쓰인 예외가 해당 버그를 숨기기 때문에 디버깅을 어렵게 만듦
- 예외가 발생한 부분에서 실제로 문제가 발생한 것인지, 아니면 그 이전의 코드에서 문제가 발생한 것인지 확인하기 어려울 수 있음
- 반복문은 동일한 코드 블록을 반복적으로 실행하기 때문에 여러 번의 반복이 일어날 수 있으며 이러한 상황에서 어느 반복에서 문제가 발생했는지 정확히 파악하기 어려움
반복문에서 호출한 메서드가 내부에서 관련 없는 배열을 사용했다가 `ArrayIndexOutOfBoundsException` 예외를 던졌다고 가정했을 때
- 표준 관용구는 예외를 잡지 않고 스택에 정보를 로깅 후 쓰레드를 즉각 종료
- 반면, 예외를 사용한 반복문에서는 버그 때문에 발생한 엉뚱한 예외를 정상적인 반복문 종료 상황으로 오해하고 넘어감 (예외를 잡고 던지지 않았기 때문에)
3. 예외는 예외 상황에서만 사용하고 일반적인 제어 흐름용으로 쓰지 말자
- 일반적인 제어 흐름은 표준 관용구인 for 문을 사용할 것
- 앞서 예시 코드처럼 성능 개선을 목적으로 예외 사용하는 것을 지양해야 함
- 실제 성능이 좋아지더라도 자바 버전이 올라가면서 최적화를 진행하기 때문에 성능 우위가 오래가지 않을 확률이 높음
- 반면, 버그의 폐해와 유지보수 문제는 지속될 확률이 높음
4. 잘 설계된 API는 클라이언트가 정상적인 제어 흐름에서 예외를 사용하지 말자
- 특정 상태에서만 호출할 수 있는 `상태 의존적`인 메서드를 제공할 경우 `상태 검사` 메서드도 함께 제공해야 함
- ex) Iterator 인터페이스의 `next`와 `hasNext`가 각각 상태 의존적 메서드와 상태 검사 메서드에 해당
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
List<Integer> list = new ArrayList<>(); | |
for (int i = 0; i < 10; i++) { | |
list.add(i); | |
} | |
for (Iterator<Integer> i = list.iterator(); i.hasNext(); ) { | |
System.out.print(i.next()); | |
if (i.hasNext()) { | |
System.out.print(" "); | |
} | |
} | |
System.out.println(); |

4.1 지양해야 하는 코드 스타일
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
try { | |
Iterator<Integer> i = list.iterator(); | |
while (true) { | |
System.out.print(i.next() + " "); | |
} | |
} catch (NoSuchElementException e) { | |
log.error("" + e); | |
} |

4.2 상태 검사 메서드 대신 사용할 수 있는 선택지
올바르지 않은 상태일 때 빈 Optional 혹은 null 같은 특수한 값을 반환하는 방법도 존재합니다.
- 동기화 없이 여러 쓰레드가 동시에 접근할 수 있거나 외부 요인으로 상태가 변할 수 있다면 옵셔널이나 특정 값을 사용
- 성능이 중요한 상황일 경우 상태 검사 메서드가 상태 의존적 메서드의 작업 일부를 중복 수행하기 때문에 Optional이나 특정 값 선택
예시를 들자면 다음과 같습니다.
- 빈 Optional 반환: 메서드가 올바르지 않은 상태라고 판단되면 빈 Optional을 반환
- 이렇게 함으로써 메서드 호출자는 반환된 Optional이 비어있는지 여부를 확인하고 적절히 처리
- 이 방법은 명시적이며, 상태 검사 메서드를 호출한 후에 반환된 값으로 처리를 수행하는 대신, Optional을 반환하여 상태를 명확하게 표현 가능
- null 반환: Java에서는 null을 사용하여 참조형 변수의 값이 없음을 표현
- 메서드가 올바르지 않은 상태라고 판단되면 null을 반환하여 호출자에게 상태를 알릴 수 있음
- 하지만 이 방법은 NullPointerException을 유발할 수 있으므로 주의해서 사용
- 특정 값을 반환: 때로는 특정 값을 사용하여 상태를 나타낼 수 있음
- ex) -1을 사용하여 유효하지 않은 인덱스를 나타내거나, 빈 문자열을 사용하여 유효하지 않은 문자열을 나타낼 수 있음
- 이러한 접근 방식은 Optional보다는 덜 안전하고 명시적이지 않을 수 있으므로 사용에 주의
위에서 언급한 두 케이스를 제외하고는 상태 검사 메서드 방식이 더 나은 방식입니다.
- 가독성 측면에서 우위이고 오류를 발견하기 쉬움
- 상태 검사 메서드 호출을 누락했을 경우 상태 의존적 메서드가 예외를 던져 버그를 확실히 드러낼 수 있음
참고
이펙티브 자바
반응형
'JAVA > Effective Java' 카테고리의 다른 글
[아이템 71] 필요 없는 검사 예외 사용은 피하라 (0) | 2024.03.25 |
---|---|
[아이템 70] 복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라 (0) | 2024.03.23 |
[아이템 68] 일반적으로 통용되는 명명 규칙을 따르라 (0) | 2024.03.20 |
[아이템 67] 최적화는 신중히 하라 (0) | 2024.03.19 |
[아이템 66] 네이티브 메서드는 신중히 사용하라 (0) | 2024.03.19 |