최근에 Java로 프로젝트를 진행하면서 지금까지 예외를 잘못된 방법으로 사용하고 있었다는 것을 인지했다. Effective Java를 읽은 후 지향해야 될 Exception 전략을 정리해본다.
예외는 진짜 예외 상황에만 사용하라
- 잘못된 상황의 예외 사용은
- JVM의 최적화를 방해한다.
- 실제 예외 상황 발생에 대한 디버깅이 하기 힘들어진다.
- 잘 설계된 API라면 클라이언트가 정상적인 제어 흐름에서 예외를 사용할 일이 없게 해야 한다.
- ‘상태 의존적’ 메서드를 제공하는 클래스는 ‘상태 검사’ 메서드(또는 옵셔널이나 특수한 값)도 함께 제공하는 것이 좋다.
- 멀티 스레딩으로 상태 검사 메서드와 상태 의존적 메서드 호출 사이에 객체의 상태가 변할 수 있는 경우 옵셔널이나 특정 값을 사용한다.
- 성능이 중요한 상황에서 상태 검사 메서드가 상태 의존적 메서드의 작업의 일부를 중복 수행한다면 옵셔널이나 특정 값을 선택한다.
- 다른 모든 경우에는 상태 검사 메서드 방식이 낫다. 가독성이 살짝 더 좋고, 잘못 사용했을 때 발견하기가 쉽다.
- ‘상태 의존적’ 메서드를 제공하는 클래스는 ‘상태 검사’ 메서드(또는 옵셔널이나 특수한 값)도 함께 제공하는 것이 좋다.
복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라
- 호출하는 쪽에서 복구하리라 여겨지는 상황이라면 검사 예외를 사용한다.
- 프로그래밍 오류를 나타낼 때는 런타임 예외를 사용하자. 런타임 예외는 대부분 전제조건을 만족하지 못했을 때 발생한다.(ArrayIndexOutOfBoundsException)
- 검사 예외도 아니고 런타임 예외도 아닌 throwable은 정의하면 안된다.
필요 없는 검사 예외 사용은 피하라
- 검사 예외는 발생한 문제를 프로그래머가 처리하여 안정성을 높이게 도와준다.
- 하지만 과하게 사용하면 오히려 쓰기 불편한 API가 된다.
- API를 제대로 사용할 때도 발생할 수 있는 예외거나, 프로그래머가 의미 있는 조치를 취할 수 있을 경우에만 사용해야된다.
- 검사 예외를 피하는 방법
- 옵셔널을 반환한다. 하지만 이 방법은 예외가 발생한 이유를 알려주는 부가 정보를 담을 수 없다.
- 상태 검사 메서드와 비검사 예외를 던지는 메서드로 쪼갠다. 하지만 이 방법은 모든 상황에 적용할 수는 없다.
표준 예외를 사용하라
- 표준 예외를 사용한 API가 다른 사람이 익히고 사용하기 쉬워진다.
- 예외 클래스 수가 적을수록 메모리 사용량도 줄고 클래스를 적재하는 시간도 적게 걸린다.
- 널리 재사용되는 표준 예외
IllegalArgumentException
: 허용하지 않은 값이 인수로 건네졌을 때(null은 따로NullPointerException
으로 처리)IllegalStateException
: 객체가 메서드를 수행하기에 적절하지 않은 상태일 때NullPointerException
: null을 허용하지 않는 메서드에 null을 건넸을 때IndexOutOfBoundsException
: 인덱스가 범위를 넘어섰을 때ConcurrentModificationException
: 멀티 스레드 환경에서 허용하지 않는 동시 수정이 발견됐을 때UnsupportedOperationException
: 호출한 메서드를 지원하지 않을 때
IllegalArgumentException
과IllegalStatementException
중에 고민이 되는 경우가 있다. 이럴 때는, 인수 값이 무엇이든 어차피 실패했을거라면IllegalStateException
을, 그렇지 않으면IllegalArgumentException
을 던진다.
추상화 수준에 맞는 예외를 던져라
- 아래 게층의 예외를 예방하거나 스스로 처리할 수 없고, 그 예외를 상위 계층에 그대로 노출하기 곤란하면 예외 번역을 사용하라.
- 예외 연쇄를 이용하면 상위 계층에는 맥락에 어울리는 고수준 예외를 던지면서 근본 원인도 함께 알려주어 오류를 분석하기에 좋다
try {
... // 저수준 추상화를 이용한다.
} catch (LowerLevelException cause) {
// 저수준 예외를 고수준 예외에 실어 보낸다.
throw new HigherLevelException(cause);
}
메서드가 던지는 모든 예외를 문서화하라
- 검사 예외는 항상 따로따로 선언하고, 각 예외가 발생하는 상황을 자바독의 @throws 태그를 사용하여 정확히 문서화하라
- 메서드가 던질 수 있는 예외를 각각 @throws 태그로 문서화하되, 비검사 예외는 메서드 선언의 throws 목록에 넣지마라
- 비검사 예외도 문서화하라고는 했지만 현실적으로 불가는할 때도 있다.
- 한 클래스에 정의된 많은 메서드가 같은 이유로 같은 예외를 던진다면 그 예외를 클래스 설명에 추가하는 방법도 있다.
예외의 상세 메시지에 실패 관련 정보를 담으라
- 사후 분석을 위해 실패 순간의 상황을 정확히 포착해 예외의 상세 메시지에 담아야 한다.
- 실패 순간을 포착하려면 발생한 예외에 관여된 모든 매개변수와 필드의 값을 실패 메시지에 담아야 한다.
가능한 한 실패 원자적으로 만들라
- 실패 원자적: 호출된 메서드가 실패하더라도 해당 객체는 메서드 호출 전 상태를 유지하는 것
- 작업 도중 예외가 발생해도 그 객체가 여전히 정상적으로 사용할 수 있는 상태의 코드가 안정적이다.
- 실패 원자적인 구현을 하는 방법은 다음 중 한가지를 사용할 수 있다.
- 불변 객체로 설계.
- 작업 수행에 앞서 매개변수의 유효성을 검사.
- 실패할 가능성이 있는 모든 코드를, 객체의 상태를 바꾸는 코드보다 앞에 배치하는 방법.
- 임시 복사본에서 작업을 수행한 다음, 작업이 성공적으로 완료되면 원래 객체로 교체. 데이터를 임시 자료구조에 저장해 작업하는 게 더 빠를 때 적용하기 좋은 방식이다.
- 작업 도중 발생하는 실패를 가로채는 복구 코드를 작성하여 작업 전 상태로 되돌리는 방법.
- 일패 원자적인 코드를 항상 달성할 수 있는 것은 아니다. 실패 원자적으로 만들 수 있더라도 비용이나 복잡도가 크면 구현하지 않는 경우도 있다.
예외를 무시하지 말라
- API 설계자가 메서드 선언에 예외를 명시하는 까닭은, 그 메서드를 사용할 때 적절한 조치를 취해달라는 뜻이다.
- try-catch로 예외를 잡은 후 아무일도 하지 않는 것은 좋지않다.
- 만약 예외를 무시해야할 때라면, 예외를 무시하는 이유를 주석으로 남겨두고 예외 변수의 이름도 ignored로 바꿔놓는다.
댓글남기기