Effective Java : 아이템21. 완벽공략 38. ConcurrentModificationException

    들어가기 전

    이 글은 인프런 백기선님의 강의를 복습하며 작성한 글입니다. 


    이 글의 요약

    • ConcurrentModificationException은 멀티 쓰레드 환경에서 변하면 안되는데 변한 조건들이 있을 때 발생하는 에러다. 예를 들면 syncronzied 메서드 내에서 다른 메서드가 변수를 수정한다거나 했을 때 발생한다.
    • ConcurrentModificiationException은 싱글 쓰레드에서도 발생하는데 주로 fail-fast를 기반으로 한 Collection을 순회하면서, element를 변경했을 때 발생한다. 

    완벽 공략 38. ConcurrentModificationException

    ConcurrentModificationException은 현재 바뀌면 안되는 것을 수정할 때 발생하는 예외다. 이 Modification은 이름과는 다르게 멀티 스레드 뿐만 아니라 싱글 스레드 상황에서도 발생할 수 있다. 가장 대표적인 예시로는 fail-fast Iterator를 사용해 Collection을 순회하고 있는 상황에서 Collection을 변경하는 경우에 발생한다. 

    어떤 한 스레드가 Collection을 순회하고 있는데, 다른 쓰레드가 Collection을 변경하려 한다면 순회한 결과가 예측이 불가능해진다. 그래서 fail-fast Iterator 라는 개념이 도입되었는데, 이런 상황이 감지되면 ConcurrentModificationException을 던져서 '빨리 실패'하게 한다. 

    fail-fast Iterator는 Collection을 도는 도중에 '이런 변경점'이 발생하면 바로 예외를 던진다. 나중에 이상한 것을 감지하는 리스크를 감수하지 않고, 문제가 발생한 시점에 바로 예외를 던져서 더 큰 문제의 발생을 원천 차단한다. 

     

    List.of() / ArrayList 의 차이점

    Collection 중에는 List.of()와 ArrayList가 있다. 그런데 각각은 다르게 동작한다. List.of()로 생성하는 List는 ImmutableCollections 클래스다. 이것은 수정이 불가능한 Collection을 의미한다. 따라서 초기에 한번 생성되고 난 다음에는 수정 (추가 / 삭제)등이 불가능하다. 

    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5); // immutableCollection임
        numbers.remove(1);
    }
    
    // List.java
        static <E> List<E> of(E e1, E e2, E e3, E e4, E e5) {
            return new ImmutableCollections.ListN<>(e1, e2, e3, e4, e5);
        }

    만약 List.of()로 생성된 List에 수정 작업을 할 경우 UnsupportOperationException을 던져준다. 

    Exception in thread "main" java.lang.UnsupportedOperationException
    	at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:71)
    	at java.base/java.util.ImmutableCollections$AbstractImmutableCollection.add(ImmutableCollections.java:75)
    	at com.example.effectivejava1.chapter22.failfast.FailFast.main(FailFast.java:10)

    반면 ArrayList는 얼마든지 수정이 가능한 클래스다.  따라서 ArrayList를 생성하고 배열에 값을 넣고 뺄 수 있다. 


    ArrayList에서 수정 시 ConcurrentModificationException 발생

    아래 코드를 실행시켜보면, ConcurrentModificationException이 발생한다.

    • numberList에서 Iteration을 하게 되면, ListItr 객체가 반환된다.
    • ListItr 객체는 fast-fail Iterator이기 때문에 Iteration 도중에 변경이 감지되면 ConcurrentModificationException을 발생시킨다. 
    ArrayList<Integer> numberList = new ArrayList<>();
    numberList.add(1);
    numberList.add(2);
    numberList.add(3);
    numberList.add(4);
    numberList.add(5);
    
    // 이터레이션으로 콜렉션을 순회하는 중에 Collection의 remove를 사용하면,
    // ConcurrentModificationException 발생
    for (Integer number : numberList) {
        if (number == 3) {
            numberList.remove(number);
        }
    }

    아래에서 볼 수 있듯이 listIterator()는 ListIterator 인스턴스를 반환한다. 이 인스턴스는 fast-fail Iterator 이기 때문에 동작 도중에 변경이 감지되면 ConcurrentModification이 발생한다. 

    ArrayList.java
    // fail-fast Iterator를 반환시킨다. 
    public ListIterator<E> listIterator(int index) {
        rangeCheckForAdd(index);
        return new ListItr(index);
    }

    Collection을 순회하면서 삭제하고 싶다면? 

    일반적으로 Iteration을 돌면서 Element를 삭제한다면, fast-fail Iterator에 의해서 ConcurrentModificationException이 발생할 수 있다. 이 에러가 발생하지 않고 Collection을 순회하면서 삭제할 때는 다음 방법을 사용할 수 있다. 

    • Iterator를 직접 사용해서 제거를 한다. 
    • Index를 기준으로 for문을 돌며 제거한다.
    • removeIf를 이용해서 제거한다. (내부적으로 Iterator를 직접 사용하는 것이다.) 

    아래에 각 코드의 예시가 있다. 가장 좋은 방법은 removeIf를 이용해서 제거하는 것이다. 코드의 가독성이 가장 좋기 때문이다. 

    // 이터레이터의 remove 사용하기
    // Exception 발생 X
    for (Iterator<Integer> iterator = numberList.iterator(); iterator.hasNext();) {
        Integer integer = iterator.next();
        if (integer == 3) {
            iterator.remove();
        }
    }
    
    // 인덱스 사용하기
    // Exception 발생 X
    for (int i = 0; i < numberList.size(); i++) {
        if (numberList.get(i) == 3) {
            numberList.remove(numberList.get(i));
        }
    }
    
    // removeIf 사용하기
    // 내부적으로 Iterator를 직접 사용하는 것과 동일
    // Exception 발생 X
    numberList.removeIf(integer -> integer == 3);

    댓글

    Designed by JB FACTORY