ITEM 69. 예외는 진짜 예외 상황에만 사용하라
try {
int i = 0;
while(true) {
range[i++].climb();
}
} catch(ArrayIndexOutOfBoundsException e) {
}
- 잘못된 추론
- 예외는 예외 상황에 쓸 용도로 설계되었으므로 JVM 구현자 입장에서는 명확한 검사만큼 빠르게 만들어야 할 동기가 약하다
- 코드를 try-catch 블록 안에 넣으면 JVM이 적용할 수 있는 최적화가 제한된다.
- 배열을 순회하는 표준 관용구는 앞서 걱정한 중복 검사를 수행하지 않는다. JVM이 알아서 최적화해 없어준다.
-
예외를 사용한 쪽이 표준 관용구보다 훨씬 느리다.
- 상태 검사 메서드, 옵셔널, 특정 값 중 하나를 선택하는 지침
- 외부 동기화 없이 여러 스레드가 동시에 접근할 수 있거나 외부 요인으로 상태가 변할 수 있다면 Optional이나 특정 값을 사용. 상태 검사 메서드와 상태 의존적 메서드 호출 사이에 객체의 상태가 변할 수 있기 때문이다.
- 성능이 중요한 상황에서 상태 검사 메서드가 상태 의존적 메서드의 작업 일부를 중복 수행한다면 옵셔널이나 특정 값을 선택
- 다른 모든 경우엔 상태 검사 메서드 방식이 조금 더 낫다고 할 수 있다. 가독성이 살짝 더 좋고 잘못 사용했을 때 발견하기 쉽다. 상태 검사 메서드 호출을 깜빡잊었다면 상태 의존적 메서드가 예외를 던져 버그를 확실히 들어낼 것이다.
ITEM 70. 복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라
- throwable(문제상황을 알리는 타입)으로 검사 예외, 런타임 예외, 에러가 있다.
- 검사 예외
- 호출하는 쪽에서 복구하리라 여겨지는 상황이면, 검사 예외 사용
- try-catch 문으로 잡아 처리해야 되기 때문에 바깥으로 전파한다.
- 비검사 예외
- 런타임 예외
- 프로그래밍 오류를 나타낼 때에는 런타임 예외를 사용
- 에러
- JVM이 자원 부족, 불변식 깨짐 등 더 이상 수행을 계속할 수 없을 때 사용
- 구현하고자 하는 비검사예외는 RuntimeException의 하위 클래스여야 한다.
- Error 클래스는 상속에 사용해서는 안되고, throw 문으로 던지는 일도 없어야 한다.
- 런타임 예외
- 검사 예외도 아니고 런타임 예외도 아닌 throwable은 정의하지도 말자.
- 검사 예외라면 복구에 필요한 정보를 알려주는 메서드도 제공하자.\
- 검사 예외
ITEM 71. 필요 없는 검사 예외 사용은 피하라.
- 검사 예외
- 결과를 코드로 반환하거나 비검사 예외를 던지는 것과 달리, 검사 예외는 발생한 문제를 프로그래머가 처리하여 안전성을 높여준다.
- 하지만 검사 예외를 과하게 사용하면 쓰기 불편한 API가 된다.
- Optional
- 검사 예외를 회피하는 가장 쉬운 방법은 적절한 결과 타입을 담은 Optional을 반환하는 것
- 단점은 예외가 발생한 이유를 알려주는 부가 정보를 담을 수 없다는 것
- 검사 예외를 던지는 메서드를 2개로 쪼개 비검사 예외로 변경
try { obj.actions(args); } catch(TheCheckedException e) { ... // 예외 상황 대치 }
if(obj.actionPermitted(args) { obj.action(args); } else { ... // 예외 상황 대처 }
- 모든 상황에 적용할 수는 없지만 적용할 수 있다면 사용하기 편리한 API를 제공할 수 있다.
- 단점으로는 외부 동기화 없이 여러 스레드가 동시에 접근할 수 있거나 외부 요인에 의해 상태가 변할 수 있다면 적절하지 않다.
ITEM 72. 표준 예외를 사용하라
- 표준 예외 사용
- 많은 프로그래머에게 이미 익숙해진 규약을 그대로 따르기 때문에 다른 사람이 익히고 사용하기 편해진다.
- IllegalArgumentException
- 호출자가 인수로 부적절한 값을 넘길 때 던지는 예외
- IndexOutOfBoundsException / NullPointerException
- 특수한 상황에는 해당 예외를 던지는 것이 좋다.
- IllegalStateException
- 대상 객체의 상태가 호출된 메서드를 수행하기에 적합하지 않을 때 주로 사용
- CurrentModificationException
- 단일 스레드에서 사용하려고 설계한 객체를 여러 스레드가 동시에 수정하려고 할 때 던진다.
- UnsupportedOperationException
- 클라이언트가 요청한 동작을 대상 객체가 지원하지 않을 때 던진다.
- Exception, RuntimeException, Throwable, Error는 직접 재사용하지 말자
- 여러 성격의 예외들을 포괄하는 클래스이므로 안정적으로 테스트할 수 없다.
ITEM 73. 추상화 수준에 맞는 예외를 던지라
- 예외 번역
- 상위 계층에서는 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꿔 던져야 한다.
try { ... // 저수준 추강화를 이용 } catch(LowerLevelException e) { // 추상화 수준에 맞게 번역 throw new HigherLevelException(...); }
- 남용해서는 안된다.
- 저수준 메서드가 반드시 성공하도록하여 아래 계층에서는 예외가 발생하지 않도록 하는 것이 최선
- 아래 계층에서 예외를 피할 수 없다면, 상위 계층에서 그 예외를 조용히 처리하여 문제를 API 호출자에게까지 전파하지 않는 방법이 있다.
- 상위 계층에서는 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꿔 던져야 한다.
ITEM 74. 메서드가 던지는 모든 에외를 문서화하라
- @throws 태그
- 검사 예외는 항상 따로따로 선언하고, 각 예외가 발생하는 상황을 자바독의 @throws 태그를 사용하여 정확히 문서화하자.
- 상위 클래스 하나로 뭉뚱그려서 선언하는 일은 삼가자
- 비검사 예외도 검사 예외처럼 문서화하는 것이 좋다.
- 메서드가 던질 수 있는 예외를 각각 @throws 태그로 문서화하되, 비검사 예외는 메서드 선언의 thros 목록에 넣지 말자.
- 한 클래스에 많은 메서드가 같은 이유로 예외를 던진다면, 클래스 설명에 추가하자.
- 검사 예외는 항상 따로따로 선언하고, 각 예외가 발생하는 상황을 자바독의 @throws 태그를 사용하여 정확히 문서화하자.
ITEM 75. 예외 상세 메시지에 실패 관련 정보를 담으라.
- stack trace
- 예외를 잡지 못해 프로그램이 실패하면 자바 시스템은 해당 예외의 스택 추적 정보를 자동으로 출력한다.
- 실패 순간을 포착하는 데 중요한 역할을 한다.
- 실패 순간을 포착하려면 발생한 예외에 관여된 모든 매개변수와 필드의 값을 실패 메시지에 담아야 한다.
- 예외 상세 메시지
- 최종 사용자에게 보여줄 오류 메시지와 혼동해서는 안된다.
```java
/*
- IndexOutOfBoundsException을 생성 *
- @param lowerBound 인덱스의 최솟값
- @param upperBound 인덱스의 최댓값 + 1
-
@param index 인덱스의 실젯값 */ public IndexOutOfBoundsException(int lowerBound, int upperBound, int index) { // 실패를 포착하는 상세 메시지 생성 super(String.format(“최솟값: %d, 최댓값: %d, 인덱스: %d”, lowerBound, upperBound, index));
// 프로그램에서 이용할 수 있도록 실패 정보 저장 this.lowerBound = lowerBound; this.upperBound = upperBound; this.index = index; } ```
- 이렇게 해두면 실패에 대한 정보를 훨씬 잘 파악할 수 있다.
- 최종 사용자에게 보여줄 오류 메시지와 혼동해서는 안된다.
```java
/*
ITEM 76. 가능한 한 실패 원자적으로 만들라
- 실패 원자적(failure atomic)
- 호출된 메서드가 실패하더라도 해당 객체는 메서드 호출 전 상태를 유지해야 한다.
- 실패 원자적 만드는 방법
- 불변 객체로 설계
- 불변 객체는 태생적으로 실패 원자적이다.
- 메서드가 실패하면 새로운 객체가 만들어지지는 않을 수 있으나 기존 객체가 불안정한 상태에 빠지는 일은 결코 없다.
- 매개변수 유효성 검사
- 객체의 내부 상태를 변경하기 전에 잠재적 예외의 가능성 대부분을 걸러낼 수 있는 방법
- 객체의 임시 복사본에서 작업을 수행한 다음 작업이 성공적으로 완료되면 원래 객체와 교체
- 실패를 가로채는 복구 코드를 작성하여 작업 전 상태로 되돌리는 방법
- 불변 객체로 설계
- 만약 실패 원자적 규칙을 지키지 못한다면 실패 시의 객체 상태를 API 설명에 명시해야 한다.
ITEM 77. 예외를 무시하지 말라
- catch 블록
- catch 블록을 비워두면 에외가 존재할 이유가 없어진다.
- 만약 예외를 무시하기로 했다면 catch 블록 안에 그렇게 결정한 이유를 주석으로 남기고 예외 변수의 이름도 ignored로 변경
Future<Integer> f = exec.submit(planerMap::chromatiocNumber); int numColors = 4; // 기본값, 어떤 지도라도 이 값이면 충분하다. try { numColors = f.get(1L, TimeUnit.SECONDS); } catch(TimeoutException | ExecutionException ignored) { // 기본값을 사용한다.(색상 수를 최소화하면 좋지만, 필수는 아니다.) }