2 Mar 2019

Effective Java 10 - 예외

예외


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

try {
    int i = 0;
    while (true) {
        range[i++].climb();
    }
} catch (ArrayIndexOutOfBoundsException e) {

}

위의 코드는 무한 루프를 통해 배열을 순회 후 예외가 발생하면 끝을 내는 코드이다. 이렇게 예외를 일상적인 제어 흐름으로 쓰여선 안된다. 예외는 오직 예외 상황에서만 쓰라고 설계된 것이다. 특히 try-catch 문 안에 넣으면 JVM이 적용할 수 있는 최적화가 제한될 뿐만 아니라 코드를 헷갈리게 하고 성능을 더 떨어뜨린다. 또한 흐름 제어에 쓰인 예외가 실제 버그를 숨겨 디버깅도 훨씬 어렵게 만들 수 있다.

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


70. 복구할 수 있는 상황에는 Checked 예외를, 프로그래밍 오류에는 Unchecked 예외를 사용하라.

자바는 문제 상황을 알리는 타입으로 Checked 예외, Unchecked 예외, Error 이렇게 세 가지를 제공하는데 언제 어떤 예외를 사용할지는 지침이 있다.

호출하는 쪽에서 복구하리라 여겨지는 상황이라면 checked 예외를 사용한다. 이 것이 checked 예외와 unchecked 예외를 구분하는 규칙이다. checked 예외를 던지면 호출자가 그 예외를 잡아 처리하거나 더 바깥으로 전달하도록 강제하게 된다. 달리 말하면 API 설계자는 사용자에게 checked 예외를 던져 그 상황에서 복구하라고 요구하는 것이다.

unchecked 예외와 Error는 프로그램에서 잡을 필요가 없거나 통상적으로 잡지 말아야 한다. 프로그램에서 이를 던졌다는 것은 복구가 불가능하거나 더 실행해봐야 득이 없다는 뜻이다.

프로그래밍 오류에는 unchecked 예외를 사용해야 한다. 보통 이런 예외는 전제조건을 만족하지 못했을 경우에 발생한다. 단순히 클라이언트가 API의 명세에 기록된 제약을 지키지 못했을 경우에 발생한다는 것이다.

어떤 문제가 발생했을 때, 이 상황이 복구할 수 있는지 아니면 프로그래밍 오류인지가 명확히 구분되지는 않는다. 예를 들어 말도 안되는 크기의 배열을 할당해 생긴 프로그래밍 오류일 수도 있고, 자원이 부족해서 발생한 것일 수도 있는 것이다. 만약 자원이 일시적으로만 부족하거나 순간적으로 몰린 것이라면 충분히 복구할 수 있는 상황일 것이다. 따라서 이런 상황이 복구할 수 있는 것인지는 API 설계자의 판단에 달렸다. 복구할 수 있다고 생각되면 checked 예외를, 아니면 unchecked 예외를 사용해야 한다.

Error는 보통 JVM이 자원 부족, 불변식 깨짐으로 인해 더 이상 수행할 수 없을 경우에 발생한다. 그러나 Error는 애플리케이션이 던지는 것도 아니고 잡아서도 안된다. 애플리케이션 입장에서는 복구가 가능한 것도 아니기 때문이다. 따라서 Error 클래스를 상속하여 사용하는 일은 없어야 한다.

Exception, RuntimeException, Error를 상속하지 않는, 예외의 최상위 클래스인 throwable도 만들 수 있다. 이 것은 암묵적으로 checked 예외처럼 다루지만 사용해봤자 이로울 게 없다. throwable은 정상적인 checked 예외와 나을게 없으면서도 API 사용자를 헷갈리게만 한다.

예외도 역시 어떤 메서드라도 정의할 수 있는 완벽한 객체이다. 예외의 메서드는 주로 그 예외를 일으킨 상황에 관한 정보를 코드 형태로 전달하는 데 쓰인다. Checked 예외는 일반적으로 복구할 수 있는 조건에 발생하는데, 호출자가 이 예외 상황에서 벗어날 수 있도록 충분한 정보를 제공하는 것이 중요하다.


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

Checked 예외를 싫어하는 자바 프로그래머가 많지만 제대로 활용하면 API와 프로그램의 질을 높일 수 있다. 결과를 코드로 반환하거나 unchecked 예외를 던지는 것과는 달리, checked 예외는 발생한 문제를 프로그래머가 처리하도록 하여 안정을 높이게끔 해준다. 물론 checked 예외를 과하게 사용하면 오히려 쓰기 불편한 API가 된다.

API를 제대로 사용해도 발생할 수 있는 예외이거나 프로그래머가 의미있는 조치를 취할 수 있는 경우라면 checked 예외를 사용하도록 한다. 둘 중 어디에도 속하지 않는다면 unchecked 예외를 써야 한다.


72. 표준 예외를 사용하라.

자신만의 라이브러리를 사용하기보다 표준 라이브러리를 활용하는 것처럼, 예외도 이미 있는 것을 재사용하는 것이 좋다. 자바 라이브러리는 대부분의 API에서 쓰기에 충분한 수의 예외를 미리 제공한다.

표준 예외를 재사용하면 얻는 게 많다. 그 중 최고는 다른 사람이 익히고 사용하기 쉬워진다는 점이다. 단, Exception, RuntimeException, Throwable, Error는 직접 재사용하지 말아야 한다. 이 클래스들은 추상 클래스라고 생각해야 한다. 이런 예외들은 다른 예외들의 상위 클래스라서 여러 성격들의 예외를 포괄하여 안정적으로 테스트할 수 없다.

예외 주요 쓰임
IllegalArgumentException 허용하지 않는 값이 인수로 건네졌을 때, 단 null은 NullPointerException으로 처리
IllegalStateException 객체가 메서드를 수행하기에 적절하지 않은 상태
NullPointerException null을 허용하지 않는 메서드에 null을 넘겼을 경우
IndexOutOfBoundsException 인덱스가 범위를 넘어섰을 때
ConcurrentModificationException 허용하지 않는 동시 수정이 발견되었을 때
UnsupportedOperationException 호출한 메서드를 해당 객체가 지원하지 않을 경우

위에서 IllegalArgumentException과 IllegalStateException 예외의 주요 쓰임이 상호 배타적이 아니라서 헷갈릴 때가 있는데, 일반적인 규칙은 이렇다. 인수 값이 무엇이든 상관없이 실패했을 경우라면 IllegalStateException을, 그렇지 않다면 IllegalArgumentException을 사용하도록 한다.

만약 어떤 예외에 대한 정보를 더 제공하고 싶다면 이런 예외를 확장해서 사용하면 된다.


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

수행하는 일과 상관없는 예외가 튀어나오면 당황스럽다. 메서드가 SQLException과 같은 저수준 예외를 처리하지 않고 바깥으로 전파해버릴 때 종종 일어나는 일이다. 이는 내부 구현 방식을 드러낼 뿐만 아니라 윗 계층을 오염시킨다. 특히 윗 계층에서는 전혀 상관없는 예외를 처리하기 위해 알 필요도 없는 클래스나 인터페이스, 메서드를 사용해야 될 수도 있다.

이 문제를 피하기 위해서는 각 계층에서는 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꿔 던져야 한다. 이를 예외 번역(Exception Translation) 이라고 한다.

try {
    ...
} catch (LowerLevelException e) {
    throw new HigherLevelException(...);
}

예외를 번역할 때, 저수준의 예외가 디버깅에 도움이 된다면 예외 연쇄(Exception Chaining)을 사용할 수 있다. 예외 연쇄란 문제의 근본 원인(clause)인 저수준 예외를 고수준 예외에 실어보내는 것이다. 그러면 필요할 때 Throwable의 getClause 메서드를 통해 저수준 예외를 꺼내어 참조할 수 있다.

try {
    ...
} catch (LowerLevelException e) {
    throw new HigherLevelException(e);
}

고주순 예외의 생성자에서는 상위 클래스의 생성자에게 이 저수준 예외를 건네주어 최종적으로 Throwable 생성자까지 건네지게 된다.

class HigherLevelException extends Exception {
    HigherLevelException(Throwable cause) {
        super(cause);
    }
}

무턱대로 예외를 전파하는 것보다는 예외 번역이 우수하지만 그렇다고 남용해서는 곤란하다. 가능하다면 저수준 메서드가 반드시 성공하도록 하게하여 아래 계층에서는 예외가 발생하지 않도록 하는 것이 최선이다. 아니면 상위 계층에서 그 예외를 조용히 처리하여 API 호출자에게 전파하지 않는 방법도 있다.


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

메서드가 던질 가능성이 있는 모든 예외는 문서화하는 것이 좋다. checked 예외는 unchecked 예외든, 어떤 메서드이든지 간에 모두 마찬가지이다. 문서화에는 자바 독의 @throws 태그를 통해 문서화할 수 있다.


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

예외를 잡지 못해 프로그램이 실패할 경우 자바 시스템은 그 예외의 스택 추적(stack trace) 정보를 자동으로 출력한다. 스택 추적은 예외 객체의 toString 메서드를 호출해서 얻는 문자열이다. 이런 정보는 예외 발생했을 당시의 상황을 캐치해낼 수 있는 유일한 정보이다. 따라서 실패 원인에 관한 정보를 가능한 예외 객체에 많이 담아 반환하는 일은 아주 중요하다. 특히 checked 예외일 경우, 클라이언트 쪽에서 정보를 참고하여 복구를 쉽게 할 수 있도록 별도의 접근자 메서드를 통해 정보에 접근하도록 하는 것이 좋다.

실패 순간을 포착하려면 발생한 예외와 관련된 모든 매개변수와 필드의 값을 실패 메시지에 담아야 한다. 관련된 데이터는 모두 담아야 겠지만 장황할 필요는 없다. 보통 문제를 분석하는 사람은 스택 추적 뿐만 아니라 관련 문서와 소스 코드도 들여다 보기 때문이다.

예외의 상세 메시지와 최종 사용자에게 보여줄 오류 메시지를 혼동해서는 안된다. 최종 사용자에게는 친절한 안내 메시지를 보여주어야 하고, 예외 메시지는 가독성보다는 담긴 내용이 중요하다.


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

작업 도중 예외가 발생하더라도 사용 중이던 객체는 여전히 정상적으로 사용할 수 있는 상태라면 더욱 멋질 것이다. 특히 checked 예외일 경우 클라이언트가 오류 상태를 복구할 수 있을테니 더욱 유용할 것이다. 호출된 메서드가 실패하더라도 그 객체는 메서드 호출 전 상태로 유지하는 것이 좋다. 이를 실패 원자적(failure-atomic)이라고 한다.

메서드를 실패 원자적으로 만드는 방법은 다양하다. 가장 간단한 방법은 객체를 불변으로 만드는 것이다. 불변 객체는 태생적으로 실패 원자적이다. 메서드가 실패하면 새로운 객체가 만들어질 수는 있지만 기존 객체가 불안정한 상태로 빠지는 일은 결코 없다.

가변 객체의 메서드를 실패 원자적으로 만드는 법은 작업 수행에 앞서 매개변수의 유효성을 검사하는 것이다. 객체의 내부 상태를 변경하기 전에 잠재적 예외의 가능성 대부분을 걸러낼 수 있는 방법이다. 이와 비슷하게, 실패할 가능성이 있는 모든 코드를, 객체의 상태를 바꾸는 코드보다 앞에 배치하는 방법도 있다.

실패 원자성을 얻는 세 번째 방법은 객체의 임시 복사본에서 작업을 수행 후, 작업이 완료되면 원래 객체와 교체하는 것이다. 마지막으로는 작업 도중 발생하는 실패를 가로채어 원래 상태로 되돌리는 복구 코드를 작성하는 방법도 있다.

실패 원자성은 항상 달성할 수 있는 것은 아니다. 특히 두 스레드가 동기화 없이 같은 객체를 동시에 수정한다면 객체의 일관성이 깨질 수 있다. 이 때 발생하는 ConcurrentModificationException 예외를 잡았다고 해서 그 객체가 여전히 쓸 수 있는 상태라고 봐서는 안된다.

Error는 애초에 애플리케이션 레벨에서 복구할 수 있는 것이 아니므로 실패 원자성을 고려할 필요가 없다.


77. 예외를 무시하지 말라.

API 설계자가 메서드 선언에 예외를 명시하고 문서화하는 것은 그 메서드를 사용할 때 적절한 조치를 취하라는 것이다. 예외 발생시 아무것도 하지 않고 무시할 경우 예외가 존재할 이유가 사라진다.


Tags:
Stats:
0 comments