최근에 Java로 프로젝트를 진행하면서 지금까지 예외를 잘못된 방법으로 사용하고 있었다는 것을 인지했다. Effective Java를 읽은 후 지향해야 될 Exception 전략을 정리해본다.

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

  • 잘못된 상황의 예외 사용은
    • JVM의 최적화를 방해한다.
    • 실제 예외 상황 발생에 대한 디버깅이 하기 힘들어진다.
  • 잘 설계된 API라면 클라이언트가 정상적인 제어 흐름에서 예외를 사용할 일이 없게 해야 한다.
    • ‘상태 의존적’ 메서드를 제공하는 클래스는 ‘상태 검사’ 메서드(또는 옵셔널이나 특수한 값)도 함께 제공하는 것이 좋다.
      • 멀티 스레딩으로 상태 검사 메서드와 상태 의존적 메서드 호출 사이에 객체의 상태가 변할 수 있는 경우 옵셔널이나 특정 값을 사용한다.
      • 성능이 중요한 상황에서 상태 검사 메서드가 상태 의존적 메서드의 작업의 일부를 중복 수행한다면 옵셔널이나 특정 값을 선택한다.
      • 다른 모든 경우에는 상태 검사 메서드 방식이 낫다. 가독성이 살짝 더 좋고, 잘못 사용했을 때 발견하기가 쉽다.

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

  • 호출하는 쪽에서 복구하리라 여겨지는 상황이라면 검사 예외를 사용한다.
  • 프로그래밍 오류를 나타낼 때는 런타임 예외를 사용하자. 런타임 예외는 대부분 전제조건을 만족하지 못했을 때 발생한다.(ArrayIndexOutOfBoundsException)
  • 검사 예외도 아니고 런타임 예외도 아닌 throwable은 정의하면 안된다.

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

  • 검사 예외는 발생한 문제를 프로그래머가 처리하여 안정성을 높이게 도와준다.
  • 하지만 과하게 사용하면 오히려 쓰기 불편한 API가 된다.
  • API를 제대로 사용할 때도 발생할 수 있는 예외거나, 프로그래머가 의미 있는 조치를 취할 수 있을 경우에만 사용해야된다.
  • 검사 예외를 피하는 방법
    • 옵셔널을 반환한다. 하지만 이 방법은 예외가 발생한 이유를 알려주는 부가 정보를 담을 수 없다.
    • 상태 검사 메서드와 비검사 예외를 던지는 메서드로 쪼갠다. 하지만 이 방법은 모든 상황에 적용할 수는 없다.

표준 예외를 사용하라

  • 표준 예외를 사용한 API가 다른 사람이 익히고 사용하기 쉬워진다.
  • 예외 클래스 수가 적을수록 메모리 사용량도 줄고 클래스를 적재하는 시간도 적게 걸린다.
  • 널리 재사용되는 표준 예외
    • IllegalArgumentException: 허용하지 않은 값이 인수로 건네졌을 때(null은 따로 NullPointerException으로 처리)
    • IllegalStateException: 객체가 메서드를 수행하기에 적절하지 않은 상태일 때
    • NullPointerException: null을 허용하지 않는 메서드에 null을 건넸을 때
    • IndexOutOfBoundsException: 인덱스가 범위를 넘어섰을 때
    • ConcurrentModificationException: 멀티 스레드 환경에서 허용하지 않는 동시 수정이 발견됐을 때
    • UnsupportedOperationException: 호출한 메서드를 지원하지 않을 때
  • IllegalArgumentExceptionIllegalStatementException 중에 고민이 되는 경우가 있다. 이럴 때는, 인수 값이 무엇이든 어차피 실패했을거라면 IllegalStateException을, 그렇지 않으면 IllegalArgumentException을 던진다.

추상화 수준에 맞는 예외를 던져라

  • 아래 게층의 예외를 예방하거나 스스로 처리할 수 없고, 그 예외를 상위 계층에 그대로 노출하기 곤란하면 예외 번역을 사용하라.
  • 예외 연쇄를 이용하면 상위 계층에는 맥락에 어울리는 고수준 예외를 던지면서 근본 원인도 함께 알려주어 오류를 분석하기에 좋다
try {
	... // 저수준 추상화를 이용한다.
} catch (LowerLevelException cause) {
	// 저수준 예외를 고수준 예외에 실어 보낸다.
	throw new HigherLevelException(cause); 
}

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

  • 검사 예외는 항상 따로따로 선언하고, 각 예외가 발생하는 상황을 자바독의 @throws 태그를 사용하여 정확히 문서화하라
  • 메서드가 던질 수 있는 예외를 각각 @throws 태그로 문서화하되, 비검사 예외는 메서드 선언의 throws 목록에 넣지마라
  • 비검사 예외도 문서화하라고는 했지만 현실적으로 불가는할 때도 있다.
  • 한 클래스에 정의된 많은 메서드가 같은 이유로 같은 예외를 던진다면 그 예외를 클래스 설명에 추가하는 방법도 있다.

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

  • 사후 분석을 위해 실패 순간의 상황을 정확히 포착해 예외의 상세 메시지에 담아야 한다.
  • 실패 순간을 포착하려면 발생한 예외에 관여된 모든 매개변수와 필드의 값을 실패 메시지에 담아야 한다.

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

  • 실패 원자적: 호출된 메서드가 실패하더라도 해당 객체는 메서드 호출 전 상태를 유지하는 것
  • 작업 도중 예외가 발생해도 그 객체가 여전히 정상적으로 사용할 수 있는 상태의 코드가 안정적이다.
  • 실패 원자적인 구현을 하는 방법은 다음 중 한가지를 사용할 수 있다.
    • 불변 객체로 설계.
    • 작업 수행에 앞서 매개변수의 유효성을 검사.
    • 실패할 가능성이 있는 모든 코드를, 객체의 상태를 바꾸는 코드보다 앞에 배치하는 방법.
    • 임시 복사본에서 작업을 수행한 다음, 작업이 성공적으로 완료되면 원래 객체로 교체. 데이터를 임시 자료구조에 저장해 작업하는 게 더 빠를 때 적용하기 좋은 방식이다.
    • 작업 도중 발생하는 실패를 가로채는 복구 코드를 작성하여 작업 전 상태로 되돌리는 방법.
  • 일패 원자적인 코드를 항상 달성할 수 있는 것은 아니다. 실패 원자적으로 만들 수 있더라도 비용이나 복잡도가 크면 구현하지 않는 경우도 있다.

예외를 무시하지 말라

  • API 설계자가 메서드 선언에 예외를 명시하는 까닭은, 그 메서드를 사용할 때 적절한 조치를 취해달라는 뜻이다.
  • try-catch로 예외를 잡은 후 아무일도 하지 않는 것은 좋지않다.
  • 만약 예외를 무시해야할 때라면, 예외를 무시하는 이유를 주석으로 남겨두고 예외 변수의 이름도 ignored로 바꿔놓는다.

댓글남기기