Effective Java : 아이템 76. 가능한 한 실패 원자적으로 만들어라.

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

    • 실패 원자성이란 실패 전후로 객체의 상태가 바뀌지 않는 것을 의미함.
    • 실패한 후, 프로그램이 계속 진행된다면 이 때는 실패 원자적으로 만드는 것이 좋음.
    • 실패 원자성 구현 방법
      • 불변 객체로 만듦. 
      • 가변 객체의 경우 매개변수의 유효성 검사.
      • 실패할 가능성이 있는 모든 코드를 객체의 상태를 바꾸는 코드보다 앞에 배치.
      • 임시 복사본에서 작업 수행 후, 작업 성공되면 원래 객체와 교환하기 
    • 실패 원자성을 보장하지 않아도 되는 경우
      • 실패 원자성을 달성하기 위한 연산의 비용 / 복잡도가 클 경우.
      • ConcurrentModificationException이 발생한 경우. → 이미 콜렉션은 변했기 때문에 어떻게 할 수가 없음

     


    실패 원자성이란? 

    특정 객체의 메서드를 실행하던 도중 예외가 발생해서 실패하는 경우가 있다. 메서드를 호출한 곳에서 이 예외를 잡고 다음 작업을 진행하는 경우라면, 메서드 실행 전/후로 객체의 상태가 변하지 않는 것이 좋다. 메서드가 실패했을 때 실행 전/후로 객체의 상태가 변하지 않는 것을 '실패 원자성'을 가진다고 한다.

    아래에서 메서드가 실패했을 때, 객체의 상태가 실행 전/후로 변하는 예시를 볼 수 있다. pop() 메서드가 실행되었을 때, IndexOutOfBoundsException 에러가 발생할 것이다. catch 절에서 이 예외를 잡은 후, NonAtomicFailure 객체의 내부 상태를 살펴보면 실행 전후로 0 → 1로 변경된 것을 볼 수 있다. 이것이 '실패 원자성'이 없는 경우다.

    public class NotAtomicFailure {
        private int size = 0;
        private final String[] elements = new String[10];
    
        public Object pop() {
            Object result = elements[--size];
            elements[size] = null;
            return result;
        }
    
        public int getSize() {
            return this.size;
        }
    
        public static void main(String[] args) {
    
            NotAtomicFailure notAtomicFailure = new NotAtomicFailure();
            try {
                Object pop = notAtomicFailure.pop();
            } catch (IndexOutOfBoundsException e) {
                System.out.println(notAtomicFailure.getSize());
            }
        }
    }
    
    >>>
    -1

    위 코드에서 메서드 실행 전 '전제 조건'을 검사하면 메서드의 실패 원자성을 확보할 수 있다. 아래 코드를 실행해보면 메서드가 실패하지만 실행 전/후로 Size는 0을 가지는 것을 알 수 있다. 즉, '실패 원자성'을 확보할 수 있다. 

    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }
    

     


    실패 원자성을 만드는 방법은? 

    • 불변 객체로 만듦. 
    • 가변 객체의 경우 매개변수의 유효성 검사.
    • 실패할 가능성이 있는 모든 코드를 객체의 상태를 바꾸는 코드보다 앞에 배치.
    • 임시 복사본에서 작업 수행 후, 작업 성공되면 원래 객체와 교환하기 

    실패 원자성을 만드는 방법은 다음과 같다. 

    메서드가 실패하면 새로운 객체가 생성되지 않는다. 따라서 불변 객체를 한번 만들어두면 값이 바뀌지 않고, 메서드가 실패했을 때 새로운 객체가 생성되지 않았기 때문에 실패 원자성이 보장된다.

    가변 객체는 값이 바뀔 수 있다. 따라서 유효성 검사를 미리 한 후에 만족하지 못하면 객체 상태를 바꾸기 전에 예외를 던져서 실패 원자성을 확보하는 방법이다. 

    public Object pop() {
        if (size == 0) {
            throw new EmptyStackException();
        }
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }
    

    유효성 검사를 하기 애매한 상태라면, 실패할만한 코드를 객체의 상태를 바꾸는 코드보다 앞에 배치하는 것이 방법이 될 수 있다. 아래처럼 실패 가능성이 있는 코드와 상태가 바뀌는 연산을 나누고, 실패할만한 코드부터 실행하면서 확보할 수도 있다.

    public Object pop() {
        // 실패할만한 코드.
        int nextSize = size - 1;
        Object result = elements[nextSize];
    
        // 상태 바뀌는 연산.
        elements[nextSize] = null;
        this.size = nextSize;
        return result;
    }

    혹은 임시 복사본에서 연산을 먼저 수행하고, 복사본의 연산이 완료되면 해당 객체를 반환하는 형태로도 실패 원자성을 확보할 수 있다.  데이터를 임시 자료구조에 저장해 작업하는게 더 빠를 때 적용하기 좋은 방식이다. 예를 들어 정렬 메서드에서는 리스트에 있는 값을 배열로 옮겨담고 정렬해서 반환하기도 한다. 성능을 높이기 위한 방식이지만, 덤으로 정렬이 실패해도 '기존 리스트'의 실패 원자성은 보장되는 효과가 있다.


    실패 원자성을 보장하지 않아도 되는 경우? 

    • 실패 원자성을 달성하기 위한 연산의 비용 / 복잡도가 클 경우.
    • ConcurrentModificationException이 발생한 경우.

    실패 원자성은 달성하면 좋지만, 실패 원자성을 달성하기 위한 비용이 크거나 복잡한 경우라면 달성하지 않아도 좋다고 한다. 또한 여러 쓰레드가 동기화 없이 같은 객체를 동시에 수정한다면 그 객체의 일관성이 깨질 수 있다. 이럴 때, ConcurrentModificationException을 Catch 했다고 해서 그 객체를 쓸 수 있다고 생각하면 안된다. 객체의 상태는 어떻게 되었는지 전혀 알 수 없기 때문이다.

    아래 코드에서는 ConcurrentModificationException이 발생하지만, list에서 'one'이라는 원소는 이미 삭제되어있다. 만약 실패 원자성이 있다고 하면 'one', 'two', 'three'가 모두 있어야 할 것이다. 굉장히 간단한 예시라 이해할 수 있는데, 만약 더 복잡한 예시라면 객체가 어떤 상태인지 짐작조차 할 수 없다. 

    public static void main(String[] args) {
    
        List<String> list = new ArrayList<>();
        list.add("one");
        list.add("two");
        list.add("three");
    
        try {
            for (String item : list) {
                if (item.equals("one")) {
                    list.remove(item); // 여기서 ConcurrentModificationException이 발생합니다.
                }
            }
        } catch (ConcurrentModificationException e) {
            list.forEach(s -> System.out.println(s));
        }
    }

     

     

    댓글

    Designed by JB FACTORY