Link
Today
Total
10-17 00:15
Archives
관리 메뉴

초보개발자 긍.응.성

(이펙티브 자바 3) 10장 예외 정리 본문

책 정리/이펙티브 자바 3

(이펙티브 자바 3) 10장 예외 정리

긍.응.성 2020. 12. 7. 23:33
반응형

예외를 제대로 활용한다면 프로그램의 가독성, 신뢰성, 유지보수성이 높아지지만, 잘못 사용하면 반대의 효과만 나타난다. 예외를 효과적으로 활용하는 지침들에 대해 배워보자.

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

예외는 그 이름이 말해주듯 오직 예외 상황에서만 써야 한다. 절대로 일상적인 제어 흐름용으로 쓰여선 안 된다. 잘 설계된 API라면 클라이언트가 정상적인 제어 흐름에서 예외를 사용할 일이 없게 해야 한다. 특정 상태에서만 호출할 수 있는 '상태 의존적' 메서드를 제공하는 클래스는 '상태 검사' 메서드도 함께 제공해야 한다. 예시로 Iterator 인터페이스의 hasNext와 next메서드가 각각 상태 검사와 상태 의존적 메서드이다. for-each도 내부적으로 hasNext를 사용한다)

상태 검사 메서드 대신 사용할 수 있는 메서드는 Optional이나 null 같은 특수값을 반환하는 방법이다. 이의 쓰임 방법은 아래와 같다.

  1. 외부 동기화 없이 여러 스레드가 동시에 접극할 수 있거나 외부 요인으로 상태가 변할 수 있다면 Optional이나 특정 값을 사용한다.
  2. 성능이 중요한 상황에서 상태 검사 메서드가 상태 의존적 메서드의 작업 일부를 중복 수행한다면 옵셔널이나 특정 값을 선택한다.
  3. 상태 검사 메서드 호출을 깜빡 잊었다면 상태 의존적 메서드가 예외를 던져 버그를 확실히 드러낼 것이다. 반면 특정 값은 검사하지 않고 지나쳐도 발견하기가 어렵다.

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

자바의 throwable에는 검사 예외(Exception), 런타임 예외(RuntimeException), 에러(Error)가 존재한다.

호출하는 쪽에서 복구하리라 여겨지는 상황이라면 검사 예외를 사용하라. 검사 예외는 API 사용자에게 검사 예외를 던져주어 그 상황에서 회복해내라고 요구한 것이다. 검사 예외는 일반적으로 복구할 수 있는 조건일 때 발생한다. 따라서 호출자가 예외 상황에서 벗어나는데 필요한 정보를 알려주는 메서드를 함께 제공하는 것이 중요하다.

비검사 thowable의 두 종류로 런타임 예외와 에러가 존재한다. 이 둘은 프로그램에서 잡을 필요가 없거나 혹은 통상적으로 잡지 말아야 한다. 이런 throwable을 잡지 않은 스레드는 적절한 오류 메서드를 내뱉으며 중단된다.

  • 프로그래밍 오류를 나타낼 때는 런타임 예외를 사용하자
  • 에러는 보통 JVM이 자원부족, 불변식 깨짐 등 더 이상 수행을 계속할 수 없는 상황을 나타낼 때 사용한다. 그렇기에 우리가 구현하는 비검사 throwable은 모두 RuntimeException의 하위 클래스여야 한다. Error는 상속하지 말아야 할 뿐 아니라, throw 문으로 직접 던지는 일도 없어야 한다.

직접 throwable은 절대로 사용하지 말자. throwable 클래스들은 대부분 오류 메시지 포맷을 상세히 기술하지 않는데, 이는 JVM이나 릴리스에 따라 포맷이 달라질 수 있다는 뜻이다. 따라서 메시지 문자열을 파싱 해 얻은 코드는 깨지기 쉽고 다른 환경에서 동작하지 않을 수 있다.

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

검사 예외는 발생한 문제를 프로그래머가 처리하여 안정성을 높이게끔 해준다. 꼭 필요한 곳에만 사용한다면 검사 예외는 프로그램의 안정성을 높여준다. API 호출자가 예외 상황에서 복구할 방법이 없다면 비검사 예외를 던지자. 복구가 가능하고 호출자가 그 처리를 해주길 바란다면 옵셔널 반환도 생각하자.

표준 예외를 사용하라

표준 예외를 재사용하면

  • 여러분의 API가 다른 사람이 익히고 사용하기 쉬워진다.
  • 읽기 쉽게 된다.
  • 예외클래스 수가 적을수록 메모리 사용량도 줄고 클래스를 적재하는 시간도 적게 걸린다.

Exception, RuntimeException, Throwable, Error는 직접 재사용하지 말자. 이 클래스들은 추상 클래스라고 생각하자. 이 예외들은 다른 예외들의 상위 클래스이므로, 즉 여러 성격의 예외들을 포괄하는 클래스이므로 안정적으로 테스트할 수 없다.

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

메서드가 저수준 예외를 처리하지 않고 바깥으로 전파해버리면, 수행하려는 일과 관련 없어 보이는 예외가 튀어나올 수 있다. 이 문제를 피하려면 예외 번역을 사용하자.

예외 번역(exception translation): 상위 계층에서 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꾸어 던지는 것.

try {
	... // 저수준 추상화 이용.
} catch (LowerLevelException e) {
	throw new HigherLevelExcetpion(...);
}

예외 연쇄(exception chaining): 저수준 예외를 고수준 예외에 실어 보내는 방식. 저수준 예외가 디버깅에 도움이 될 때 사용한다.

try {
	... // 저수준 추상화 이용.
} catch (LowerLevelException cause) {
	// 저수준 예외를 고수준 예외에 실어 보낸다.
	throw new HigherLevelExcetpion(cause);
}

무턱대고 예외를 전파하는 것보다야 예외 번역이 우수한 방법이지만, 그렇다고 남용하는 것은 좋지 않다. 가능하면 저수준 메서드가 반드시 성공하도록 하여 아래 계층에서는 예외가 발생하지 않도록 하는 것이 최선이다. 때론 상위 계층 메서드의 매개변수 값을 아래 계층 메서드로 건네기 전에 미리 검사하는 방법으로 이 목적을 달성할 수 있다.

차선책으로, 아래 계층에서의 예외를 피할 수 없다면, 상위 계층에서 그 예외를 조용히 처리하여 문제를 API 호출자에까지 전파하지 않는 방법이 있다. 이 경우 발생한 예외는 적절한 로깅 기능을 활용하여 기록해두면 좋다.

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

검사 예외는 항상 따로 선언하고, 각 예외가 발생하는 상황을 자바독의 @throws 태그를 사용하여 정확히 문서화하라. 검사 예외는 메서드 선언의 throws 문에 일일이 선언하지만, 비검사 예외는 메서드 선언의 throws 목록에 넣지 말자. 검사냐 비검사냐에 따라 API 사용자가 해야 할 일이 달라지므로 이 둘을 확실히 구분해주는게 좋다. 한 클래스에 정의된 많은 메서드가 같은 이유로 같은 예외를 던진다면 그 예외를 클래스 설명에 추가하는 방법도 있다.

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

실패 순간을 포착하려면 발생한 예외에 관예된 모든 매개변수와 필드의 값을 실패 메시지에 담아야 한다(비밀번호나 암호 키 같은 정보는 담아선 안된다). 예외의 상세 메시지와 최종 사용자에게 보여줄 오류 메시지를 혼동해서는 안 된다. 최종 사용자에게는 친절한 안내 메시지를 보여줘야 하는 반면, 예외 메시지는 가독성보다는 담긴 내용이 훨씬 중요하다.

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

실패 원자적 (failure-atomic) - 일반화해 이야기하면, 호출된 메서드가 실패하더라도 해당 객체는 메서드 호출 전 상태를 유지해야한다.

메서드를 실패 원자적으로 만드는 방법은 아래와 같다.

  • 불변 객체로 설계하라
  • 가변 객체의 메서드일 경우 작업 수행에 앞서 매개변수의 유효성을 검사하라
  • 실패할 가능성이 있는 모든 코드를, 객체의 상태를 바꾸는 코드보다 앞에 배치하라
    • TreeMap의 put시 원소가 들어갈 위치를 찾기 전 타입을 검사하여 ClassCastException처리
  • 임시 복사본에서 작업을 수행한 다음, 작업이 성공적으로 완료되면 원래 객체와 교체하라
    • 정렬 메서드에서 새로운 배열에 옮겨 담아 정렬 후 교체
  • 작업 도중 발생하는 실패를 가로채는 복구 코드를 작성하여 작업 전 상태로 되돌린다.
    • 주로 (디스크 기반의) 내구성(durability)을 보장해야 하는 자료구조에 쓰임(자주 쓰이는 방법은 아니다)

메서드 명세에 기술한 예외라면 설혹 예외가 발생하더라도 객체의 상태는 메서드 호출 전과 똑같이 유지돼야 한다는 것이 기본 규칙이다. 이 규칙을 지키지 못한다면 실패 시의 객체 상태를 API 설명에 명시해야 한다.

예외를 무시하지 말라

예외를 무시하지 말라. API 설계자가 메서드 선언에 예외를 명시하는 까닭은, 그 메서드를 사용할 때 적절한 조치를 취해달라고 말하는 것이다. 예외는 문제 상황에 잘 대처하기 위해 존재하는데 catch 블록을 비워두면 예외가 존재할 이유가 없어진다. 예외를 무시하기로 했다면 catch 블록 안에 그렇게 결정한 이유를 주석으로 남기고 예외 변수 이름도 ignore로 바꿔놓도록 하자

웬만한 경우가 아니면 예외를 무시하지 않도록 하자. 예측할 수 있는 예외 상황이든 프로그래밍 오류든, 빈 catch 블록으로 못 본 척 지나치면 그 프로그램은 오류를 내재한 채 동작하게 된다. 그러나 어느 순간 문제의 원인과 아무 상관없는 곳에서 갑자기 죽어버릴 수도 있다. 예외를 적절히 처리하면 오류를 완전히 피할 수도 있다. 무시하지 않고 바깥으로 전파되게만 놔둬도 최소한 디버깅 정보를 남긴 채 프로그램이 신속히 중단되게는 할 수 있다.

반응형
Comments