for문 안에서 리스트 원소를 지웠더니 ConcurrentModificationException이 난 이유
문제 발생
조건에 맞는 항목을 지우려고 반복문 안에서 remove를 호출했더니 예외가 났습니다. 스레드는 하나뿐이었습니다.
for (Order order : orders) {
if (order.isCancelled()) {
orders.remove(order); // ConcurrentModificationException
}
}원인 분석
이름에 "Concurrent"가 들어 있어서 멀티스레드 문제로 오해하기 쉽지만, 자바독이 명확히 합니다 — 이 예외가 항상 다른 스레드에 의한 동시 수정을 뜻하지는 않습니다. 한 스레드가 객체의 계약을 어기는 호출 순서를 실행해도 발생할 수 있습니다. 반복 중에 컬렉션을 직접 수정하는 것이 정확히 그 경우입니다.
향상된 for문은 내부적으로 Iterator를 씁니다. JRE의 범용 컬렉션 구현이 제공하는 이터레이터는 fail-fast입니다 — 자바독의 설명대로 불특정 시점에 임의의 비결정적 동작이 일어날 위험을 감수하는 대신, 빠르고 깔끔하게 실패합니다.
여기에 중요한 단서가 붙습니다. 자바독은 fail-fast 동작이 보장될 수 없으며 최선 노력(best-effort) 이라고 못박고, 이 예외에 의존하는 프로그램을 작성하는 것은 잘못이며 오직 버그를 탐지하는 용도로만 써야 한다고 적습니다.
해결 방안
- 조건 삭제는
removeIf가 가장 단순합니다.
orders.removeIf(Order::isCancelled);- 반복하면서 지워야 하면 이터레이터의
remove를 씁니다. 이터레이터 자신을 통한 수정은 허용됩니다.
Iterator<Order> it = orders.iterator();
while (it.hasNext()) {
if (it.next().isCancelled()) {
it.remove();
}
}- 새 컬렉션을 만드는 방법도 있습니다. 원본을 건드리지 않아 의도가 분명하고, 결과가 불변이어도 되는 자리라면 이쪽이 낫습니다.
List<Order> active = orders.stream().filter(o -> !o.isCancelled()).toList();- 여러 스레드가 함께 쓰는 컬렉션이라면 자료구조를 바꿉니다.
synchronized래퍼나CopyOnWriteArrayList,ConcurrentHashMap같은 동시성 컬렉션을 씁니다. 이 예외가 안 나온다고 스레드 안전한 것은 아닙니다 — 최선 노력이라 놓칠 수 있습니다. Map을 순회하며 수정할 때도 같습니다. 키 집합을 순회하면서map.remove(key)를 호출하면 같은 예외가 납니다.entrySet().removeIf(...)나 이터레이터의remove를 씁니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.