작업 도중 예외가 발생해도 그 객체는 여전히 정상적으로 사용할 수 있는 상태라면 멋질 것이다. 검사 예외를 던진 경우라면, 호출자가 오류 상태를 복구할 수 있을 테니 특히 더 유용할 것이다. 일반화해 이야기하면, 호출된 메서드가 실패하더라도 해당 객체는 메서드 호출 전 상태를 유지해야 한다. 이러한 특성을 실패 원자적
이라고 한다.
메서드를 실패 원자적으로 만드는 방법은 다양하지만 가장 간단한 방법은 객체를 불변 객체
로 만드는 것이다. 불변 객체
는 태생적으로 실패 원자적이기 때문이다.
가변 객체의 메서드를 실패 원자적으로 만드는 가장 흔한 방법은 작업 수행에 앞서 매개변수의 유효성을 검사하는 것이다. 객체 내부 상태를 변경하기 전 잠재적 예외 가능성 대부분을 걸러낼 수 있는 방법이다.
public Object pop(){
if(size == 0)
throw new EmptyStackException();
... // 내부 상태 변경
}
객체의 임시 복사본에서 작업으 수행한 다음, 작업이 성공적으로 완료되면 원래 객체와 교체하는 방법이 있다. 데이터를 임시 자료구조에 저장하고 작업하는 게 더 빠를 때 좋은 방식이다.
예를 들어서, 어떤 정렬 메서드에서는 정렬을 수행하기 전에 입력 리스트의 원소들을 배열로 옮겨 담는다. 배열을 사용하면 정렬 알고리즘의 반복문에서 원소들에 훨씬 빠르게 접근할 수 있기 때문이다. 물론 이는 성능을 높이고자 하는 결정이지만 혹시나 정렬에 실패하더라도 입력 리스트는 변하지 않는 효과를 덤으로 얻는다.
작업 도중 발생하는 실패를 가로채는 복구 코드를 작성해서 작업 전 상태로 되돌리는 방법이다. 주로 내구성을 보장해야 하는 (디스크 기반의)자료구조에 쓰이는데 자주 쓰이는 방법은 아니다.
실패 원자성은 일반적으로 권장되지만 항상 달성할 수 있는 것은 아니다. 예를 들어서 두 쓰레드가 동기화없이 같은 객체를 동시에 수정한다면 그 객체의 일관성이 깨질 수 있다. 따라서 ConcurrentModificationException
을 잡아냈다 해서 그 객체가 여전히 쓸 수 있는 상태라고 가정해서는 안된다.
한편, Error
는 복구할 수 없으므로 AssertionError
에 대해서는 실패 원자적으로 만들려는 시도조차 할 필요가 없다.
실패 원자적으로 만들수 있다고 해도 항상 그리 해야 하는 것도 아니다. 실패 원자성을 달성하기 위해서 비용이나 복잡도가 아주 큰 연산도 있기 때문이다.