Computer >> 컴퓨터 >  >> 프로그래밍 >> Java

Java에서 컬렉션 순회 중 ConcurrentModificationException을 피하는 방법

컬렉션(Collection) 객체를 다룰 때 한 스레드가 특정 컬렉션을 순회(iteration)하는 도중에 해당 컬렉션에 요소를 추가하거나 삭제하면 ConcurrentModificationException이 발생합니다.

뿐만 아니라, 컬렉션을 순회하는 도중에 요소를 추가·삭제한 뒤 기존 Iterator 객체로 다시 내용을 읽으려고 시도하면, 여러 스레드가 동시에 컬렉션에 접근하려는 것으로 간주되어 역시 같은 예외가 발생합니다. 이는 Java 컬렉션이 fail-fast(즉시 실패) 방식으로 동작하기 때문인데, 내부적으로 구조 변경 횟수(modCount)를 추적하다가 불일치가 감지되면 즉시 예외를 던집니다.

예외 발생 예시

import java.util.ArrayList;
import java.util.Iterator;
public class OccurenceOfElements {
    public static void main(String args[]) {
        ArrayList <String> list = new ArrayList<String>();
        // ArrayList 객체 생성
        list.add("JavaFX");
        list.add("Java");
        list.add("WebGL");
        list.add("OpenCV");
        System.out.println("배열 리스트의 내용 (처음부터 끝까지): ");
        Iterator<String> it = list.iterator();
        while(it.hasNext()) {
            System.out.print(it.next()+", ");
        }
        //list.remove(3);
        list.add(3, "Hadoop");
        while(it.hasNext()) {
            System.out.print(it.next()+", ");
        }
    }
}

실행 결과

배열 리스트의 내용 (처음부터 끝까지):
JavaFX, Java, WebGL, OpenCV, Exception in thread "main"
java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
    at java.util.ArrayList$Itr.next(Unknown Source)
    at sample.OccurenceOfElements.main(OccurenceOfElements.java:23)

위 예제에서는 첫 번째 순회가 끝난 후 리스트에 요소를 추가하고, 같은 Iterator 객체로 다시 순회를 시도했기 때문에 예외가 발생했습니다.

해결 방법

1. 수정 후 새로운 Iterator 객체 다시 얻기

여러 스레드에서 컬렉션 객체에 접근해야 하는 경우에는 synchronized 블록이나 synchronized 메서드를 사용해 접근을 동기화해야 합니다. 또한, 데이터를 조회하는 도중에 수정이 필요하다면 수정 작업을 마친 후 새로운 Iterator 객체를 다시 얻어 순회를 시작해야 합니다.

예시 코드

import java.util.ArrayList;
import java.util.Iterator;
public class OccurenceOfElements {
    public static void main(String args[]) {
        ArrayList <String> list = new ArrayList<String>();
        // ArrayList 객체 생성
        list.add("JavaFX");
        list.add("Java");
        list.add("WebGL");
        list.add("OpenCV");
        System.out.println("배열 리스트의 내용 (처음부터 끝까지): ");
        Iterator<String> it = list.iterator();
        while(it.hasNext()) {
            System.out.print(it.next()+". ");
        }
        list.remove(3);
        System.out.println("");
        System.out.println("삭제 후 배열 리스트의 내용: ");
        // 수정 후 새로운 Iterator 객체를 다시 얻음
        it = list.iterator();
        while(it.hasNext()) {
            System.out.print(it.next()+". ");
        }
    }
}

실행 결과

배열 리스트의 내용 (처음부터 끝까지):
JavaFX. Java. WebGL. OpenCV.
삭제 후 배열 리스트의 내용:
JavaFX. Java. WebGL.

2. Iterator의 remove() 메서드 활용

순회 중에 요소를 삭제해야 한다면 컬렉션의 remove() 대신 Iterator가 제공하는 remove() 메서드를 사용하면 안전하게 삭제할 수 있습니다. 이 방식은 내부 카운터를 함께 갱신하기 때문에 예외가 발생하지 않습니다.

Iterator<String> it = list.iterator();
while(it.hasNext()) {
    String element = it.next();
    if(element.equals("OpenCV")) {
        it.remove(); // 안전한 삭제
    }
}

3. 동시성 지원 컬렉션 사용

멀티스레드 환경이라면 CopyOnWriteArrayList, ConcurrentHashMap과 같은 java.util.concurrent 패키지의 동시성 컬렉션을 사용하는 것도 좋은 대안입니다. 이러한 컬렉션은 순회 중 수정이 발생해도 ConcurrentModificationException을 던지지 않도록 설계되어 있습니다.

정리

  • 순회 중 컬렉션 구조를 직접 수정하면 ConcurrentModificationException이 발생한다.
  • 수정 후에는 반드시 새로운 Iterator 객체를 다시 얻어 사용한다.
  • 순회 중 삭제가 필요하면 Iterator의 remove() 메서드를 활용한다.
  • 멀티스레드 환경에서는 synchronized 동기화 또는 동시성 지원 컬렉션(CopyOnWriteArrayList 등)을 사용한다.