S E P H ' S

[Java] Iterator (ConcurrentModificationException 에러) 본문

Programing & Coding/JAVA

[Java] Iterator (ConcurrentModificationException 에러)

yoseph0310 2023. 4. 25. 17:45

Iterator

iterator의 사전적 의미는 반복자이다. 동사형인 iterate는 사전을 찾아보면 (계산, 컴퓨터 처리 절차를)반복하다. 라고 되어있다. 뉘앙스만 보자면 코드에서 반복적인 일을 처리하도록 할 것 같다. 그런데 이미 for, while과 같은 반복문으로 우리는 코드의 반복적인 작업을 처리하고 있다. 그런데 굳이 왜 iterator를 사용할까? 또, 알고리즘 문제를 풀다가 상대적으로 많이 접해보지 않아서 정확히 알아보고자 포스팅을 하게 됐다.

 

알고리즘 문제 풀이를 하거나 코드 작업을 하다보면 Collection 객체의 요소들을 조작할 일들이 정말 많다. 삽입이나 수정, 삭제 등과 같은 일을 하게 되는데 코드로 예시를 살펴보자.

 

public static void main(String[] args) {
    ArrayList<Integer> list = new ArrayList<>();

    for (int i = 0; i < 10; i++) {
        list.add(i + 1);
    }

    for (int n : list) {
        System.out.println(n);
    }

}

 

리스트에 1 ~ 10까지의 숫자를 넣어주었고 리스트의 요소들을 for-each 문을 활용해 출력하고 있다. 예를 들어서 이 리스트에서 반복을 하면서 짝수인 요소들을 삭제하려고 한다.

 

for (int n : list) {
    System.out.println(n);
    if (n % 2 == 0) list.remove(n);
}

 

이렇게 하면 짝수인 요소들을 삭제할 수 있다. 과연 그럴까? 이는 에러를 발생시키는 코드이다. 부끄럽지만 이 사실을 얼마전에 알게됐다.

 

for-each문으로 자료구조를 돌면서 삭제하거나 수정하는 코드를 작성하면 ConcurrentModificationException 에러가 발생한다. 당장 에러 메시지에 있는 것을 확인해보도록 하자. ArrayList의 967 라인부터 확인해보자.

 

코드 1

 

벌써부터 Iterator가 등장한다. 사실 Collection 객체들은 모두 iterator 구현체들을 모두 지니고 있어 관련 메소드들을 사용할 수 있다. 이에 대한 것은 조금 나중에 다루고 우선 에러부터 확인해보자. 표시가 되어있는 checkForComodification 메소드를 확인해보면

 

코드 2

에러메시지에서 보냈던 1013 번째 줄에 해당하는 곳이 나온다. 여기서 modCount와 expectedModCount가 달라 에러를 throw 한 것이었다. 이 modCount와 expectedModCount란 무엇일까? 

 

ModCount & ConcurrentException

modCount는 ArrayList의 멤버 변수이고 expectedModCount는 ArrayList의 내부 클래스인 (코드 1) Itr의 멤버 변수이다. 이름에서 유추할 수 있듯 연산 횟수 카운트를 modCount변수로 두는 것이다. 코드 1을 확인해보면 Itr의 expectedModCount = modCount; 로 초기화를 하고 있다. List에 add나 remove와 같은 요소의 개수를 변화시키는 메소드를 호출할때마다 List의 멤버 변수인 modCount가 변화하게 된다.

 

ArrayList.iterator()는 list의 modCount를 복사하여 Iterator 인스턴스의 멤버 변수인 expectedModCount에 저장한다. 그리고 next() 메소드를 호출하면서 list의 modCount와 expectedModCount를 비교해서 중간에 데이터의 변화가 있었는지 체크한다. 이때 같지 않다면 데이터의 변화가 있었다고 생각하고 ConcurrentModificationException 에러를 throw 하는 것이다.

 


회피 방법

Iterator으로 순회 및 요소 삭제

 

이 문제를 회피하기 위해 여러 해결책이 있겠지만 포스트가 Iterator인 만큼 Iterator를 사용한 회피 방법을 가져왔다.

 

public static void main(String[] args) {
    ArrayList<Integer> list = new ArrayList<>();

    for (int i = 0; i < 10; i++) {
        list.add(i + 1);
    }

    for (Iterator<Integer> it = list.iterator(); it.hasNext();) {
        int n = it.next();
        if (n % 2 == 0) it.remove();
    }

    for (int n :list) {
        System.out.println(n);
    }
}

정상적으로 의도한대로 동작하는 것을 볼 수 있다. 그럼 Iterator는 어떻게 문제 없이 에러를 피해 동작하게 할 수 있었을까? 우리는 위에서 List의 modCount와 Itr의 expectedModCount가 달라서 에러가 발생했었다는 것을 알고 있다. 무언가 이 둘을 같은 값으로 만들어주기 때문이 아닐까? 여기서 List의 iterator를 사용했기 때문에 하나씩 추적을 해나가 보도록하자. 먼저 Iterator의 remove를 살펴본다.

 

Iterator 인터페이스이다. 실제 동작은 구현체가 했을 테니 ArrayList에서 Iterator 인터페이스의 구현체들 중 누군가 remove를 실행했을 것이다. 조금만 찾아보면 정답을 알게 되는데 바로 ListItr이라는 클래스의 remove이다. 

 

Itr 클래스를 상속받고 ListIterator를 구현한 구현체이다. Itr 클래스가 바로 Iterator 인터페이스의 구현체이고 Itr 클래스에서 remove 메소드를 확인할 수 있다.

 

list에서의 remove와의 차이점은 lastRet이 가리키고 있는 요소를 list.remove의 인자로 넣어서 호출한다. 이 후에 lastRet이 가리키고 있던 위치로 cursor가 뒤로 이동하게 되고 다시 expectedModCount에 modCount를 복사하게 된다. 이곳에서 우리가 직면한 문제를 해결해주고 있었다.


정리

엄연히, 정확히 말하자면 에러를 추적해서 상황에 맞게 에러를 회피한 것이지 모든 상황에 적용되는 무적의 방법은 아니다. 다만 이렇게 확인해 본것은 에러가 발생했을 때, 내부적인 작동 원리를 이해하고 이를 해결해나가는 방법을 습득하기 위함이다. 따라서 상황에 따라서 에러를 파악하면서 적합한 해결방법을 모색해나가는 것이 좋은 습관일 것이다.

 

 


자료 출처

 

 

[ JAVA ] Iterator의 내부동작 - 자바니또의 Tech선물

개요 요즘 코딩테스트 준비를 위해 알고리즘 문제들을 풀고있다. 그러던 중 옆에서 같이 공부하던 생각한대로 동작하지 않는다며 보여준 코드를 보고 이번 포스팅의 주제를 정했다. public void met

brandpark.github.io

 

 

잘못 알고 있었던 java for each 구문과 modcount 필드

이펙티브 자바 3편을 보면 for each에 대한 설명이 나와 있습니다. 거기에서도 언급했다 시피, for each문을 도는 동안에 이터를 돌고 있는 자료구조에 변형이 오면 안 됩니다. (item 58) 만약에 변형이

codingdog.tistory.com