실패 원자적(failure-atomic) 이란, 호출된 메서드가 실패하더라도 해당 객체는 메서드 호출 전 상태를 유지하는 것을 말한다.
1. 불변 객체로 설계하기
불변 객체는 태생적으로 실패 원자적이다. 불변 객체의 상태는 생성 시점에 고정되어 변하지 않기 때문에, 메서드가 실패하면 새로운 객체가 만들어지지는 않을 수 있으나 기존 객체가 불안정 상태에 빠지는 일은 없다.
2. 작업 수행 전에 매개변수의 유효성 검사하기 (아이템 49)
객체의 내부 상태를 변경하기 전에 잠재적 예외의 가능성 대부분을 걸러낼 수 있는 방법이다.
Stack.pop 메서드 예를 보자.
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;
return result;
}
또한 이와 비슷한 취지로 실패할 가능성이 있는 모든 코드를 객체의 상태를 바꾸는 코드보다 앞에 배치하는 방법도 있다. 계산을 수행해보기 전에는 인수의 유효성을 검사해볼 수 없을 때 앞서의 방식에 덧붙여 쓸 수 있는 기법이다.
3. 객체의 임시 복사본에서 작업 수행하기
객체의 임시 복사본에서 작업을 수행한 다음, 성공적으로 완료되면 원래 객체와 교체하는 것이다. 데이터를 임시 자료구조에 저장해 작업하는 게 더 빠를 때 적용하기 좋은 방식이다.
예를 들어, 리스트를 정렬하는 메서드에서 입력 리스트의 원소들을 배열로 옮겨 담아 정렬을 수행한다. 배열을 사용할 때 반복문에서 원소들에 훨씬 빠르게 접근할 수 있기 때문이다.
4. 작업 도중 발생하는 실패를 가로채는 복구 코드 작성하기
복구 코드를 통해 작업 전 상태로 되돌리는 방법이다. 주로 내구성(durability)을 보장해야 하는 자료구조에서 쓰인다. 이 방법은 자주 쓰이지는 않는다.
실패 원자성은 일반적으로 권장되지만, 항상 달성할 수 있는 것은 아니다.
두 스레드가 동기화 없이 같은 객체를 동시에 수정한다면 그 객체의 일관성이 깨질 수 있다. (ConcurrentModificationException
을 잡아냈다고 해도 그 객체가 여전히 쓸 수 있는 상태라고 가정해서는 안 된다)
Error
는 복구할 수 없으므로 AssertionError
에 대해서는 실패 원자적으로 만들려는 시도조차 할 필요가 없다.
또한 실패 원자적으로 만들 수 있더라도, 실패 원자성을 달성하기 위해 비용이나 복잡도가 아주 큰 연산이라면 반드시 그리 해야 하는 것은 아니다.
메서드 명세에 기술한 예외라면 예외가 발생하더라도 객체의 상태는 메서드 호출 전과 똑같이 유지돼야 한다는 것이 기본 규칙이다.
그러나 만약 이 규칙을 지키지 못한다면 실패 시의 객체 상태를 API 설명에 명시해야 한다.