10장. 예외 - GOAL

예외를 제대로 활용한다면 프로그램의 가독성, 신뢰성, 유지보수성이 높아진다.
그러나 잘못 사용하면 반대의 효과만 나타난다.
예외를 효과적으로 활용하는 방법을 습득하자.

아이템71. 필요 없는 검사 예외 사용은 피하라

검사 예외를 싫어하는 자바 프로그래머가 많지만, 제대로 활용하면 API와 프로그램의 질을 높일 수 있다.
결과를 코드로 반환하거나 비검사 예외를 던지는 것과 달리, 검사 예외는 발생한 문제를 프로그래머가 처리하여 안전성을 높이게끔 해준다. 하지만 문제가 있다.

검사 예외의 문제

  • 검사 예외를 남발하면 쓰기 불편한 API가 된다.
  • 어떤 메서드가 검사 예외를 던질 수 있다고 선언했다면, 이를 호출하는 코드에서는 catch 블록을 두어 그 예외를 붙잡아 처리하거나 더 바깥으로 던져 문제를 전파해야만 한다.
    어느쪽이든 API 사용자에게 부담을 준다.
  • 검사 예외를 던지는 메서드는 스트림 안에서 직접 사용할 수 없기 때문에 자바 8부터는 부담이 더욱 커졌다.
  • API를 제대로 사용해도 발생할 수 있는 예외이거나, 프로그래머가 의미 있는 조치를 취할 수 있는 경우라면 이 정도 부담쯤은 받아들일 수 있다.
    그러나 둘 중 어디에도 해당하지 않는다면 비검사 예외를 사용하는게 좋다.

참고. 비검사 예외의 사용

검사 예외, 비검사 예외 중 어느 것을 선택해야 할지는 프로그래머가 그 예외를 어떻게 다룰지 생각해보면 알 수 있다. 아래와 같은 상황에선 비검사 예외를 선택해야 한다.

} catch(TheCheckedException e){
    throw new AssertionError();  //일어날 수 없다.
}

// 이것도 최악이다. 
} catch(TheCheckedException e) {
    e.printStackTrace();
    System.exit(1);
}

검사 예외를 피하는 방법

1. 결과 타입을 담은 옵셔널을 반환하기

검사 예외를 던지는 대신 단순히 빈 옵셔널을 반환하면 된다.
이 방식의 단점은 예외가 발생한 이유를 알려주는 부가 정보를 담을 수 없다는 것이다.
반면, 예외를 사용하면 구체적인 예외 타입과 그 타입이 제공하는 메서드들을 활용해 부가 정보를 제공할 수 있다.

2. 검사 예외를 던지는 메서드를 2개로 쪼개 비검사 예외로 바꾸자

try-catch 블록을 boolean 값을 반환하는 메서드로 리팩터링할 수 있다. 아름다운 구조는 아니지만, 유연하다.

// 검사 예외를 던지는 메서드 - 리팩터링 전
try {
    obj.action(args);
} catch (TheCheckException e) {
  ... // 예외 상황에 대처한다.     
}

// 상태 검사 메서드와 비검사 예외를 던지는 메서드 - 리팩터링 후
if(obj.actionPermitted(args)) {
    obj.action(args);
} else {
    // 예외 상황에 대처한다.
}

/**
 *  프로그래머가 이 메서드가 성공하리라는 걸 안다거나,
 *  실패 시 스레드를 중단하길 원한다면 한 줄로 작성해도 무방하다.
 */
obj.action(args);

외부 동기화 없이 여러 스레드가 동시에 접근할 수 있거나, 외부 요인에 의해 상태가 변할 수 있다면 이 리팩터링은 적절하지 않다. actionPermitted와 action호출 사이에 객체의 상태가 변할 수 있기 때문이다.
또한 actionPermitted가 action 메서드의 작업 일부를 중복 수행한다면 성능에서 손해이니 적적하지 않을 수도 있다.

결론

  • 꼭 필요한 곳에만 사용한다면 검사 예외는 프로그램의 안전성을 높여주지만, 남용하면 쓰기 고통스러운 API를 낳는다.
  • API 호출자가 예외 상황에서 복구할 방법이 없다면, 비검사 예외를 던지자.
  • 복구가 가능하고 호출자가 그 처리를 해주길 바란다면, 우선 옵셔널을 반환해도 될지 고민하자.
  • 옵셔널만으로는 상황을 처리하기에 충분한 정보를 제공할 수 없을 때만 검사 예외를 던지자.