본문 바로가기

Reading/Effective Java

[Effective-Java] Item 69~71. 예외는 진짜 예외 상황에만 사용하라, 복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라, 필요 없는 검사 예외 사용은 피하라

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



// 배열의 원소를 순회하는데, 무한루프를 돌다가 배열의 끝에 도달시 Exception으로 종료
try {
    int i = 0;
    while(true)
        range[i++].climb();
} catch (ArrayIndexoutOfBoundsException e) {

}

// 표준적인 관용구
for (Mountain m : range) {
    m.climb();
}

 

JVM은 배열에 접근 시 경계를 넘는지 검사하는데, 일반 반복문도 배열 경계에 도달 시 종료한다.

이 검사를 반복문에도 명시한다면 검증 로직이 중복되므로 하나를 생략한다고 생각할 수 있으나, 이는 잘못된 추론이다.

  1. 예외는 예외 상황에 쓸 용도로 설계되었기에, 구현자 입장에선 최적화에 별 신경쓰지 않았을 가능성이 크다.
  2. try-catch 블록 안에 넣으면 JVM이 적용할 수 있는 최적화가 제한된다.
  3. 배열 순회 표준 관용구는 JVM이 알아서 최적화해줘서 중복 검사를 수행하지 않는다.

예외를 사용한 배열의 순회는 속도가 표준 관용구보다 느리다. 책에서는 예외를 사용한 게 원소 100개에서 2배정도 느리다고 나왔으나, 직접 원소들로 테스트했을땐 눈에 띄는 성능차이가 발생하지 않았다. --> [ 작성한 테스트코드 / 사용한 클래스 ]

 

그리고 climb 메소드 내부에서 ArrayIndexoutOfBoundsException을 던지는 경우가 있다고 가정해보자. 표준 관용구라면 이 버그는 예외를 잡지않고 스택 추적 정보를 남기며 해당 스레드를 즉각 종료시킬 것이다. 반면 예외 사용 반복문은 엉뚱한 예외때문에 반복문이 종료될 수 있다. 즉, 반복문에 예외를 사용하면 장황하고 헷갈리며 성능도 떨어지고 엉뚱한 곳에서 발생한 버그를 숨긴다. 

 

예외는 오직 예외 상황에만 쓰고 일상적 제어 흐름용으로 쓰지말자.

성능이 개선되더라도 자바 플랫폼의 업데이트로 성능 우위가 변경될 수 있고, 숨겨진 버그와 어려워진 유지보수 문제만 계속될 뿐이다.

 

잘 설계된 API라면 클라이언트가 정상적인 제어 흐름에서 예외를 사용할 일이 없어야한다.

상태 의존적 메소드를 제공하는 클래스는 상태 검사 메소드도 함께 제공하자. ex) Iterator의 next(), hasNext() -> 표준 for 관용구 가능

Iterator가 hasNext를 제공하지 않았다면 위의 예외를 사용한 반복문과 유사하게 클라이언트가 작성했을 것이다.

상태 검사 메소드 이외 선택지

옵셔널, 특정 값(null 같은)을 사용할 수 있다.

  • 외부 동기화 없이 여러 스레드가 동시 접근 가능하거나 외부 요인으로 상태가 변경될 수 있는 경우(상태 검사와 상태 의존적 메소드 호출 사이 객체의 상태가 변할 수 있는 경우)
  • 성능이 중요한 상황에서 상태 검사 메소드가 상태 의존적 메소드의 작업 일부를 중복 수행하는 경우

이외에는 상태 검사 메소드 방식이 더 낫다. 호출을 잊었다면 상태 의존적 메소드가 예외를 던져 버그를 확실히 알 수 있다. 그러나 특정 값은 검사하지 않고 지나쳐도 발견하기 어렵다.

 

예외는 예외 상황에서만 쓸 의도로 설계됐으니 정상적 제어 흐름에서 사용하지 말고, 프로그래머에게 강요하는 API를 만들지도 말자.

 

 

 

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



각 예외별로 정리가 잘 되어있는 레퍼런스가 있다. 읽기 전에 한 번 읽어보면 도움이 될 것 같다.

 

 

Java 예외(Exception) 처리에 대한 작은 생각

일상생활에서도 기본적인 것은 고민하지 않고 습관처럼 사용하는 경우가 있다. 초급 개발자인 나에게 ‘예외(Exception)’이 바로 그런 것이었다. 처음 JAVA수업 때 강사님께 "왜 로직을 try문으로

www.nextree.co.kr

 

검사 예외(Checked Exception)

RuntimeException을 제외한 Exception과 하위클래스가 이에 속한다. 호출하는 쪽에서 복구하리라 여겨지는 상황에서 사용한다. 호출자가 그 예외를 catch로 잡아 처리하거나 더 바깥으로 전파하도록 강제하게 된다. 즉, API 사용자에게 상황을 회복하라고 요구하는 것이다.

비검사 예외(Unchecked Exception)

Error와 RuntimeException의 하위 클래스가 이에 속한다. 잡을 필요가 없거나 통상적으로 잡지 않아야한다. 복구가 불가능하거나 더 실행해봐야 득보다는 실이 많다. catch로 잡지 않은 스레드는 적절한 오류 메시지를 내뱉으며 중단된다.

검사 예외 vs 비검사 예외

복구할 수 있는 상황인지, 프로그래밍 오류인지 항상 명확히 구분될 순 없다. 확신하기 어렵다면 비검사 예외를 선택하는 편이 낫다.

ex) 자원 고갈은 큰 배열을 할당해 생긴 프로그래밍 오류거나, 진짜 자원이 부족해 발생한 문제일 수도 있다.API 설계자의 결정이 중요하다.

런타임 예외(Runtime Exception)

전제조건을 위배할 때(client가 API 명세에 기록된 제약을 못 지켰을 때) 발생하는 런타임 예외는 프로그래밍 오류를 나타낼 때 사용하자.

ex) 배열 인덱스는 [0, 배열크기 -1]인데, ArrayIndexOutOfBoundsException은 이 전제조건이 지켜지지 않았다는 뜻

에러(Error)

JVM의 자원 부족, 불변식 깨짐 등 더 이상 수행을 계속할 수 없는 상황이다. 업계에 널리 퍼진 규약에 따라 AssertionError를 제외한 Error 클래스를 상속해 하위 클래스를 만들지 말고 throw 문으로 직접 던지지도 말자.

throwable

암묵적으론 일반적 검사 예외처럼 다룬다. Exception과 Error의 상위 클래스이기 때문에 정상적 예외보다 나을 게 없고 API 사용자를 헷갈리게 만든다.

메소드로 정보 전달

throwable의 하위 클래스인 Exception과 Error는 생성자에서 상위 생성자를 호출하는 역할만 가지고있다. 따라서 throwable 대신 각 역할에 맞는 클래스를 호출해서 사용하는 게 맞다. 이와 별개로 예외의 메소드는 주로 그 예외를 일으킨 상황에 관한 정보를 코드 형태로 전달하는데 쓰여 이런 메소드가 없다면 오류 메시지를 파싱해 정보를 빼야하기에 좋지않다.

 

JVM이나 릴리스에 따라 포맷이 달라질 수 있기 때문에 throwable 클래스들은 대부분 오류 메시지 포맷을 상세히 기술하지 않는다. 따라서 메시지 문자열을 파싱해 얻은 코드는 깨지기 쉽고 다른 환경에서 동작하지 않을 수 있다. 따라서 있는 그대로 메소드를 호출해서 메시지나 다른 필드들을 사용하도록 하자. 예외 상황을 벗어나는데 필요한 정보를 알려주는 메소드를 함께 제공하는 것은 중요하다. 

 

복구할 수 있는 상황이면 검사 예외를 던지며 복구에 필요한 정보를 알려주는 메소드를 제공하자. 그리고 프로그래밍 오류라면 비검사 예외를 던지자. 애매하면 비검사 예외를 던지고 검사 예외도, 런타임 예외도 아닌 throwable은 정의하지도 말자.

 

 

 

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



결과를 코드로 반환하거나 비검사 예외를 던지는 것과 달리 검사 예외는 발생한 문제를 프로그래머가 처리해 안정성을 높이게 해준다.

catch 블록으로 예외를 잡아 처리하거나, 바깥으로 던져 문제를 전파하는 것 어느 것이든 API 사용자에게는 부담이된다.

그리고 검사 예외를 던지는 메소드는 스트림 안에서 사용할 수 없기에 부담은 더 커진다. [ 참고코드 ]

 

API를 제대로 사용해도 발생할 수 있는 예외거나, 프로그래머가 의미 있는 조치를 취할 수 있다면 사용해도 괜찮다. 하지만 둘 중 하나라도 해당하지 않으면 비검사 예외를 쓰자. API 사용자가 catch에서 아래와 같이 사용하는 것을 보자. 최선이 아니라면 비검사 예외를 써야한다.

 

} catch (CheckedException e) {
    throw new AssertionError();
}

} catch (CheckedException e) {
    e.printStackTrace();
    System.exit(1);
}

 

다른 검사 예외도 던지는 상황에서 또 다른 검사 예외를 추가하는 경우 catch문 하나 추가하는 선에서 끝이지만 단 하나라면 그 예외 때문에 try 블록을 추가해야 하고 스트림에서 직접 사용하지 못한다. 따라서 검사 예외를 회피할 방법을 찾아야한다.

 

1. 옵셔널 반환

검사 예외 대신 빈 옵셔널을 반환한다. 대신 예외를 사용하면 구체적인 예외 타입과 그 타입이 제공하는 메소드들을 통해 부가 정보를 제공할 수 있지만 예외가 발생한 이유를 알려주는 부가 정보를 담을 수 없다는 단점이 있다.

 

2. 검사 예외를 던지는 메소드를 2개로 쪼개 비검사 예외로 변경

예외가 던져질지 여부를 boolean 값으로 반환받고 비검사 예외를 던지면 된다.

 

// 검사 예외를 던지는 메소드
try {
    obj.action(args);
} catch (TheCheckedException e) {
    ... // 예외 대처
}

// 상태 검사 메소드와 비검사 예외를 던지는 메소드
if (obj.actionPermitted(args)) {
    obj.action(args);
} else {
    ... // 예외 상황에 대처
}

// 메소드가 성공할 걸 알고 실패 시 스레드 중단을 원할 경우
obj.action(args);

 

모든 상황에 적용할 수 없지만 검사 예외보다 유연하다. 상태 검사 메소드는 69에서 말한 단점처럼 외부 동기화 없이 여러 스레드가 접근 가능하거나, 외부 요인에 의해 상태가 변경될 수 있거나, 상태 의존적 메소드의 작업을 중복 수행하면 성능상 손해니 주의해서 사용하자.

 

API 호출자가 예외 상황에서 복구할 방법이 없다면 비검사 예외, 복구가 가능하고 호출자가 처리해주길 바란다면 옵셔널을 고민 후 충분한 정보를 제공할 수 없다면 검사 예외를 던지자.

 

 

 

* 위 글은 EffectiveJava 3/E 책 내용을 정리한 글로, 저작권 관련 문제가 있다면 댓글로 남겨주시면 즉각 삭제조치 하겠습니다.