본문으로 건너뛰기
개발 머꼬
개발 노트Java
hohyeon.dev31

for문 안에서 리스트 원소를 지웠더니 ConcurrentModificationException이 난 이유

  • #Collections
  • #Debugging
  • #Engineering Note
  • #Java

문제 발생

조건에 맞는 항목을 지우려고 반복문 안에서 remove를 호출했더니 예외가 났습니다. 스레드는 하나뿐이었습니다.

for (Order order : orders) {
    if (order.isCancelled()) {
        orders.remove(order);   // ConcurrentModificationException
    }
}

원인 분석

이름에 "Concurrent"가 들어 있어서 멀티스레드 문제로 오해하기 쉽지만, 자바독이 명확히 합니다 — 이 예외가 항상 다른 스레드에 의한 동시 수정을 뜻하지는 않습니다. 한 스레드가 객체의 계약을 어기는 호출 순서를 실행해도 발생할 수 있습니다. 반복 중에 컬렉션을 직접 수정하는 것이 정확히 그 경우입니다.

향상된 for문은 내부적으로 Iterator를 씁니다. JRE의 범용 컬렉션 구현이 제공하는 이터레이터는 fail-fast입니다 — 자바독의 설명대로 불특정 시점에 임의의 비결정적 동작이 일어날 위험을 감수하는 대신, 빠르고 깔끔하게 실패합니다.

여기에 중요한 단서가 붙습니다. 자바독은 fail-fast 동작이 보장될 수 없으며 최선 노력(best-effort) 이라고 못박고, 이 예외에 의존하는 프로그램을 작성하는 것은 잘못이며 오직 버그를 탐지하는 용도로만 써야 한다고 적습니다.

해결 방안

  1. 조건 삭제는 removeIf가 가장 단순합니다.
orders.removeIf(Order::isCancelled);
  1. 반복하면서 지워야 하면 이터레이터의 remove를 씁니다. 이터레이터 자신을 통한 수정은 허용됩니다.
Iterator<Order> it = orders.iterator();
while (it.hasNext()) {
    if (it.next().isCancelled()) {
        it.remove();
    }
}
  1. 새 컬렉션을 만드는 방법도 있습니다. 원본을 건드리지 않아 의도가 분명하고, 결과가 불변이어도 되는 자리라면 이쪽이 낫습니다.
List<Order> active = orders.stream().filter(o -> !o.isCancelled()).toList();
  1. 여러 스레드가 함께 쓰는 컬렉션이라면 자료구조를 바꿉니다. synchronized 래퍼나 CopyOnWriteArrayList, ConcurrentHashMap 같은 동시성 컬렉션을 씁니다. 이 예외가 안 나온다고 스레드 안전한 것은 아닙니다 — 최선 노력이라 놓칠 수 있습니다.
  2. Map을 순회하며 수정할 때도 같습니다. 키 집합을 순회하면서 map.remove(key)를 호출하면 같은 예외가 납니다. entrySet().removeIf(...)나 이터레이터의 remove를 씁니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.