[Effective Java] 가능한 한 실패 원자적으로 만들라

들어가며

호출된 메소드가 실패하더라도 해당 객체는 메소드 호출 전 상태를 유지해야 한다. 검사 예외를 던진 경우라면 호출자가 이를 복구할 수 있을 테니 더욱 유용하다. 이러한 특성을 실패 원자적이라고 한다.

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

불변 객체로 설계하라

불변 객체는 태생적으로 실패 원자적이니 기존 객체가 불안정한 상태에 빠지는 일은 결코 없다.

작업 수행에 앞서 매개변수의 유효성을 검사하라

가변 객체의 메소드를 실패 원자적으로 만드는 방법은 작업 수행에 앞서 매개변수의 유효성을 검사하는 것이다. 다음은 Stack의 코드이다.

1
2
3
4
5
6
7
8
public E pop() {
if (size == 0)
throw new EmptyStackException();
@SuppressWarnings("unchecked")
E result = (E) elements[--size];
elements[size] = null;
return result;
}

위 코드에서는 작업 수행 전에 size의 유효성을 검증한다. 이를 검증하지 않아도 예외는 던져진다. 하지만 그 다음 스택의 size의 상태는 -1이 되어 ArrayIndexOutOfBoundsException이 될 것이다. 이는 추상화 수준이 상황에 어울리지 않는다.

이와 비슷한 취지로 실패할 가능성이 있는 모든 코드를 객체의 상태를 바꾸는 코드보다 앞에 배치하는 방법도 있다. 계산을 수행해보기 전에는 인수의 유효성을 검사해볼 수 없을 때 앞서의 방식에 덧붙여 쓸 수 있는 기법이다. 예를 들어 TreeMap은 원소를 어떤 기준으로 정렬하는데 TreeMap에 원소를 추가하려면 그 원소는 TreeMap의 기준에 따라 정렬할 수 있는 타입이여야 한다. 엉뚱한 타입의 원소를 추가하려 들면 원소를 트리를 변경하기에 앞서 원소가 들어갈 위치를 찾는 과정에서 ClassCastException을 던진다.

임시 복사본을 만들라

또 하나의 방법은 객체의 임시 복사본에서 작업을 수행한 다음 작업이 성공적으로 완료되면 원래 객체와 교체하는 것이다. 데이터를 임시 자료구조에 저장해 작업하는게 더 빠를 때 적용하기 좋은 방식이다. 예를 들어 정렬을 수행하는데 있어 배열은 Cache Locality를 잘 만족하는 자료구조이다. 이는 성능을 위한 결정이지만 실패하더라도 입력 리스트는 변하지 않는 효과를 불러온다.

어려움

실패 원자성은 일반적으로 권장되지만 항상 달성할 수 있는 것은 아니다. 예를 들어 두 스레드가 동기화 없이 같은 객체를 동시에 수정한다면 그 객체의 일관성이 깨질 수 있다. 따라서 ConcurrentModificationException을 잡아냈다고 해서 그 객체가 여전히 쓸 수 있는 상태라고 가정해서는 안된다. 한편, Error는 복구할 수 없으므로 AssertionError에 대해서는 실패 원자적으로 만들려는 시도조차 할 필요가 없다.

실패 원자적으로 만들 수 있더라도 이를 달성하기 위한 비용이나 복잡도가 아주 큰 연산이라면 항상 만들어야하는 것은 아니다.

Author: Song Hayoung
Link: https://songhayoung.github.io/2020/08/22/Languages/Effective%20JAVA/item76/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.