본문으로 건너뛰기
김신건의 로그

[Java] Fail-Fast Iterator

· 수정 · 📖 약 2분 · 865자/단어 #java #collection #iterator #concurrency #fail-fast
fail-fast iterator, fail-fast, fail fast, fail-fast 이터레이터, fail-fast 순회, modCount

정의

Fail-Fast Iterator순회 도중 컬렉션의 구조가 변경되면 즉시 ConcurrentModificationException 을 던지는 iterator. List, Set, Map 등 대부분의 java.util 컬렉션의 iterator 가 이 방식.

목적은 단순하다, 나중에 발견되면 디버깅이 끔찍하게 어려울 정합성 위배를, 가능한 가장 이른 시점에 노출 시키는 것이다. 실패 자체를 빠르게 만든다는 뜻에서 fail-fast.

java.util.concurrent 의 컬렉션 (CopyOnWriteArrayList, ConcurrentHashMap, ConcurrentLinkedQueue 등) 은 일부러 fail-fast 가 아닌 weakly consistent / snapshot iterator 를 쓴다.

메커니즘, modCount

AbstractList, ArrayList, LinkedList, Vector, HashMap 등은 내부에 modCount 라는 정수 필드를 가진다.

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
    protected transient int modCount = 0;
    // 구조 변경 메서드마다 modCount++
}
  • 구조 변경 (structural modification) 이 일어날 때마다 modCount++
  • iterator 생성 시 expectedModCount = modCount 로 스냅샷
  • next(), remove() 등 호출마다 expectedModCount == modCount 검사
  • 다르면 throw new ConcurrentModificationException()
// ArrayList.Itr (단순화)
private class Itr implements Iterator<E> {
    int cursor;
    int lastRet = -1;
    int expectedModCount = modCount;   // ← 스냅샷

    public E next() {
        checkForComodification();      // ← 매번 검사
        // ... return element ...
    }

    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

구조 변경이란

modCount 가 증가하는 작업은 컬렉션의 크기를 바꾸거나 내부 배열을 새로 할당하는 작업.

  • add(E), add(int, E), addAll(...)
  • remove(int), remove(Object), removeAll(...), clear()
  • ArrayList.trimToSize(), ensureCapacity(int)
  • LinkedHashMap 등의 put() 으로 새 키 추가

set(int, E) 처럼 기존 원소를 교체만 하는 작업은 구조 변경이 아니다. 따라서 fail-fast 가 발생하지 않는다.

List<Integer> list = new ArrayList<>(List.of(1, 2, 3));
for (Integer x : list) {
    list.set(0, 99);   // ✓ OK, 구조 변경 아님
}

트리거 예시

단일 스레드의 자기 수정

List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4));
for (Integer x : list) {
    if (x == 2) list.remove(x);  // ❌ CME
}

순회 중 자기 자신을 수정해도 발생. 멀티스레드만의 문제가 아니다.

안전한 패턴

// ✓ Iterator.remove() 만 fail-fast 영향 없음
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
    if (it.next() == 2) it.remove();
}

// ✓ Java 8+: Collection.removeIf
list.removeIf(x -> x == 2);

// ✓ 복사본을 순회하면서 원본 수정
for (Integer x : new ArrayList<>(list)) {
    if (x == 2) list.remove(x);
}

멀티스레드에서

다른 스레드가 add/remove 하면 modCount 가 증가, 내 iterator 가 다음 next() 에서 CME. 단, fail-fast 는 best-effort 검사, 절대 보장이 아니다.

CAUTION

fail-fast 가 CME 를 던지지 않는다고 해서 안전한 것은 아니다. modCount 검사는 동기화 없이 수행되므로, 두 스레드가 동시에 수정/순회하면 modCount 조차 비정상 상태로 보일 수 있다. CME 가 발생하지 않은 채 잘못된 결과 (배열 인덱스 ArrayIndexOutOfBoundsException, NullPointerException, 무한 루프) 가 나올 수 있다.

진짜 동시성 안전이 필요하면 CopyOnWriteArrayList / ConcurrentHashMap 또는 외부 동기화.

fail-fast vs weakly consistent vs snapshot

종류컬렉션동시 수정 시 동작보는 데이터
fail-fastArrayList, LinkedList, Vector, HashMapCME 즉시 던짐 (best-effort)수정 전 상태
weakly consistentConcurrentHashMap, ConcurrentLinkedQueue예외 없음, 순회 계속일부 수정 반영 가능
snapshotCopyOnWriteArrayList, CopyOnWriteArraySet예외 없음, 순회 계속iterator 생성 시점 (이후 수정 안 보임)

왜 fail-fast 인가

처음 보면 “왜 일부러 예외를 던지는 것이지?” 싶다. 대안 (silent corruption) 이 훨씬 무서운 디버깅 경험을 만들기 때문이다.

  • CME 가 없다면: 순회가 무한 루프, 같은 원소 두 번 방문, 잘못된 인덱스 접근 등 진단 불가능한 증상으로 나타난다. 며칠을 디버깅하다 결국 다른 스레드가 컬렉션을 건드린 것을 발견.
  • CME 가 있으면: 즉시 스택 트레이스로 위치 파악, 수정 누가 한 것인지 추적 가능.

IMPORTANT

CME 는 적이 아니다, 친구다. CME 가 발생하면 코드의 동시성 가정이 틀렸다는 신호. 무시하거나 catch 해서 삼키지 말고, 동시성 컬렉션을 쓰거나 동기화를 추가하라.

참고

이 글의 용어 (6개)
[Java] ArrayListjava
정의 는 동적 배열 (dynamic array) 로 구현된 . 내부적으로 를 들고 다니며, 용량이 부족해지면 새 배열로 옮긴다. Java 의 가장 흔한 컬렉션 구현체. 인덱스 기…
[Java] ConcurrentModificationExceptionjava
정의 (CME) 는 가 컬렉션의 구조 변경을 탐지했을 때 던지는 unchecked 예외 ( ). 이름과 달리 반드시 동시성 (멀티스레드) 문제일 때만 발생하는 것이 아니다. 단일…
[Java] CopyOnWriteArrayListjava
정의 는 쓰기 시 배열 전체를 복사 하는 thread-safe 구현. 읽기에는 lock 이 전혀 없고, 쓰기에는 으로 직렬화한다. (JSR-166) 의 컬렉션. 읽기 압도적 다,…
[Java] LinkedListjava
정의 는 양방향 연결 리스트 (doubly linked list) 로 구현된 이자 . 각 원소는 와 포인터를 가진 노드로 보관된다. 와 같은 List 이지만, 메모리 레이아웃과 …
[Java] Listjava
정의 는 순서가 있고 중복이 허용 되는 컬렉션을 표현하는 인터페이스. 각 원소는 0 부터 시작하는 정수 인덱스로 접근 가능. 인터페이스를 확장하며, 의 모든 성질을 물려받는다. …
Geometry Basic: 벡터, CCW, 외적, 내적algorithm
정의 기하 알고리즘의 밑바탕. 대부분의 2D/3D 기하 문제는 벡터 연산, 내적/외적, CCW 판정 세 가지의 조합으로 풀립니다. 벡터 2D 벡터: 두 점 $A = (ax, ay…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기