[Java] ConcurrentModificationException 원인 분석 및 해결

yerim·2025년 1월 7일

트러블슈팅

목록 보기
2/2
post-thumbnail

1. 배경


Java Collection 프레임워크를 공부하면서 ArrayList를 사용하고 있었다. 그런데, ArrayList 타입인 afterSet 의 홀수만 제거하는 for-each문을 실행해보니 ConcurrentModificationException 예외가 발생했다.

Set<Integer> integerSet = new HashSet<>(integerList);
List<Integer> afterSet = new ArrayList<>(integerSet);

System.out.println("pre: " + afterSet);
afterSet.sort(Comparator.reverseOrder());
System.out.println("aft: " + afterSet);

for (int num: afterSet){
    if (num % 2 != 0) {
        Integer temp = afterSet.indexOf(num);
        afterSet.remove(temp);
    }
}
Exception in thread "main" java.util.ConcurrentModificationException
    at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
    at java.base/java.util.ArrayList$Itr.next(ArrayList.java:967)
    at Collections.Practice.main(Practice.java:33)



2. 원인


ConcurrentModificationException 예외가 어디서 나는지 찾아보고자 디버깅을 진행했다. 그 결과 홀수가 제거되고 난 후, 다음 배열 원소에 접근하려고 할 때, 예외가 발생하는 것을 확인하였다.

2.1 동시성 문제

ArrayList가 반복하는 동안 구조적 수정이 이루어질 때, 동시성 문제를 방지하기 위해 예외가 발생한다.

ConcurrentModificationException
객체의 동시 수정이 허용되지 않을 때 발생하는 예외이다. 일반적으로 한 스레드가 컬렉션을 수정하는 동안 다른 스레드가 수정하는 것은 허용되지 않기 때문이다. 공유 자원에 여러 작업이 동시에 진행될 때, 발생할 수 있는 문제를 방지하기 위함이다.


2.2 for-each 문의 ConcurrentModificationException 발생 과정

for-each 순회에서 다음 요소에 접근하려고 할 때 ConcurrentModificationException 예외가 발생했는데, 그렇다면 동시성 문제가 발생한 것으로 보아야할까?

  • 처리 과정을 확인하고 싶어 문제 메소드 부분을 찾아보았다.
  1. List 는 modCount 를 사용하여 인스턴스의 구조적으로 수정된 횟수를 관리한다.

    // AbstractList.java
    protected transient int modCount = 0;
  2. ArrayList 는 순회 중 인덱스를 이동하는 next() 메소드에서 checkForComodification() 를 사용하여 변경을 감지한다.

        // ArrayList.java
        public E next() {
              checkForComodification();
              ...
        }
        
        final void checkForComodification() {
                    if (modCount != expectedModCount)
                        throw new ConcurrentModificationException();
        }
    
    • ArrayList 는 인덱스를 이동하는 과정에서 modCountexpectedModcount를 비교하여 배열이 구조적으로 변경이 일어났는지 확인한다.
      • modCount : 구조적으로 수정된 횟수
      • expectedModcount : 순회을 시작하며 저장한 수정 횟수

  1. 즉, 순회 중 구조적인 변경을 일으키는 add()remove() 를 사용한다면, 인덱스 이동 과정에서 동시 수정 문제를 확인하고 예외를 발생시킨다.

    • remove() -> fastRemove() -> modCount 증가 -> checkForComodification() -> ConcurrentModificationException 발생
        // ArrayList.java
        // remove() -> fashRemove() 호출
        private void fastRemove(Object[] es, int i) {
              modCount++;	// 구조적 수정 횟수 증가
              final int newSize;
              if ((newSize = size - 1) > i)
                  System.arraycopy(es, i + 1, es, i, newSize - i);
              es[size = newSize] = null;
          }


3.3 ❗[중요]

처음에 코드를 보았을 때는, for-each 뿐만 아니라 ArrayList의 모든 반복문에서 문제가 발생한다고 생각했다. 하지만 관련 글과 코드를 다시보니 동시성 문제라기보다는 for-each 문의 특징 때문이다.

  • for 문 안 remove() : for문과 인덱스로 접근하니 예외가 발생하지 않음
    // Exception이 발생하지 않음.
    for (int i = 0; i < afterSet.size(); i++) {
            if (afterSet.get(i) % 2 != 0) {
                afterSet.remove(afterSet.get(i));
            }
    }
    System.out.println(afterSet);

for-each 실행 시 컬렉션의 Iterator이 생성된다.

  • Iterator 는 순회 과정에서 컬렉션의 변경 여부를 감지할 수 있도록 설계되었다.
    -> 위에 2.2 for-each 의 ConcurrentModificationException 발생 과정 에서 설명했던 코드가 ArrayList.java 안에 구현된 private class Itr implements Iterator<E> 구현 클래스였다.
  • Iterator은 '읽기 전용' 으로 컬렉션을 읽는 작업만 가능하고, 구조적 변경(삽입, 삭제, 변경)을 허용하지 않는다



3. 해결


결국 기존 ArrayList 를 그대로 사용하며 수정하려 했기 때문에 발생한 문제이다. 결과값을 도출해내기 위한 ArrayList 인스턴스 evenList를 새로 생성하여, 저장하였다.

    List<Integer> evenList = new ArrayList<>();
        for (int num: afterSet){
            if (num % 2 == 0) {
                evenList.add(num);
            }
        }
    System.out.println(evenList);
or

for-each 가 아닌 for 문을 사용할 수도 있다. 혹은 Iterator 을 직접 사용할 수 있다.

	for (int i = 0; i < afterSet.size(); i++) {
            if (afterSet.get(i) % 2 != 0) {
                afterSet.remove(afterSet.get(i));
            }
    }
    System.out.println(afterSet);



F. 정리


  • 하나의 스레드가 공유 자원에 대해 작업하고 있을 때, 다른 스레드가 접근할 경우 동시성 문제가 발생할 수 있다.

  • 하지만 for-each 문의 ConcurrentModificationException 발생은 동시성 문제보다는 '읽기 전용'의 Iterator 을 사용하기 때문에 구조적 변경이 일어났을 때, 예외를 발생시킨다.




Review


  • 개선할 점

    • 문제 부분를 찾아가며 원인을 찾아보려 했지만 코드를 너무 지엽적으로 파악한 것 같다.
    • 공식 문서를 좀 더 참고하며 찾아봐야할 것 같다.
    • 동시성 작업에 대한 이해가 부족한 것 같아 추가적으로 공부하려고 한다.
  • 잘한 점

    • 오늘 궁금했던 점을 바로 공부하고 정리했다.
    • 코드를 분석하면서 보려고 노력했다.
profile
쌓아가는 중

0개의 댓글