View

반응형

호출된 메서드가 실패해도 해당 객체는 메서드 호출 전 상태를 유지하는 특성실패 원자적이라고 한다. 우리는 메서드를 가능한 한 실패 원자적으로 만들어야 한다. 작업 도중 예외가 발생해도 그 객체를 정상적으로 사용할 수 있는 상태라면 호출자가 오류 상태를 복구할 수 있을 테니 더 유용하기 때문이다.

 

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

1. 메서드를 불변 객체로 설계한다.

불변 객체는 태생이 실패 원자적이며 객체가 불안정한 상태에 빠지는 일은 생기지 않는다. 불변 객체는 생성 시점에 고정되어 변하지 않기 때문이다.

메서드가 실패하면 새로운 객체가 만들어지지 않을 뿐이다.

 

2. 매개변수의 유효성을 검사한다.

가변 객체를 다루는 메서드를 실패 원자적으로 만들기 위한 가장 쉬운 방법은 수행에 앞서 매개변수의 유효성을 체크하는 것이다. 객체의 내부 상태를 변경하기 전, 잠재적 예외 가능성을 제거할 수 있기 때문이다.

public Object pop() {
    if (size == 0)
        throw new EmptyStackException();

    Object result = elements[--size];
    elements[size] = null; // 참조 해제
    return result;
}

 

위 코드의 if문을 제거해도 여전히 예외가 발생한다. 하지만 Object result = elements[--size]; 부분에서 ArrayIndexOutOfBoundException이 발생하며 이는 추상화 수준이 상황에 어울리지 않다고 볼 수 있다.

 

3. 실패 가능성 있는 코드는 객체의 상태 변경 코드보다 앞에 배치한다.

 

TreeMap을 예로 들어 생각해보자. TreeMap은 원소들을 어떤 기준으로 정렬한다. 즉, 원소들은 비교할 수 있는 타입이어야 하며 원소를 put(객체의 상태를 변경) 하기 전에 비교 가능한 타입인지 검사한다.

 

만약 비교할 수 없는 타입이라면 ClassCastException을 던진다.

 

4. 객체의 임시 복사본으로 작업이 성공하면 원래 객체와 교체한다.

데이터를 임시 자료구조에 저장해 작업하는 게 더 빠를 때 적용하기 좋은 방법이다.

예를 들면, 어떤 정렬 메서드는 정렬하기 전 리스트의 값을 배열에 옮겨 담고 정렬을 수행한다. 배열을 사용하면 원소 접근이 빨라질뿐더러 정렬에 실패하더라도 기존 리스트는 변하지 않는 효과를 가질 수 있기 때문이다.

 

 

5. 발생한 실패를 가로채는 복구 코드를 작성하고 이전 상태로 되돌린다.

주로 내구성(디스크 기반의)을 보장해야 하는 자료구조에 쓰이는데, 자주 쓰이는 방법은 아니다.

 

실패 원자성 알아두기

실패 원자성은 권장되지만 항상 달성할 수 있는 것은 아니다. 또한, 실패 원자적으로 만들 수 있더라도 항상 그리해야 하는 것도 아니다.

 

1. 동시성 문제

두 스레드가 동기화 없이 각은 객체를 동시에 수정한다면 그 객체의 일관성이 깨질 수 있다. 그러니 ConcurrentModificationException을 잡아냈다고 해서 그 객체가 여전히 사용할 수 있는 상태라고 생각해서는 안 된다.

 

2. 복구할 수 없는 에러는 실패 원자적으로 만들 필요가 없다.

Error는 복구할 수 없으므로 AssertionError에 대해서는 실패 원자적으로 만들려는 시도조차 할 필요가 없다.

 

3. 비용이나 복잡도가 크다면..

항상 실패 원자적으로 만들어야 하는 것은 아니기 때문에 만들 수 있더라도 이를 위한 비용이나 복잡도가 아주 크다면 넘어가도 된다.

 

4. 예외가 발생해도 객체는 이전 상태와 똑같이 유지돼야 한다.

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

아쉽게도 지금의 API 문서 상당 부분이 잘 지키지 않고 있지만, 이러한 방향이 이상적이다.

반응형
Share Link

인기 글

최신 글

전체 방문자

Today
Yesterday