[Java] ConcurrentModificationException 원인 및 해결방법

하원·2025년 3월 24일
post-thumbnail

안녕하세요, 하원입니다.
오늘은 제가 겪었던 ConcurrentModificationException 오류에 대해 소개해 보겠습니다.

이 오류는 알고리즘 문제를 푸는 도중에 발생했는데요, 자세히 살펴보겠습니다.


문제 상황

        List<String> words = new ArrayList<>();

        words.add("A");
        words.add("B");
        words.add("C");
        words.add("D");

        for (String word : words) {
            if (word.equals("A")) {
                words.remove(word);
            }
        }

위 코드는 String 타입 List에 A, B, C, D라는 값을 추가한 이후, List를 순회하면서 A라는 값을 찾을 때 List에서 삭제하는 간단한 코드입니다.

실행하면 어떤 결과가 나올까요?

위 사진과 같이 ConcurrentModificationException이 발생하게 됩니다.

근데 신기한 점은 A, B, D를 삭제하면 오류가 발생하고, C를 삭제하면 정상적으로 동작합니다.
왜 이런 상황이 발생한 걸까요?


원인

결과적으로는 remove 동작 과정에서 modCount 변수의 불일치 때문에 발생한 것입니다.
단계적으로 설명해 보겠습니다. 먼저 modCount 변수가 무엇인지 설명하겠습니다.

modCount 변수

  • ArrayList, LinkedList 등과 같은 리스트 내부에서 새로운 요소가 추가되거나 삭제될 때 modCount라는 변수가 증가합니다.
  • 즉, modCount는 리스트가 변경되었음을 감지할 수 있게 해주는 변수입니다.

오류 발생 과정

1. for-each문이 실행될 때, Iterator가 생성

  • for-each문은 Iterator를 사용하여 리스트를 순회합니다.
  • Iterator는 반복을 시작하기 전, modCount 값을 저장해둡니다.

2. words.remove(word) 실행

  • remove()가 호출되면, ArrayList 내부에 있던 modCount 변수가 증가하게 됩니다.

3. Iterator가 리스트의 다음 요소를 가져올 때, modCount 불일치 감지

  • Iterator는 다음 요소를 가져올 때마다(= next() 호출) 초기에 저장한 modCount와 현재 modCount 값을 비교합니다.
  • 만약, modCount 값이 서로 다르다면, ConcurrentModificationException을 발생시킵니다.

Iterator 살펴보기

        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
  • 위 코드는 Iterator의 next()checkForComodification() 함수입니다.
  • next() 코드를 살펴보면, 첫 번째 줄에 checkForComodification()이 실행되는 것을 볼 수 있습니다.

checkForComodification()

  • modCount 변수를 비교하는 함수입니다.
  • expectedModCount : Iterator가 생성될 때 초기에 저장한 modCount 값
  • modCount: 리스트가 변경되었을 때마다 증가하는 값
  • 위 2개의 변수가 불일치하면, ConcurrentModificationException을 발생시키게 됩니다.

해결 방법

1. break 사용

        for (String word : words) {
            if (word.equals("A")) {
                words.remove(word);
                break;
            }
        }
  • 요소를 삭제한 이후 break문을 통해 for-each문을 종료시켜 버리는 겁니다.
  • 하지만 삭제할 요소가 여러 개 있을 경우, 이 방법을 사용하면 안 됩니다.

2. Iterator의 remove() 사용

Iterator<String> iterator = words.iterator();
while (iterator.hasNext()) {
    String str = iterator.next();
    if ("A".equals(str)) {
        iterator.remove();
    }
}
  • 가장 권장되는 방법이라고 합니다.
  • iterator.remove() 코드는 내부적으로 expectedModCount 변수를 함께 업데이트 하기 때문에
    예외 발생 없이 삭제할 수 있게 됩니다.

3. removeIf() 사용

words.removeIf(str -> "A".equals(str));
  • Java 8 이상에서 사용할 수 있는 removeIf() 함수입니다.
  • removeIf()는 내부적으로 modCount를 직접 업데이트 하기 때문에 예외가 발생하지 않습니다.

기본기의 중요성

사실 코딩 테스트를 진행하던 도중 ConcurrentModificationException 오류가 발생해버렸습니다. 검색도 불가능해서 해결 방안을 찾지 못했고, 결국 코드를 완성하지 못한 채 제출하게 되었습니다. 문제를 몰라서 못 푼 것보다 이러한 프로그래밍 언어의 미숙으로 인해 못 푼 것이 더 속상한 것 같습니다. 프로젝트를 통해 경험을 쌓는 것도 중요하지만, 자바와 같은 기본기를 더 깊이 있게 다룰 줄 아는 것도 꽤나 중요하다는 것을 느끼게 되었습니다.


참고

profile
호기심 저장소

0개의 댓글