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) {
    // 배열의 끝에 도달하면 여기로 온다
}

위 코드의 문제점

  1. 코드의 의도가 명확하지 않다

    • 코드를 읽는 사람이 무엇을 하려는지 바로 이해하기 어렵다

  2. 예외는 성능을 고려하지 않고 설계되었다

    • 예외 처리는 일반적인 코드 흐름보다 느리다

  3. JVM의 최적화가 제한된다

    • try-catch 블록 안에 코드가 있으면 JVM이 할 수 있는 최적화가 제한된다

  4. 의도하지 않은 예외를 무시할 수 있다

    • 모든 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());  // 값이 있을 때만 실행

어느 패턴을 선택해야 할까?

  1. 여러 스레드가 동시에 접근하거나 외부 요인으로 상태가 변할 수 있는 경우

    • 옵셔널이나 특수 값을 사용하자

    • 상태 검사 후 메서드 호출 사이에 상태가 바뀔 수 있기 때문

  2. 성능이 중요한 상황에서 상태 검사가 중복 작업을 하는 경우

    • 옵셔널이나 특수 값을 사용하자

  3. 그 외의 경우

    • 상태 검사 메서드 패턴이 가독성이 좋고 오류를 발견하기 쉽다


🧩 어려웠던 점

예외가 성능에 미치는 영향을 이해하는 데 시간이 걸렸다

제어 흐름에 예외를 사용하는 코드가 실제로 표준 방법보다 느리다는 사실은 직관적이지 않았다

또한 API 설계 시 여러 패턴(상태 검사 메서드, 옵셔널, 특수 값 반환) 중 어느 것을 사용하는 것이 적절한지 판단하는 기준을 이해하는 데도 어려움이 있었다

💡 느낀 점

코드의 가독성과 성능을 고려할 때, 표준적인 제어 흐름을 사용하는 것이 중요하다는 것을 알게 되었다

API를 설계할 때는 사용자의 관점에서 생각하여 예외에 의존하지 않고도 쉽게 API를 사용할 수 있도록 해야 한다는 점도 배웠다

Last updated