본문 바로가기

Reading/Effective Java

[Effective-Java] Item 75~77. 예외의 상세 메시지에 실패 관련 정보를 담으라, 가능한 실패 원자적으로 만들라, 예외를 무시하지 말라

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



실패 순간을 포착하려면 발생한 예외에 관여된 모든 매개변수와 필드의 값을 실패 메시지에 담아야한다

IndexOutOfBoundsException을 예로들면 상세 메시지로 범위의 최소와 최대값, 그리고 범위를 벗어난 인덱스를 모두 담으면 실패에 관한 정보를 얻기 쉽다. 최소와 최대가 예측이 되더라도 셋중 어떤 것이 잘못된지에 따라 현상이 다를 수 있다. 예를들어 내부 불변식이 깨져 최솟값이 최댓값보다 클수도 있다. 

 

 

인덱스를 파라미터로 넘기는 게 있는데 이는 자바 9에와서 추가된 것이고 아쉽게 최솟값과 최댓값까지 받진 않는다.

생성자에서 필요한 정보를 받아 상세 메시지까지 작성하게하여 아래와 같이 작성할 수도 있을 것이다.

 

public class IndexOutOfBoundsException extends RuntimeException {
    private final int lowerBound;
    private final int upperBound;
    private final int index;

    /**
     * IndexOutOfBoundsException을 생성한다.
     *
     * @param lowerBound 인덱스의 최솟값
     * @param upperBound 인덱스의 최댓값 + 1
     * @param index      인덱스의 실젯값
     */
    public IndexOutOfBoundsException(int lowerBound, int upperBound, int index) {
        super(String.format("최솟값: %d, 최댓값: %d, 인덱스: %d", lowerBound, upperBound, index));
        this.lowerBound = lowerBound;
        this.upperBound = upperBound;
        this.index = index;
    }
}

 

필드에서 사용될 값을 초기화 하면서 파라미터들로 새로운 상세 메시지를 상위 클래스로 넘겨버리는 것이다. 이렇게 하면 클래스 사용자가 메시지를 만드는 작업을 중복하지 않아도 된다.

 

 

메시지는 장황할 필요가 없다. 그리고 예외의 상세 메시지와 최종 사용자에게 보여줄 오류 메시지를 구분하자

스택 추적은 예외가 발생한 파일 및 호출 메소드들의, 이름과 줄번호 정확히 기록되어 있는 게 보통이다. 따라서 관련 데이터를 모두 담아야 하지만 길게 늘어놓을 필요가 없다. 그리고 최종 사용자에게는 친절한 안내 메시지를 보여줘야 하지만, 예외 메시지는 프로그래머가 보기에 가독성보단 담긴 내용이 더 중요하다. 

 

+ 예외 복구에 더 유용할 수 있도록 예외에서 추가로 사용된 필드(위 코드 예: XXbound, index)의 접근자를 함께 제공하자

+ 실패 관련 정보를 상세 메시지에 담되, 보안과 관련된 비밀번호나 암호 키 같은 정보까진 담지말자.

 

 

 

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



실패 원자적(failure-atomic): 호출된 메소드가 실패하더라도 해당 객체는 메소드 호출 전 상태를 유지하는 특성

 

메소드를 실패 원자적으로 만드는 방법

1. 불변 객체로 설계

메소드가 실패하면 새로운 객체가 만들어지지 않고, 생성 시점에서 객체의 상태는 고정되어 절대 변하지 않는다.

 

2. 작업 수행 전 매개변수의 유효성 검사

객체의 내부 상태 변경 전, 잠재적 예외들을 걸러낸다. 실패할 가능성이 있는 모든 코드를 객체 상태 변경 코드보다 앞에 배치하면 된다.

 

(1) stack의 pop 예시

 

public Object pop() {
    if (size == 0) {
        throw new EmptyStackException();
    }
    Object result = elements[--size];
    elements[size] = null;
    return result;
}

 

size 확인을 안해도 예외는 터지겠지만, 그건 ArrayIndexOutOfBoundsException으로 추상화 수준이 상황에 어울려 보이진 않는다.

 

(2) TreeMap

TreeMap 자료구조는 Comparator를 기본적으로 사용하며 자료를 저장한다. 엉뚱한 타입의 원소를 추가하려 한다면 트리 변경에 앞서 원소가 들어갈 위치를 찾는 과정에서 ClassCastException을 던진다. 

 

 

3. 객체의 임시 복사본에서 작업을 수행한 후, 완료되면 원래 객체와 교체하는 방식

 

 

List의 sort 메소드는 정렬 수행 전 원소들을 배열로 옮겨 담는다. 배열을 사용하면 정렬 알고리즘의 반복문에서 원소들에 더 빠르게 접근할 수 있기 때문이다. 이점은 성능에 그치지 않고 혹시나 정렬에 실패하더라도 입력 리스트는 변하지 않는 효과도 있다.

 

4. 작업 도중 발생하는 실패를 가로채는 복구 코드 작성

디스크 기반의 내구성을 보장해야하는 자료구조에 주로 쓰인다. (DB의 대량 트랜잭션의 롤백? 프로그램 설치 오류 시 이전으로 되돌리기?)

 

실패 원자성을 항상 가져야하는가

  • 두 스레드가 동기화 없이 같은 객체를 동시에 수정하면 객체의 일관성은 깨진다. ConcurrentModificationException을 던졌다고 하더라도 객체가 여전히 쓸 수 있는 상태라고 확신할 순 없다. 이미 값을 동시에 변경했을 수도 있기 때문이다.
  • Error는 심각한 손상이라 판단하고 복구할 수 없기에 AssertionError에 대해선 실패 원자적으로 만들려는 시도조차 할 필요가 없다.
  • 실패 원자성을 달성하기 위해 비용이나 복잡도가 아주 큰 연산도 있다.

위 상황들을 놓고 봤을 때 실패 원자성은 일반적으로 권장되지만 항상 달성할 수 있는 것도 아니고 실패 원자성을 만들 수 있더라도 항상 그리 해야하는 것도 아니다. 그래도 문제가 뭔지 알고나면 실패 원자성을 공짜로 얻기도 한다.

 

메소드 명세에 기술한 예외면 예외 발생 시 객체 상태는 메소드 호출 전과 똑같이 유지돼야 한다. 만약 지키지 못한다면 실패 시 객체 상황을 API에 명시하자.

 

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



예외는 문제 상황에 잘 대처하기 위해 존재하는데 catch 블록을 비워두면 예외가 존재할 이유가 없어지고 무시하는 행위다.

 

검사 예외든, 비검사 예외든 모두 포함된다. 예외를 무시하면 문제 원인과 상관 없는 곳에서 사이드 이펙트가 터질 수 있다. 물론 예외를 무시해야할 때도 있다. FileInputStream은 입력 전용 스트림이니 닫을 때 파일 상태를 변경하지 않는다. 따라서 복구할 게 없고 스트림을 닫는다는 건 필요 정보를 이미 다 읽었다는 뜻이고 남은 작업을 중단할 이유도 없다. 하지만 혹시 모르니 로그로는 남겨두자.

이런 식으로 예외를 무시하기로 했다면 catch 블록 안에 그렇게 결정한 이유를 주석으로 남기고 예외 변수명도 ignored 등으로 바꾸자.

 

Future<Integer> f = exec.submit(planarMap::chromaticNumber);
int numColors = 4;
try {
    numColors = f.get(1L,TimeUnit.SECONDS);
} catch (TimeOutException | ExecutionException ignored) {
    // 기본 값을 사용한다(색상 수를 최소화하면 좋지만, 필수는 아니다)
}

 

 

 

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