반응형
[이펙티브 자바] 09. 예외
아이템 69. 예외는 진짜 예외 상황에만 사용하라
예외를 완전히 잘못 사용한 예 - 따라하지 말 것!
try {
int i = 0;
while(true) {
range[i++].climb();
}
} catch (ArrayIndexOutOfBoundsException e) {
}
- 직관적이지 않다.
- 예외를 써서 루프를 종료하는 이상한 방식으로 구현
다음과 같이 표준 관용구대로 작성했다면 누구나 쉽게 이해했을 것이다.
for(Mountain m : range)
m.climb();
예외를 써서 루프를 종료한 이유는 뭘까?
- 잘못된 추론을 근거로 성능을 높여보려 한 것
- JVM은 배열에 접근할 때마다 경계를 넘지 않는지 검사
- 일반적인 반복문도 배열 경계에 도달하면 종료(경계를 넘지 않는지 검사)
-> 따라서 이 검사를 반복문에도 명시하면 같은 일이 중복 되리라 판단하여 하나를 생략한 것이다.
잘못된 추론인 이유
- 예외는 예외 상황에 쓸 용도로 설계되었으므로 명확한 검사만큼 빠르게 만들어야 할 동기가 약하다.(즉, 예외처리는 최적화에 신경 쓰지 않았을 가능성이 크다.)
- 코드를 try-catch 블록 안에 넣으면 JVM이 적용할 수 있는 최적화가 제한된다.
- 배열을 순회하는 표준 관용구는 앞서 걱정한 중복 검사를 수행하지 않는다. JVM이 알아서 최적화해 없애준다.
추론과는 다르게 예외를 사용한 쪽이 표준 관용구보다 훨씬 느리다.
잘못된 예외 사용으로 인한 문제점
- 코드를 헷갈리게 하고 디버깅이 어렵다.
- 오히려 성능을 떨어뜨릴 가능성이 크다.
- 코드 자체가 제대로 동작하지 않을 수 있다.
- 예를 들어 위의 잘못된 코드에서 내부와 관련없는 배열을 사용하다가 ArrayIndexOutOfBoundsException을 일으켰을 경우, 정상적인 반복문 종료 상황으로 오해하고 넘어갈 것이다.
교훈
1. 예외는 오직 예외 상황에서만 써야 한다. 절대로 일상적인 제어 흐름용으로 쓰여선 안 된다.
- 섣부른 최적화를 하지말아야 한다. 당장 성능이 더 좋을 수 있어도 자바가 계속 업그레이드 되며 성능 우위는 영원하지 않을 것이다.
- 과하게 영리한 기법에 숨겨진 미묘한 버그로 인해 유지보수 문제는 계속 이어질 것이다.
2. 잘 설계된 API라면 클라이언트가 정상적인 제어 흐름에서 예외를 사용할 일이 없게 해야 한다.
- 특정 상태에서만 호출할 수 있는 '상태 의존적 메서드'를 제공하는 클래스는 '상태 검사 메서드'도 함께 제공해야 한다.
- Ex. Iterator 인터페이스의 next : '상태 의존적 메서드', hasNext : '상태 검사 메서드'
- 만약 hasNext가 제공되지 않는다면, 위와 같이 예외를 통해 종료시켜야 할 것이다.
- '상태 검사 메서드' 외의 선택지도 있다. '빈 옵셔널' 혹은 'null과 같은 특정 값'을 통해 반환하는 방법이다.
'상태 검사 메서드', '옵셔널', '특정 값'의 선택 기준
1. '옵셔널'이나 '특정 값' 선택
- '상태 검사 메서드'와 '상태 의존적 메서드' 호출 사이에 객체의 상태가 변할 수 있는 경우 - 여러 스레드가 동시 접근, 외부 요인으로 상태 변할 수 있는 경우
- 성능이 중요한 상황에서 '상태 검사 메서드'가 '상태 의존적 메서드'의 작업 일부를 중복 수행
2. 그 외 모든 경우에는 '상태 검사 메서드' 방식 사용
- 가독성이 좋고, 오류를 발견하기 쉽다.
아이템 70. 복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라
자바는 문제 상황을 알리는 타입(throwable)으로 검사 예외, 런타임 예외, 에러 이렇게 세가지를 제공한다.
검사 예외 - 호출하는 쪽에서 복구하리라 여겨지는 상황일 때
- 호출자가 예외를 catch로 잡아 처리하거나 더 바깥으로 전파하도록 강제한다.
- 호출자가 예외 상황에서 벗어나는데 필요한 정보를 알려주는 메서드를 함께 제공해야 한다.
- 메서드 선언에 포함된 검사 예외는 그 메서드를 호출했을 때 발생할 수 있는 유력한 결과임을 API 사용자에게 알려준다.
비검사 예외 - 런타임 예외와 에러, 프로그램에서 잡을 필요가 없거나 혹은 잡지 말아야 한다.
- 프로그램에서 비검사 예외나 에러를 던졌다는 것은 복구가 불가능하거나 더 실행해봐야 득보다는 실이 많다는 뜻
- 이런 throwable을 잡지 않은 스레드는 적절한 오류 메시지를 내뱉으며 중단한다.
- 프로그래밍 오류를 나타낼 때는 런타임 예외를 사용하자.
- 런타임 예외의 대부분은 전제조건을 만족하지 못했을 때 발생한다.
- 에러는 보통 JVM이 자원 부족, 불변식 깨짐 등 더이상 수행을 계속 할 수 없는 상황을 나타낼 때 사용한다.
- 우리가 구현하는 비검사 throwable은 모두 RuntimeException의 하위 클래스여야 한다. Error는 상속하면 안될 뿐만 아니라 throw 문으로 직접 던지는 일도 없어야 한다.
아이템 71. 필요 없는 검사 예외 사용은 피하라
- 검사 예외는 발생한 문제를 프로그래머가 처리하여 안전성을 높일 수 있다.
- 그러나 API 사용자에게 처리에 대한 부담을 주고, 더구나 검사 예외를 던지는 메서드는 스트림 안에서 직접 사용할 수 없다.
- API를 제대로 사용해도 발생할 수 있는 예외이거나, 프로그래머가 의미 있는 조취를 취할 수 있는 경우에만 검사 예외를 사용하자
- 그 외에는 비검사 예외를 사용하자.
- 검사 예외가 메서드에 하나만 있다면 검사 예외를 던지지 않는 방법이 있는지 고민해보자.
검사 예외를 회피하는 가장 쉬운 방법
1. 적절한 결과 타입을 담은 옵셔널을 반환하는 것이다.
- 검사 예외를 던지는 대신 단순히 빈 옵셔널을 반환하면 된다.
- 이 방식의 단점은 예외가 발생한 이유를 알려주는 부가 정보를 담을 수 없다.
- 예외를 사용하면 구체적인 예외 타입과 그 타입이 제공하는 메서드들을 활용해 부가 정보를 제공할 수 있다.
2. 검사 예외를 던지는 메서드를 2개로 쪼개 비검사 예외로 바꿀 수 있다.
검사 예외를 던지는 메서드 - 리팩터링 전
try {
obj.action(args);
} catch(TheCheckedException e) {
... // 예외 상황에 대처한다.
}
상태 검사 메서드와 비검사 예외를 던지는 메서드 - 리팩터링 후
if(obj.actionPermitted(args)){
obj.action(args);
} else {
... // 예외 상황에 대처한다.
}
- 리팩터링 후의 API가 딱히 더 아름답지는 않지만 더 유연한것은 확실하다.
아이템 72. 표준 예외를 사용하라
- 다른 코드와 마찬가지로 예외도 재사용하는 것이 좋으며, 자바 라이브러리는 대부분 API에서 쓰기 충분한 수의 예외를 제공한다.
- 표준 예외를 재사용 하면 API를 다른 사람이 익히고 사용하기 쉬워진다는 것이다.
- 예외 클래스가 적을수록 메모리 사용량도 줄고 클래스를 적재하는 시간도 적게 걸린다.
- Exception, RuntimeException, Throwable, Error는 직접 재사용하지 말자. 이 클래스들을 추상 클래스라고 생각하길 바란다.
널리 재사용되는 예외들
예외 | 주요 쓰임 |
IllegalArgumentException | 허용하지 않는 값이 인수로 건네졌을 때 (null은 따로 NullPointerException으로 처리) |
IllegalStateException | 객체가 메서드를 수행하기에 적절하지 않은 상태일 때 |
NullPointerException | null을 허용하지 않는 메서드에 null을 건넸을 때 |
IndexOutOfBoundsException | 인덱스가 범위를 넘어섰을 때 |
ConcurrentModificationException | 허용하지 않는 동시 수정이 발견됐을 때 |
UnsupportedOperationException | 호출한 메서드를 지원하지 않을 때 |
- 상황에 부합한다면 항상 표준 예외를 재사용하자.
- API 문서를 참고해 예외가 어떤 상황에서 던져지는지 꼭 확인하고 이름과 맥락이 부합할 때만 재사용한다.
- 예외는 직렬화할 수 있어야 하기 때문에 표준 예외 확장시 충분한 고민을 해보자.
아이템 73. 추상화 수준에 맞는 예외를 던지라
- 메서드가 저수준 예외를 처리하지 않고 바깥으로 전파해버려 수행하려는 일과 관련 없어 보이는 예외가 나오면 당황스럽다.
- 이는 내부 구현 방식을 드러내어 윗 레벨 API를 오염시킨다.
- 다음 릴리스에서 구현 방식을 바꾸면 다른 예외가 튀어나와 기존 클라이언트 프로그램을 깨지게 할 수도 있다.
- 상위 계층에서는 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꿔 던져야 한다 - 예외 번역
예외 번역
try {
... // 저수준 추상화를 이용한다
} catch(LowerLevelException e){
// 추상화 수준에 맞게 번역한다.
throw new HigherLevelException(...);
}
- 예외를 번역할 때, 저수준 예외가 디버깅에 도움이 된다면 예외 연쇄를 사용하는 게 좋다.
- 예외 연쇄란 문제의 근본 원인인 저수준 예외를 고수준 예외에 실어 보내는 방식이다.
- 그러면 별도의 접근자 메서드(Throwable의 getCause 메서드)를 통해 칠요하면 언제든 저수준 예외를 꺼내 볼 수 있다.
예외 연쇄
try {
... // 저수준 추상화를 이용한다.
} catch (LowerLevelException cause){
// 저수준 예외를 고수준 예외에 실어 보낸다.
throw new HigherLevelException(cause);
}
예외 연쇄용 생성자
class HigherLevelException extends Exception {
HigherLevelException(Throwable cause) {
super(cause);
}
}
- 대부분의 표준 예외는 예외 연쇄용 생성자를 갖추고 있고, 그렇지 않더라도 Throwable의 initCause 메서드를 이용해 '원인'을 직접 못박을 수 있다.
- 무턱대고 예외를 전파하는 것보단 예외 번역이 우수하지만, 그렇다고 남용해서는 안된다.
- 가능하면 저수준 메서드가 반드시 성공하도록 하자.
- 매개변수를 미리 검사하거나, 상위계층에서 적절한 로깅을 활용해 예외를 조용히 처리할 수도 있다.
아이템 74. 메서드가 던지는 모든 예외를 문서화하라
- 검사 예외는 항상 따로따로 선언하고, 각 예외가 발생하는 상황을 자바독의 @throws 태그를 사용하여 정확히 문서화하자.
- 공통 상위 클래스 하나로 뭉뚱그려 선언하는 일은 삼가자.
- 비검사 예외도 검사 예외처럼 정성껏 문서화해두면 좋다.
- public 메서드라면 필요한 전제조건을 문서화해야 하며, 그 수단으로 가장 좋은 것이 바로 비검사 예외들을 문서화 하는 것이다.
- 메서드가 던질 수 있는 예외를 각 @throws 태그로 문서화하되, 비검사 예외는 메서드 선언의 throws 목록에 넣지 말자.
- 검사냐 비검사냐에 따라 API 사용자가 해야할 일이 달라지므로 이 둘을 확실히 구분해주자.
- 한 클래스에 정의된 많은 메서드가 같은 이유로 같은 예외를 던진다면 그 예외를 클래스 설명에 추가하자 - ex) NullPointerException
아이템 75. 예외의 상세 메시지에 실패 관련 정보를 담으라
- 예외를 잡지 못해 프로그램이 실패하면 자바 시스템은 그 예외의 스택 추적 정보를 자동으로 출력한다.
- 스택 추적은 예외 객체의 toString 메서드를 호출해 얻는 문자열로, 보통은 예외의 클래스 이름 뒤에 상세 메세지가 붙는 형태다.
- 사후 분석을 위해 실패 순간의 상황을 정확히 포착해 예외의 상세 메시지에 담아야 한다.
- 예외의 상세 메시지와 최종 사용자에게 보여줄 오류 메시지를 혼동하지는 말자.
실패 순간을 포착하는 방법
- 발생한 예외에 관여된 모든 매개변수와 필드의 값을 실패 메시지에 담아야 한다.
- 예를 들어 IndexOutOfBoundsException의 상세 메시지는 범위의 최솟값과 최댓값, 그리고 그 범위를 벗어났다는 인덱스의 값을 담아야한다.
- 필요한 예외 정보를 예외 생성자에서 모두 받아서 상세 메시지까지 미리 생성해놓는 방법도 괜찮다.
/**
* 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.upperBoudn = upperBound;
this.index = index;
}
아이템 76. 가능한 한 실패 원자적으로 만들라
- 호출된 메서드가 실패하더라도 해당 객체는 메서드 호출 전 상태를 유지해야 한다 - 실패 원자적
메서드를 실패 원자적으로 만드는 방법
1. 불변 객체로 설계한다.
- 불변 객체는 태생적으로 실패 원자적이다.
2. 가변 객체는 작업 수행에 앞서 매개변수의 유효성을 검사하자.
- 객체 내부 상태를 변경하기 전에 잠재적 예외의 가능성 대부분을 걸러낼 수 있는 방법이다.
- 실패 가능성이 있는 모든 코드를, 객체 상태 변경 코드 앞에 배치하자.
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
3. 객체의 임시 복사본에서 작업을 수행한 후, 작업이 성공하면 원래의 객체와 교체하자.
- List를 정렬하는 메서드에서 정렬을 수행하기 전에 배열로 옮겨담아 정렬하자.
4. 작업 도중 발생하는 실패를 가로채는 복구 코드를 작성하여 작업 전 상태로 되돌리자.
- 자주 쓰이는 방법은 아니다
아이템 77. 예외를 무시하지 말라
- API 설계자가 메서드 선언에 예외를 명시하는 까닭은, 그 메서드를 사용할 때 적절한 조취를 취해달라고 말하는 것이다.
// catch 블록을 비워두면 예외가 무시된다. 아주 의심스러운 코드다!.
try {
...
} catch(SomeException e){
}
- catch 블록을 비워두면 예외가 존재할 이유가 없어진다.
- 화재 경보를 꺼버려, 다른 누구도 화재가 발생했음을 알지 못하게 하는 것과 같다.
예외를 무시해도 되는 경우
- FileInputStream과 같은 경우는 예외를 무시해도 좋다. 입력 전용 스트림이므로 파일을 상태를 변경하지 않았으니 복구할 것이 없으며, 스트림을 닫는 다는 것은 필요한 정보는 이미 다 읽었다는 뜻이니 남은 작업을 중단할 이유도 없다.
- 예외를 무시하기로 했다면 catch 블록 안에 그렇게 결정한 이유를 주석으로 남기고 예외 변수의 이름도 ignored로 바꾸자.
int numColors = 4;
try {
numColors = f.get(1L, TimeUnit.SECONDS);
} catch (TimeoutException | ExecutionException ignored) {
// 기본 값을 사용한다(색상 수를 최소화하면 좋지만, 필수는 아니다.)
}
728x90
반응형
'스터디 > 이펙티브 자바' 카테고리의 다른 글
[이펙티브 자바] 11장 직렬화 (serializable) (0) | 2022.10.19 |
---|---|
[이펙티브 자바] 10. 동시성 (0) | 2022.10.12 |
[이펙티브 자바] 08. 일반적인 프로그래밍 원칙 (0) | 2022.09.27 |
[이펙티브 자바] 07. 메서드 (1) | 2022.09.21 |
[이펙티브 자바] 06. 람다와 스트림 (0) | 2022.09.14 |
댓글