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 목록에 넣지 말자.
    • 한 클래스에 많은 메서드가 같은 이유로 예외를 던진다면, 클래스 설명에 추가하자.

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; } ```

        • 이렇게 해두면 실패에 대한 정보를 훨씬 잘 파악할 수 있다.

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) {
          // 기본값을 사용한다.(색상 수를 최소화하면 좋지만, 필수는 아니다.)
      }