EJ3E: 10장 예외 - WordyWeedyHiker/WordyWeedyHiker.github.io GitHub Wiki
10장. 예외
예외를 제대로 활용해서 프로그램의 가독성, 신뢰성, 유지보수성을 높이자. 예외를 효과적으로 활용하는 지침을 다룬다.
아이템 69. 예외는 진짜 예외 상황에서만 사용하라
요약
- 예외는 예외 상황에서 쓸 의도로 설계되었다.
- 추론에 의한 최적화 용도로 사용하지 말자. 오히려 성능을 악화시킨다.
- 실제로 성능이 좋아지더라도 자바 플랫폼이 꾸준히 개선되고 있다.
- 표준 관용구를 사용하자.
- 정상적인 제어 흐름에서 사용해서는 안된다.
- 실제 버그에 의한 예외를 정상적인 종료 상황으로 숨길 수 있다. (심각!!)
- 이를 프로그래머에게 강요하는 API를 작성해서는 안된다.
- API가 정상적인 제어 흐름에서 응답 값을 주지 않고, 예외를 발생시키면, 이 API를 사용하는 프로그래머에게 이런 방식을 강요하게 된다. 주의하자.
// 코드 69-1 예외를 완전히 잘 못 사용한 예 - 따라하지 말 것!
// 잘 못된 추론에 의해 직관적이 않게 예외를 이용해 loop를 종료하는 코드를 작성한 것이다.
try {
int i = 0;
while(true)
range[i++].climb();
} catch (ArrayIndexOutOfBoundsException e) {
}
// for-each loop
// 다음과 같은 표준적인 관용구로 작성하면 곧바로 이해가 된다.
for (Mountain m : range)
m.climb();
예외를 써서 루프를 종료한 코드를 작성하기까지 잘못된 추론 과정
다음은 잘못된 추론이다.
- JVM은 배열에 접근할 때마다 경계를 넘지 않는지 검사하는데,
- 접근할 때(배역에 index로 접근 : range[i++]), 경계 검사에서 실패하면, Runtime에 ArrayIndexOutOfBoundsException이 발생한다.
- 일반적인 반복문도 배열 경계에 도달하면 종료한다. 따라서 이 검사를(배열 경계 도달 여부) 반복문에도 명시하면(for-each loop) 같은 일이 중복된다.
- for-each loop에서는 for문에서 1차 검사를 하고, 통과한 후, for문 block에 배열에 접근하는 코드가 있으면, 중복해서 2차 검사를 한다.
하지만 추론에 의한 최적화는 하지말자.
- JVM에 입장에서 예외는 명확한 검사 만큼 빠르게 만들어야할 동기가 약하다. 예외는 예외상황에서만 발생하는 말그대로 매우 예외적으로 발생하기 때문이다.
- 즉 JVM이 최적화에 별로 신경쓰지 않았을 가능성이 크다.
- try-catch block 안에 코드는 JVM 최적화에 제한이 생긴다.
- 배열을 순회하는 표준 관용구는 앞서 설명한 중복 검사를 수행하지 않도록 JVM이 알아서 최적화해 없애준다.
API 설계
- '상태 의존적' 메서드를 제공하는 클래스는 '상태 검사' 메서도 함께 제공해야한다.
- 상태 의존적 메서드를 사용하는 클라이언트가 상태 검사 메서드를 사용하도록 유도해야한다. 즉, 상태 검사를 하기 위해 예외를 사용할 일이 없도록 메서드로 제공하는 것이다.
- Iterable interface의 next(),상태 의존적,메서드와 hasNext(),상태 검사,메서드가 해당된다.
- for-each loop 관용구가 내부적으로 hasNext() 메서드를 이용해서 검사한다.
상태검사 메서드 대신 사용할 선택지
- 상태검사
- 옵셔널, Optional (아이템 55), e.g. Optional.empty()
- 특정 값, e.g. null
상태검사, 옵셔널, 특정 값 중 하나를 선택하는 지침
- 외부 동기화 없이(synchronized 없이) 여러 쓰레드가 동시에 접근할 수 있거나(multi threading), 외부 요인으로 상태가 변할 수 있다면(concurrent), 옵셔널이나 특정 값을 사용한다.
- 상태 검사와 상태 의존적 메서드 호출 사이에 객체의 상태가 변할 수 있기 때문이다.
- Optional은 불변객체(Immutable Object)이기 때문에 Thread safe하다.
- 성능이 중요한 상황에서 상태 검사 메서드가 상태 의존적 메서드의 일부작업을 중복 수행한다면, 옵셔널이나 특정값을 선택한다.
- 그외의 경우에는 상태 검사 메서드 방식이 가독성과 버그 발견 측면에서 낫다.
- 반면 특정 값은 검사하지 않고, 지나쳐도 발견하기 어렵다.
- 감사없이 지나칠 경우, runtime에 NullPointerException이 발생하는데 상황을 명확하게 설명해주지 않는 예외이다.
- 실제 버그로 인한 NullPointerException 예외와 구별되지 않는다.
- 반면 특정 값은 검사하지 않고, 지나쳐도 발견하기 어렵다.
아이템 70. 복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라
요약
JAVA의 문제상황을 알리는 타입 : Throwable
- 검사 예외
- Exception
- 컴파일타임에 throws 또는 try-catch 구문으로 처리해야하는 예외
- 런타임 예외
- RuntimeException
- 에러
- Error
언제 무엇을 사용해야 하는지 참고할 지침
- 검사와 비검사 예외를 구분하는 기본 규칙
- 호출하는 쪽에서 복구하리라 여겨지는 상황이라면 검사 예외를 사용하라
- 검사 예외를 던지면, 호출자가 catch로 잡아서 처리하거나, 더 바깥으로 전파하도록 강제하게 된다.(컴파일타임)
- 예외를 잡기만 하고, 처리를 않아하는 것은 좋지 않다.(아이템 77)
- 비검사 Throwable은 런타임 예외와 에러, 두가지다.
- 프로그램에서 잡을 필요가 없거나 혹은 통상적으로 잡지 말아야한다.
- 복구 불가능하거나, 더 실행해봐야 득보다 실이 많은 경우
- 이 Throwable을 잡지 않은 쓰레드는 적절한 오류 메시지를 내뱉으며 중단된다.
프로그래밍 오류를 나타낼 때는 런타임 예외를 사용하자.
- 전제조건을 만족하지 못했을 때 발생한다.
- 전제조건 위배란?
- 클라이언트가 API의 명세에 기록된 제약을 지키지 못했다는 뜻
- e.g. ArrayIndexOutOfBoundsException : 배열의 인덱스 제약, 0 ~ '배열크기 - 1'
항상 명확히 구분되지 않는다.
- 확신하기 어렵다면 아마도 비검사 예외를 선택하는 편이 나을 것이다. (아이템 71)
에러(Error), 보통 JVM이 자원 부족, 불변식 깨짐 등 더이상 수행을 계속할 수 없는 상황을 나타낼때 사용한다.
- e.g. 자원 부족, OutOfMemoryError
- Error 클래스 상속해서 하위 클래스 만드는 일은 자제하자.
- 우리가 만드는 비검사 Throwable은 RuntimeException의 하위 클래스여야한다.
- Error는 상속하거나 throw문으로 던지는 일도 없어야한다.
Throwable은 직접 사용하지 말자.
- 자바는 Throwable을 일반적 검사 예외처럼 다루지만, 정상적인 검사 예외보다 나을것이 없고, API 사용자를 헷갈리게 한다.
예외 역시 메서드를 정의할 수 있는 완벽한 객체
- 예외에 정의된 메서드는 그 예외를 일으킨 상황에 관한 정보를 전달하는데 쓰인다.
- 이런 정보를 전달하는 메서드가 없다면 오류 메시지를 파싱해야한다. 대단히 나쁜 습관이다. (아이템 12)
- throwable 클래스는 대부분 오류 메시지 포맷을 상세히 기술하지 않는다.
- JVM이나 릴리즈에 따라서 오류 메시지 포맷이 계속 달라질 수 있다는 의미이다.
- 프로그래머가 오류 메시지를 파싱하는 경우, 코드가 깨지기 쉽고 동작하지 않을 수 있다.
검사 예외는 복구할 수 있는 조건일 때 발생하므로
- 호출자가 예외상황에서 벗어나는데 필요한 정보를 알려주는 접근자 메서드를 예외 클래스에 정의해서 함께 제공하는 것이 중요하다. (아이템 75)