일반화해 이야기하면, 호출된 메서드가 실패하더라도 해당 객체는 메서드 호출 전 상태를 유지해야 함 ↔ Failure-atomic
메서드 명세에 기술한 예외라면 설혹 예외가 발생하더라도 객체의 상태는 메서드 호출 전과 똑같이 유지돼야 한다는 것이 기본규칙이다. 이 규칙을 지키지 못한다면 실패 시의 객체 상태를 API 설명에 명시해야 한다
메서드를 실패 원자적으로 만드는 방법
- 불변 객체로 만드는 방법. 메서드가 실패하면 새로운 객체가 만들어지지는 않을 수 있으나 기존 객체가 불안정한 상태에 빠지는 일은 결코 없음
- 유효성 검사, 실패 가능성 있는 코드 앞에 배치
- if 부분을 빼게 되면, size가 음수가 되어버릴 수 있으니 실패 원자적이 될 수 없다
- TreeMap을 예시로 생각해보면, 해당 인스턴스에 원소 추가하려면 그 원소는 TreeMap의 기준에 따라 비교할 수 있는 타입이어야 하고, 그렇지 않다면
ClassCastException
을 던지게 됨
가변 객체의 메서드를 실패 원자적으로 만드는 가장 흔한 방법은 작업 수행에 앞서 매개변수의 유효성을 검사하는 것
public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; return result; }
비슷한 취지로, 실패할 가능성이 있는 모든 코드를, 객체의 상태를 바꾸는 코드보다 앞에 배치하는 방법 (계산 수행해보기 전에는 인수의 유효성을 검사해 볼 수 없을 때 위의 방식에 덧붙여 쓸 수 있는 기법)
- 객체의 임시 복사본에서 작업을 수행한 다음, 작업이 성공적으로 완료되면 원래 객체와 교체하는 것
- 작업 도중 발생하는 실패를 가로채는 복구 코드를 작성하여 작업 전 상태로 되돌리는 방법
실패 원자성 달성 못하는 경우, 필요없는 경우
실패 원자성은 일반적으로 권장되는 덕목이지만 항상 달성할 수 있는 것은 아니다. (예: 동시성 문제, Error
)
- 예를 들어 두 스레드가 동기화 없이 같은 객체를 동시에 수정한다면 그 객체의 일관성이 깨질 수 있음
ConcurrentModificationException
을 잡아냈다고 해서 그 객체가 여전히 쓸 수 있는 상태라고 가정해서는 안됨
- Error는 복구할 수 없으므로 AssertionError에 대해 실패 원자적으로 만들려는 시도조차 할 필요 없음
- 실패 원자적으로 만들 수 있더라도 항상 그리 해야 하는 것도 아니다 (실패 원자성을 달성하기 위한 비용이나 복잡도가 아주 큰 연산)