69. 예외는 진짜 예외 상황에만 사용하라
자바에서 예외는 프로그램 실행 중에 발생하는 예상치 못한 상황을 처리하기 위한 메커니즘이다
예외 처리를 통해 프로그램이 비정상적으로 종료되지 않고 계속 실행될 수 있도록 한다
// 예외 처리 예시
try {
// 예외가 발생할 수 있는 코드
int result = 10 / 0; // ArithmeticException 발생
} catch (ArithmeticException e) {
// 예외 처리 코드
System.out.println("0으로 나눌 수 없습니다");
}
예외의 잘못된 사용
예외는 이름 그대로 '예외적인 상황'에서만 사용해야 한다
하지만 일부 개발자들은 성능 최적화를 위해 예외를 일반적인 제어 흐름에 사용하기도 한다
// 잘못된 예 - 예외를 사용한 배열 순회
try {
int i = 0;
while (true) {
range[i++].climb(); // ❌ 범위를 벗어나면 예외 발생
}
} catch (ArrayIndexOutOfBoundsException e) {
// 배열의 끝에 도달하면 여기로 온다
}
위 코드의 문제점
코드의 의도가 명확하지 않다
코드를 읽는 사람이 무엇을 하려는지 바로 이해하기 어렵다
예외는 성능을 고려하지 않고 설계되었다
예외 처리는 일반적인 코드 흐름보다 느리다
JVM의 최적화가 제한된다
try-catch 블록 안에 코드가 있으면 JVM이 할 수 있는 최적화가 제한된다
의도하지 않은 예외를 무시할 수 있다
모든 ArrayIndexOutOfBoundsException을 잡아내기 때문에, 다른 곳에서 발생한 같은 예외도 무시될 수 있다
올바른 방법
일반적인 제어 흐름에는 예외가 아닌 표준 제어 구문을 사용해야 한다
// 좋은 예 - 표준 for-each문 사용
for (Mountain m : range) {
m.climb(); // ✅ 명확하고 이해하기 쉽다
}
성능 비교
많은 사람들이 예외를 사용한 방법이 더 빠를 것이라 생각하지만, 실제로는 표준 방법이 더 빠른 경우가 많다
// 성능 측정 코드
Mountain[] range = new Mountain[1000000];
long start, end;
// 1. 예외를 사용한 방법
start = System.currentTimeMillis();
try {
int i = 0;
while (true) {
range[i++].climb();
}
} catch (ArrayIndexOutOfBoundsException e) {
}
end = System.currentTimeMillis();
System.out.println("예외를 이용한 반복: " + (end - start) + "ms"); // 더 느림
// 2. 표준 for-each 방법
start = System.currentTimeMillis();
for (Mountain m : range) {
m.climb();
}
end = System.currentTimeMillis();
System.out.println("일반적 for-each: " + (end - start) + "ms"); // 더 빠름
정상 흐름과 예외 흐름 비교
코드 의도가 명확함
코드 의도가 불분명함
JVM 최적화 가능
JVM 최적화 제한됨
성능이 좋음
성능이 저하될 수 있음
예외 처리 분리됨
예외가 제어 흐름에 사용됨
API 설계 - 예외에 의존하지 않게 하기
잘 설계된 API는 클라이언트가 정상적인 제어 흐름에서 예외를 사용할 필요가 없도록 해야 한다
이를 위한 두 가지 패턴
1. 상태 검사 메서드 패턴
특정 상태에서만 호출할 수 있는 메서드가 있다면, 그 상태를 먼저 확인할 수 있는 메서드도 함께 제공한다
// Iterator 인터페이스 예시
Iterator<Mountain> iterator = range.iterator();
while (iterator.hasNext()) { // hasNext()로 상태 확인
Mountain m = iterator.next(); // next()는 hasNext()가 true일 때만 호출
m.climb();
}
2. 특수한 값 반환 패턴
상태를 검사하는 대신, 특별한 값(null이나 Optional 등)을 반환하는 방법도 있다
// Optional을 활용한 예시
public Optional<Mountain> findMountain(String name) {
// 산을 찾으면 Optional.of(mountain) 반환
// 없으면 Optional.empty() 반환
}
// 사용 예
Optional<Mountain> mountain = findMountain("에베레스트");
mountain.ifPresent(m -> m.climb()); // 값이 있을 때만 실행
어느 패턴을 선택해야 할까?
여러 스레드가 동시에 접근하거나 외부 요인으로 상태가 변할 수 있는 경우
옵셔널이나 특수 값을 사용하자
상태 검사 후 메서드 호출 사이에 상태가 바뀔 수 있기 때문
성능이 중요한 상황에서 상태 검사가 중복 작업을 하는 경우
옵셔널이나 특수 값을 사용하자
그 외의 경우
상태 검사 메서드 패턴이 가독성이 좋고 오류를 발견하기 쉽다
🧩 어려웠던 점
예외가 성능에 미치는 영향을 이해하는 데 시간이 걸렸다
제어 흐름에 예외를 사용하는 코드가 실제로 표준 방법보다 느리다는 사실은 직관적이지 않았다
또한 API 설계 시 여러 패턴(상태 검사 메서드, 옵셔널, 특수 값 반환) 중 어느 것을 사용하는 것이 적절한지 판단하는 기준을 이해하는 데도 어려움이 있었다
💡 느낀 점
코드의 가독성과 성능을 고려할 때, 표준적인 제어 흐름을 사용하는 것이 중요하다는 것을 알게 되었다
API를 설계할 때는 사용자의 관점에서 생각하여 예외에 의존하지 않고도 쉽게 API를 사용할 수 있도록 해야 한다는 점도 배웠다
Last updated