Item69. 예외는 진짜 예외 상황에만 사용하라
예외(Exception)란 사용자의 잘못된 어플리케이션 조작이나 개발자의 코딩 실수로 인해 발생하는 프로그램 오류를 말한다.
예외가 발생되면 프로그램은 곧바로 종료된다는 점에서 에러와 동일하나, 예외는 예외 처리를 통해 프로그램을 종료하지 않고 정상 실행 상태가 유지되도록 할 수 있다.
예외의 종류는 개발자가 코드 작성을 하고 .class 파일로 만들기 위해 컴파일을 진행할 때 아예 컴파일진행이 되지 않는 컴파일 예외(Checked Exception)와 Run 실행 후 애플리케이션 정상 동작중 발생되는 런타임 예외(Unchecked Exception) 두 종류가 있다.
컴파일 예외는 컴파일러가 예외처리 하라고 강제하여 알려주니 비교적 쉽게 예외처리를 할 수 있지만 런타임 예외는 명시적인 예외 처리가 강제되지 않아 언제 어떻게 일어날지 잘 알 수 없어 애플리케이션 작동 중 한참 뒤에 발견될 수도 있다. 프로그램 개발시 다양하게 테스트를 진행해야 발견될 수 있다.
런타임시 (어플리케이션 작동) 발생되는 문제로 에러와 예외가 있는데 에러는 프로그램이 코드로 복구될 수 없는 심각한 문제를 의미하고 예외는 개발자가 직접 문제를 예측하여 막을 수 있는 처리가 가능한 오류라고 보면된다.
에러의 예시로 애플리케이션 구동 시 메모리가 부족하여 어플리케이션 작동이 멈췄다면 OutOfMemoryError가 발생한 것으로 개발자는 메모리 부족이 해결되길 기다리거나 애플리케이션을 재구동 시키는 방법밖에 할 수 없다.
런타임 예외로는 어떤 수를 0으로 나누게 된다면 ArithmeticException이 발생하게 된다.
어떠한 수가 0으로 나누어질 가능성이 있다면 개발자가 별도의 분기처리문이나 try catch를 통해 예외 발생을 제어할 수 있다.
예외상황이 아닌 흐름제어용으로 사용한 경우
Loop1번의 메서드를 보면 어떤 일을 하는 기능인지 전혀 직관적이지 않다.
무한루프를 돌다가 배열의 끝에 도달하면 ArrayIndexOutOfBoundsException이 발생하며 끝을내는 이해하기 어려운 끔찍한 코드이다.
Loop2의 메서드로 작성했다면 배열의 크기만큼 순회하면서 작업을 처리한다는 의미를 한번에 알 수 있다.
Loop1처럼 예외를 사용하여 반복문을 사용한이유는 잘못된 추론을 근거로 성능을 높여보려 한 것이다.
JVM은 배열에 접근할 때마다 경계를 넘지 않는지 검사하는데 일반적인 반복문도 배열 경계에 도달하면 종료한다.
같은 일이 중복되지 않도록 하나를 생략한 것인데 이는 잘못된 추론으로
- 예외는 예외 상황에 쓸 용도로 설계되었으므로 JVM 구현자 입장에서는 명확한 검사만큼 빠르게 만들어야 할 동기가 약하다.(최적화에 별로 신경쓰지 않았을 가능성이 크다.)
- 코드를 try-catch 블록 안에 넣으면 JVM이 적용할 수 있는 최적화가 제한된다.
- 배열을 순회하는 표준 관용구(forEach문)는 앞서 걱정한 중복 검사를 수행하지 않는다. JVM이 알아서 최적화해준다.
예외를 사용한 반복문의 단점
- 코드를 헷갈리게 하고 성능을 떨어뜨린다.
- 제대로 동작하지 않을 수 있다.
- 반복문 안에 버그가 숨어있다면 흐름 제어에 쓰인 예외가 이 버그를 숨겨 디버깅을 훨씬 어렵게 한다.
- 반복문의 몸체에서 호출한 메서드가 내부에서 관련 없는 배열을 사용하다가 ArrayIndexOutOfBoundsException을 일으켰다면 버그 때문에 발생한 엉뚱한 예외를 정상 반복문 종료 상황으로 오해할 수 있다.
성능 테스트
자바11로 테스트 했는데 벌써 최적화 코드가 추가된건지 책에서 예외를 사용했을 때 2배정도 느리다고 했는데 시간은 비슷했다.
모쪼록 이 챕터의 교휸은 예외는 오직 예외 상황에서만 써야한다. 절대로 일상적인 제어 흐름용으로 쓰여서 안된다.
표준적이고 쉽게 이해되는 관용구를 사용하고, 성능 개선을 목적으로 과하게 머리를 쓴 기법은 자제해라.
실제 성능이 좋아지더라도 자바 플랫폼이 꾸준히 개선되고 있으니 최적화로 얻은 상대적인 성능 우위가 오래가지 않을 수 있다. 반면 과하게 영리한 기법에 숨겨진 미묘한 버그의 폐해와 어려워진 유지보수 문제는 계속 이어질 것이다.
잘 설계된 API
잘 설계된 API라면 클라이언트가 정상적인 제어 흐름에서 예외를 사용할 일이 없게 해야 한다.
특정 상태에서만 호출할 수 있는 '상태 의존적' 메서드를 제공하는 클래스는 '상태 검사' 메서드도 함께 제공해야한다.
Iterator 인터페이스의 next는 상태 의존적 메서드, hasNext가 상태 검사 메서드이다.
for (Iterator<Foo> i = collections.iterator(); i.hasNext(); ) {
Foo foo = i.next();
...
}
두 메서드 덕분에 표준 for 관용구를 사용할 수 있다. (for-each도 내부적으로 hasNext를 사용한다.)
try {
Iterator<Foo> i = collection.iterator();
while(true) {
Foo foo = i.next();
...
} catch (NoSuchElementException e) {
}
Iterator가 hasNext를 제공하지 않았다면 그 일을 클라이언트가 대신해야했다.
올바르지 않은 상태일 때 빈 Optional 혹은 특정값을 반환할 수도 있다.
- 외부 동기화 없이 여러 스레드가 동시에 접근할 수 있거나 외부 요인으로 상태가 변할 수 있다면 옵셔널이나 특정 값을 사용한다. 상태 검사 메서드와 상태 의존적 메서드 호출 사이에 객체의 상태가 변할 수 있기 때문이다.
- 성능이 중요한 상황에서 상태 검사 메서드가 상태 의존적 메서드의 작업 일부를 중복 수행한다면 옵셔널이나 특정 값을 선택한다.
- 다른 모든 경우엔 상태 검사 메서드 방식이 조금 더 낫다고 할 수 있다. 가독성이 살짝 더 좋고, 잘못 사용했을 때 발견하기가 쉽다. 상태 검사 메서드 호출을 깜빡 잊었다면 상태 의존적 메서드가 예외를 던져 버그를 확실히 드러낼 것이다. 반면 특정 값은 검사하지 않고 지나쳐도 발견하기 어렵다.(옵셔널엔 해당하지 않는 문제)
핵심정리
예외는 예외 상황에서 쓸 의도로 설계되었다. 정상적인 제어 흐름에서 사용해서는 안 되며, 이를 프로그래머에게 강요하는 API를 만들어서도 안된다.
https://jake-seo-dev.tistory.com/564
https://jihyehwang09.github.io/2020/06/07/effective-java-exception
https://javabom.tistory.com/82