JAVA/Effective Java

[아이템 76] 가능한 한 실패 원자적으로 만들라

꾸준함. 2024. 3. 29. 12:39

실패 원자성

  • 호출된 메서드가 예외 발생으로 인해 실패하더라도 객체가 메서드 호출 전 상태를 유지하는 특성
  • 실패 원자성이 보장되면 Checked Exception을 던졌을 때 호출자가 오류 상태를 복구할 수 있으므로 유용함

 

메서드를 실패 원자적으로 만드는 방법은 총 네 가지가 있으며 다음과 같습니다.

  • 불변 객체로 설계
  • 매개변수 유효성 검사
  • 복사본에 로직을 수행 후, 성공적으로 수행이 완료될 경우에만 원본 객체와 Swap
  • 작업 도중의 에러를 가로채는 복구 코드를 작성하여 롤백

 

1. 불변 객체로 설계

 

  • 불변 객체는 생성 시점에 고정되어 절대 변하지 않기 때문에 태생적으로 실패 원자적
  • 메서드가 실패하면 새로운 객체가 생성되지 않을 수 있으나 기존 객체가 불안정한 상태에 빠지는 일은 없음

 

2. 매개변수 유효성 검사

 

  • 가변 객체의 메서드를 실패 원자적으로 만드는 가장 흔한 방법은 작업 수행에 앞서 매개변수의 유효성을 검사하는 것
    • 객체의 내부 상태를 변경하기 전 잠재적 예외의 가능성 대부분을 걸러낼 수 있는 방법

 

 

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

 

TreeMap의 put 메서드
TreeMap의 compare 메서드

 

public class Example {
public static void main(String[] args) {
Map<Integer, String> treeMap = new TreeMap<>();
treeMap.put(1, "하나");
treeMap.put(2, "둘");
treeMap.put(3, "셋");
System.out.println("실패 전: " + treeMap);
try {
// ClassCastException 발생 시도
treeMap.put(4, (String)(Object)new Integer(4)); // 임의로 ClassCastException 유발
} catch (ClassCastException e) {
// 예외 처리
System.out.println("ClassCastException occurred: " + e.getMessage());
}
System.out.println("실패 후: " + treeMap);
}
}
view raw .java hosted with ❤ by GitHub

 

3. 복사본에 로직을 수행 후, 성공적으로 수행이 완료될 경우에만 원본 객체와 Swap

 

  • 데이터를 임시 자료구조에 저장해 작업하는 것이 더 빠를 때 적용하기 좋은 방식
  • ex) 정렬 메서드에서는 정렬을 수행하기 전에 입력 리스트의 원소들을 배열로 옮겨 담음
    • 배열을 사용하면 정렬 알고리즘의 반복문에서 시간복잡도 O(1)으로 원소들에 훨씬 빠르게 접근할 수 있음 
    • 혹시나 정렬에 실패하더라도 입력 리스트는 변하지 않는 효과도 덤으로 얻음

 

 

4. 작업 도중의 에러를 가로채는 복구 코드를 작성하여 롤백

 

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

 

실패 원자성을 보장할 수 없거나 필요 없는 케이스

  • 실패 원자성은 일반적으로 권장되는 덕목이지만 항상 달성할 수 있는 것은 아님
  • ex) 멀티 쓰레드 환경에서 동기화 없이 같은 객체를 동시에 수정할 경우 해당 객체의 일관성이 깨질 수 있음
    • 따라서, ConcurrentModificationException을 잡아냈다고 해서 그 객체가 여전히 쓸 수 있는 상태라고 가정해서는 안됨

 

  • Error는 복구할 수 없으므로 AssertionError에 대해서는 실패 원자적으로 만들려는 시도조차 할 필요 없음
  • 실패 원자적으로 만들 수 있더라도 실패 원자성을 달성하기 위한 비용이나 복잡도가 아주 큰 연산일 경우 보장하지 않아도 됨
    • ex) 대용량 파일을 정렬하거나 검색할 때는 전체 파일을 메모리에 로드하는 것은 불가능할 수 있으며, 대신 파일을 여러 조각으로 나누어 처리해야 할 수 있음
    • 이러한 작업을 수행할 때 실패 원자성을 보장하는 것은 매우 어려울 수 있음

 

결론

  • 메서드 명세에서 기술한 예외라면, 예외가 발생하더라도 발생하기 전의 객체와 동등한 상황 즉 실패 원자성을 보장해야 함
  • 그러나, 실패 원자성을 담보할 수 없다면 예외 이후 객체의 상황을 API 설명에 명시해야 함

 

참고

이펙티브 자바

반응형