Item 72. 표준 예외를 사용하라
표준 예외를 재사용하면 익숙해진 규약을 그대로 따라 다른 사람이 익히고 사용하기 쉬워진다.
그리고 예외 클래스 수가 적어지니 메모리 사용량도 줄고 클래스를 적재하는 시간도 적게 걸린다. 자주 사용되는 예외를 보자.
예외 | 주요 쓰임 | 예시 |
IllegalArgumentException | 허용하지 않는 값이 인수로 건네졌을 때 | 반복 횟수 지정하는 매개변수에 음수 파라미터 |
IllegalStateException | 객체가 메소드 수행에 적절치 않은 상태일 때 | 제대로 초기화되지 않은 객체를 사용 |
NullPointerException | null 허용하지 않는 메소드에 null을 건넷을 때 | null이 올 수 없는 파라미터에 null 삽입 |
IndexOutOfBoundsException | 인덱스가 범위를 넘어섰을 때 | 배열 크기가 3인데 인덱스 4에 접근 |
ConcurrentModificationException | 허용하지 않는 동시 수정이 발견됐을 때 | 단일 스레드에서 사용하려 설계한 객체를 여러 스레드가 동시에 수정하려 할 때 (동시 수정을 확실히 검출하는 안정된 방법은 없으니 가능성을 알릴정도로만 사용) |
UnsupportedOperationException | 호출한 메소드를 지원하지 않을 때 | 클라이언트가 요청한 동작을 대상 객체가 지원하지 않을 때 ex) remove를 구현하지 않은 List 구현체에서 삭제 시도 |
ArithmeticException | 산술 조건에 예외가 발생했을 때 | 0으로 나누기, 산술 오버플로우 등 Math 클래스에서 자주 발생 |
NumberFormatException | 문자열을 숫자 형식으로 바꿀 때 문자열의 포멧이 올바르지 않은 경우 | parseInt(String s) |
이 외의 예외들은 jdk에서는 자주 사용하지만 사용자 입장에서 많이 사용되지 않는 것처럼 보였다.
Exception, RuntimeException, Throwable, Error는 직접 재사용하지 말자.
여러 성격의 예외들을 포괄하는 클래스니 이 클래스들은 추상 클래스라고 생각을 하자. 이 외 상황에 부합하다면 항상 표준 예외를 재사용하고, API 문서를 참고해 예외가 어떤 상황에서 발생하는지 확인하고 이름뿐 아니라 던져지는 맥락에도 부합할 때만 재사용하자.
더 많은 정보를 제공하길 원한다면 표준 예외를 확장해도 좋지만, 예외는 직렬화할 수 있다는 사실을 기억하자. 직렬화에 많은 부담이 따르니 나만의 예외를 새로 만들지 말아야 할 근거가 된다. Throwable은 직렬화를 구현했고 표준 예외들은 고유 serial number를 가진다.
주요 쓰임이 상호 배타적이지 않은 탓에 재사용할 예외를 선택하기 어려울 수도 있다.
이 때 인수 값이 무엇이든 어차피 실패했을 거라면 IllegalStateException, 그렇지 않으면 IllegalArgumentException을 던지자.
Item 73. 추상화 수준에 맞는 예외를 던지라
메소드가 저수준 예외를 처리하지 않고 밖으로 전파한다면 수행하려는 일과 관련 없는 예외가 나오는데, 이는 윗 레벨 API를 오염시킨다.
다음 릴리즈에서 구현 방식을 바꿔 다른 예외를 던지면 기존 클라이언트 프로그램을 깨지게할 수도 있다. 다음을 활용하자.
예외 번역(exception translation): 상위 계층에서는 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꿔 던지는 것
예외 연쇄: 문제의 근본 원인(cause)인 저수준 예외를 고수준 예외에 실어 보내는 방식
고수준의 예외를 만들고 예외 연쇄용으로 설계된 상위 클래스의 생성자에 원인을 건네주면 최종적으로 Throwable 생성자까지 건네진다.
class HigherLevelException extends Exception {
HigherLevelException(Throwable cause) {
super(cause);
}
}
그러면 예외 연쇄를 이용해서 저수준 예외를 고수준 예외에 실어보낼 수 있다.
try {
... // 저수준 추상화를 이용
} catch (LowelLevelException cause) {
throw new HigherLevelException(cause);
}
예외를 연쇄하지 않는다면 다음의 경우를 쓸 수 있다.
public E get(int index) {
ListIterator<E> i = listIterator(index);
try {
return i.next();
} catch (NoSuchElementException e) {
// 저수준 예외를 고수준으로
throw new IndexOutOfBoundsException("인덱스: " + index);
}
}
대부분 표준 예외는 예외 연쇄용 생성자를 갖추고 있고 그렇지 않은 예외라도 Throwable의 initCause 메소드를 이용해 원인을 입력할 수 있다. 예외 옌쇄는 문제의 원인을 getCause 메소드로 프로그램에 접근하게 해주고 원인과 고수준 예외의 스택 추적 정보를 잘 통합해준다.
initCause 메소드는 원인을 입력받는다. 현재 예외의 원인이 자신과 같지 않다는 첫 번째 조건문은 이미 cause가 입력된 경우다.
처음 초기화할 때 this == this.cause 이지만, initCause를 한 번이라도 실행했거나 생성자를 통해 Throwable 파라미터를 전달했다면 이 메소드를 실행할 수 없어진다. 그리고 입력하려는 원인은 현재 예외와 달라야한다.
getCause 메소드는 이 예외에 입력된 원인을 가져온다. 이 때, 원인 cause를 initCause나 Throwable 파라미터 생성자로 초기화하지 않았다면 null을 반환하게된다.
무턱대고 예외를 전파하는 것보다 예외 번역이 우수하지만, 그렇다고 남용해선 안된다.
가능한 저수준 메소드가 반드시 성공하게 하여 아래 계층에서 예외가 발생하지 않게 하자. 불가피하다면 아래 방법을 이용하자.
- 상위 계층 메소드의 매개변수 값을 아래 계층 메소드로 건네기 전 미리 검사하는 방법
- 상위 계층에서 예외를 조용히 처리해 API 호출자에게 전파하지 않는 대신 예외는 적절한 로깅을 남기는 방법
아래 계층의 예외를 예방하거나 스스로 처리할 수 없고 상위 계층에 그대로 노출하기 곤란하면 예외 번역을 사용하자.
이 때 예외 연쇄를 이용하면 상위 계층의 맥락에 맞는 고수준 예외를 던져 원인도 함께 전달해주자.
Item 74. 메소드가 던지는 모든 예외를 문서화하라
1. 검사 예외는 항상 따로따로 선언하고 각 예외가 발생하는 상황을 자바독의 @throws 태그를 써 정확히 문서화하자
메소드가 던지는 예외는 메소드를 올바르게 사용하는 데 중요한 정보다. 인터페이스에서는 이 조건이 일반 규약에 속하게 되니 특히 더 중요하다. 모든 구현체가 일관되게 동작해야하니까!
2. 메소드가 던질 수 있는 예외를 각각 @throws 태그로 문서화하되, 비검사 예외는 메소드 선언의 throws 목록에 넣지말자
/**
* 검사예외
* @throws IOException
*/
public void checkedException() throws IOException {
...
}
/**
* 비검사예외
* @throws ArrayIndexOutOfBoundsException
*/
public void unCheckedException() {
...
}
// 대표적 예: BufferedOutputStream의 write(), List의 contains()
하지만 비검사 예외가 현실적으로 불가능할 수 있는데 클래스가 수정되며 새로운 비검사 예외를 던지면 소스와 바이너리 호환성이 그대로 유지되는 게 이유다. 내 메소드가 외부 클래스를 가져다쓴다면 내 메소드 문서에 언급되지 않아 새로운 비검사 예외를 전파하게된다.
3. 한 클래스에 정의된 많은 메소드가 같은 이유로 같은 예외를 던지면 그 예외를 클래스 설명에 추가하자
ex) 클래스의 모든 메소드는 인수로 null이 넘어오면 NPE를 던진다
예외를 문서화하지 않으면 다른 사람이 클래스나 인터페이스를 효과적으로 사용하기 어렵거나 불가능할 수 있으니 반드시 문서화하자.
* 위 글은 EffectiveJava 3/E 책 내용을 정리한 글로, 저작권 관련 문제가 있다면 댓글로 남겨주시면 즉각 삭제조치 하겠습니다.