티스토리 뷰

프로그래밍/책

[이펙티브 자바, Effeective Java] 10장 예외

시간이nullnull한 가장 2022. 2. 4. 11:36

69 예외는 진짜 예외 상황에만 사용하라


예외는 (그 이름이 말해주듯) 오직 예외 상황에서만 써야 한다. 절대로 일상적인 제어 흐름용으로 쓰여선 안 된다.

잘 설계된 API라면 클라이언트가 정상적인 제어 흐름에서 예외를 사용할 일이 없게 해야 한다.


 

70 복구할 수 있는 상황에서는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라


자바는 문제 상황을 알리는 타입(throwable)으로 검사 예외, 런타임 예외, 에러, 이렇게 세 가지를 제공한다.

각 방식을 언제 사용할지 참고할 수 있는 지침들을 살펴보자.

  1. 호출하는 쪽에서 복구하리라 여겨지는 상황이라면 검사 예외를 사용하라. → 메서드 선언에 포함된 검사 예외 각각은 그 메서드를 호출했을 때 발생할 수 있는 유력한 결과임을 API 사용자에게 알려주는 것이다. (달리 말하면 사용자에게 검사 예외란 그 상황을 회복해내라고 요구한 것과 같다.) → 비검사 throwable은 두 가지로, 바로 런타임 예외와 에러다. 이 둘은 프로그램에서 잡을 필요가 없거나 혹은 통상적으로는 잡지 말아야 한다. 즉, 복구가 불가능하거나 더 실행해봐야 득보다는 실이 많다는 뜻이다.
  2. 프로그래밍 오류를 나타낼 때는 런타임 예외를 사용하라. → 런타임 예외의 대부분은 전제조건을 만족하지 못했을 때 발생한다. 전제조건 위배란 단순히 클라이언트가 해당 API의 명세에 기록된 제약을 지키지 못했다는 뜻이다.
  3. 에러는 보통 더 이상 수행을 계속할 수 없는 상황을 나타낼 때 사용한다. 업계에 널리 퍼진 규약으로서 비검사 throwable은 모두RuntimeException의 하위 클래스여야 한다.
  4. throwable은 이로울 게 없으니 절대로 사용하지 말자! → throwable 클래스들은 대부분 오류 메시지 포맷을 상세히 기술하지 않는다. 예외의 메서드가 없다면 프로그래머들은 오류 메시지를 파싱해 정보를 빼내야 한다.
  5. 검사 예외는 일반적으로 복구할 수 있는 조건일 때 발생한다. 따라서 호출자가 예외 상황에서 벗어나는 데 필요한 정보를 알려주는 메서드를 함께 제공하는 것이 중요하다.

71 필요 없는 검사 예외 사용은 피하라


검사 예외가 단 하나뿐이라면 오직 그 예외 때문에 API 사용자는 try 블록을 추가해야 하고 스트림에서 직접 사용하지 못하게 된다. 그러니 이런 상황이라면 검사 예외를 안 던지는 방법이 없는지 고민해볼 가치가 있다.

검사 예외를 회피하는 가장 쉬운 방법은 적절한 결과 타입을 담은 옵셔널을 반환하는 것이다. 검사 예외를 던지는 대신 단순히 빈 옵셔널을 반환하면 된다.

 

이 방식의 단점이라면 예외가 발생한 이유를 알려주는 부가 정보를 담을 수 없다는 것이다.


 

72 표준 예외를 사용하라


표준 예외를 재사용하면 얻는 게 많다. 그중 최고는 내가 작성한 API가 다른 사람이 익히고 사용하기 쉬워진다는 것이다.

  1. IllegalArgumentException → 허용하지 않는 값이 인수로 건네졌을 때(null은 따로 NullPointerException으로 처리)
  2. IllegalStateException → 객체가 메서드를 수행하기에 적절하지 않은 상태일 때
  3. NullPointerException → null을 허용하지 않는 메서드에 null을 건넸을 때
  4. IndexOutOfBoundsException → 인덱스가 범위를 넘어섰을 때
  5. ConcurrentModificationException → 허용하지 않는 동시 수정이 발견됐을 때
  6. UnsupportedOperationException → 호출한 메서드를 지원하지 않을 때

이상이 확실히 가장 흔하게 재사용되는 예외이다. 상황에 부합한다면 항상 표준 예외를 재사용하자.


74 메서드가 던지는 모든 예외를 문서화하라


검사 예외는 항상 따로따로 선언하고, 각 예외가 발생하는 상황을 자바독의 @throws 태그를 사용하여 정확히 문서화하자.

 

메서드가 Exception이나 Throwable을 던진다고 선언해서는 안 된다. 메서드 사용자에게 각 예외에 대처할 수 있는 힌트를 주지 못할뿐더러, 같은 맥락에서 발생할 여지가 있는 다른 예외들까지 삼켜버릴 수 있어 API 사용성을 크게 떨어뜨린다.

 

비검사 예외도 검사 예외처럼 정성껏 문서화해두면 좋다. 비검사 예외는 일반적으로 프로그래밍 오류를 뜻하는데 자신이 일으킬 수 있는 오류들이 무엇인지 알려주면 프로그래머는 자연스럽게 해당 오류가 나지 않도록 코딩하게 된다.

 

메서드가 던질 수 있는 예외를 각각 @throws 태그로 문서화하되, 비검사 예외는 메서드 선언의 throws 목록에 넣지 말자.


 

75 예외의 상세 메시지에 실패 관련 정보를 담으라


실패 순간을 포착하려면 발생한 예외에 관여된 모든 매개변수와 필드의 값을 실패 메시지에 담아야 한다.


 

76 가능한 한 실패 원자적으로 만들라


호출된 메서드가 실패하더라도 해당 객체는 메서드 호출 전 상태를 유지해야 한다. 이러한 특성 을 실패 원자적(failure-atomic)이라고 한다.

 

메서드를 실패 원자적으로 만드는 가장 간단한 방법은 불변 객체로 설계하는 것이다.

가변 객체의 메서드를 실패 원자적으로 만드는 가장 흔한 방법은 작업 수행에 앞서 매개변수의 유효성을 검사하는 것이다.

 

만약 객체가 메서드 호출 후, 그 이전 상태를 유지하지 못한다면 실패 시의 객체 상태를 API 설명에 명시해야 한다.


 

77 예외를 무시하지 말라


예외를 무시하기로 했다면 catch 블록 안에 그렇게 결정한 이유를 주석으로 남기고 예외 변수의 이름도 ignored로 바꿔놓도록 하자.

Future<Integer> f = exec.submit(planarMap::chromaticNumber);
int numColors = 4; // 기본값. 어떤 지도라도 이 값이면 충분하다.
try {
  numColors = f.get(1L, TimeUnit.SECONDS);
} catch (TimeoutException | ExecutionException ignored) {
  // 기본값을 사용한다(색상 수를 최소화하면 좋지만, 필수는 아니다).
}