0Pricing
Java Academy · 강의

빠른 실패 반복자와 안전한 반복자

ConcurrentModificationException과 빠른 실패 동작을 이해하고 스냅샷 반복자를 사용할 시점을 학습합니다.

빠른 실패 반복자와 안전한 반복자은(는) CoddyKit의 무료 Java Academy 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Java Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Java Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

즉시 실패형 반복자와 안전형 반복자

Java 반복자는 순회 중 기반 컬렉션이 수정될 때의 동작에 따라 분류됩니다. 즉시 실패형 반복자는 즉시 예외를 발생시키고, 안전형 반복자는 스냅샷을 기준으로 계속 진행합니다.

즉시 실패형 반복자

대부분의 표준 Java 컬렉션(ArrayList, HashMap, TreeSet)은 즉시 실패형 반복자를 사용합니다. 반복자는 modCount라는 변경 횟수 카운터를 추적합니다. 순회 중 구조적 수정이 발생하면 ConcurrentModificationException이 발생합니다.

import java.util.*;

List<String> list = new ArrayList<>(List.of("a", "b", "c"));

try {
    for (String s : list) {
        list.add("x"); // structural modification — throws!
    }
} catch (ConcurrentModificationException e) {
    System.out.println("ConcurrentModificationException caught!");
}
// Same for HashMap, TreeMap, HashSet, etc.

즉시 실패형이 존재하는 이유

즉시 실패형 동작은 디버깅을 돕습니다. 손상된 상태로 순회를 계속 허용하는 대신 버그를 즉시 드러내기 때문입니다. 반대로 조용히 데이터가 손상되면 디버깅이 훨씬 어렵습니다.

// modCount is incremented on every structural change
// (add, remove, clear on ArrayList)
// Iterator records modCount on creation
// On each next(), Iterator checks: if current modCount != expected, throw CME

// This detects bugs like:
List<Integer> nums = new ArrayList<>(List.of(1,2,3,4,5));
for (Integer n : nums) {
    if (n == 3) nums.remove(n); // bug caught immediately
}
// ConcurrentModificationException — not silent wrong results

안전한 삭제: Iterator.remove()

Iterator 기반 순회 중 안전하게 수정할 수 있는 유일한 방법은 Iterator.remove()입니다. 이 메서드는 마지막으로 반환된 요소를 삭제하고 modCount를 갱신합니다.

List<Integer> nums = new ArrayList<>(List.of(1,2,3,4,5,6));
Iterator<Integer> it = nums.iterator();

while (it.hasNext()) {
    int n = it.next();
    if (n % 2 == 0) it.remove(); // safe: updates modCount
}

System.out.println(nums); // [1, 3, 5]

removeIf: 현대적인 대안

Java 8에서는 Collection에 removeIf(Predicate)를 추가했습니다. 반복자를 직접 관리하지 않고 조건에 일치하는 요소를 삭제하는 더 깔끔한 방법입니다.

List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Ann", "Charlie"));

// Modern: removeIf handles iteration internally
names.removeIf(name -> name.startsWith("A"));
System.out.println(names); // [Bob, Charlie]

// Equivalent but verbose iterator approach:
Iterator<String> it = names.iterator();
while (it.hasNext()) {
    if (it.next().startsWith("A")) it.remove();
}

안전형 반복자: CopyOnWriteArrayList

CopyOnWriteArrayList는 안전형 반복자를 사용합니다. 반복자를 만들 때 가져온 스냅샷을 순회하므로 수정이 진행 중인 순회에 영향을 주지 않습니다.

import java.util.concurrent.CopyOnWriteArrayList;

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.addAll(List.of("a", "b", "c"));

for (String s : list) {
    list.add("x"); // no ConcurrentModificationException!
    System.out.print(s + " "); // prints a, b, c (snapshot)
}
System.out.println();
System.out.println(list); // [a, b, c, x, x, x] — modified copy

ConcurrentHashMap 반복자

ConcurrentHashMap은 약한 일관성을 유지하는 반복자를 사용합니다. 이 반복자는 만들어진 시점 또는 그 이후 어느 시점의 상태를 반영하며, CME를 발생시키지 않습니다. 동시에 발생한 업데이트를 볼 수도 있고 보지 못할 수도 있습니다.

import java.util.concurrent.ConcurrentHashMap;

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("a", 1); map.put("b", 2); map.put("c", 3);

for (Map.Entry<String, Integer> e : map.entrySet()) {
    map.put("d", 4); // no CME — weakly consistent
    System.out.println(e.getKey() + "=" + e.getValue());
}
// May or may not print "d=4" — weakly consistent

Collections.synchronizedList

Collections.synchronizedList는 NOT 안전형이 아닙니다. 여전히 즉시 실패형 반복자를 사용하지만 동시 접근을 차단합니다. 순회 중에는 직접 동기화해야 합니다.

import java.util.*;

List<String> syncList = Collections.synchronizedList(new ArrayList<>());
syncList.addAll(List.of("a", "b", "c"));

// Must synchronize during iteration!
synchronized (syncList) {
    for (String s : syncList) {
        System.out.println(s);
    }
}
// Without the synchronized block, CME is still possible from another thread

스냅샷 반복자 패턴

동기화하지 않고도 변경 가능한 컬렉션을 안전하게 순회할 수 있도록 스냅샷을 직접 만듭니다.

import java.util.*;

List<String> original = new ArrayList<>(List.of("a", "b", "c"));

// Take a snapshot copy before iterating
List<String> snapshot = List.copyOf(original);

for (String s : snapshot) {
    // Safe to modify original during snapshot iteration
    original.remove(s);
    System.out.println("Removed: " + s);
}
System.out.println(original); // []

CopyOnWrite의 장단점

CopyOnWriteArrayList는 안전하지만 쓰기 작업 비용이 큽니다. 수정할 때마다 전체 복사본을 만듭니다.

  • 적합한 경우: 읽기가 많고 쓰기가 드문 경우(이벤트 수신기 목록)
  • 부적합한 경우: 수정이 잦거나 컬렉션이 큰 경우

요약: 즉시 실패형과 안전형

비교 요약:

  • 즉시 실패형: ArrayList, HashMap, TreeMap — 수정 시 CME를 발생시키며 modCount를 확인합니다.
  • 안전형/약한 일관성 유지: ConcurrentHashMap, CopyOnWriteArrayList — CME를 발생시키지 않으며 스냅샷 또는 동시 데이터를 순회합니다.
  • 안전한 수정 메서드: Iterator.remove(), removeIf(), replaceAll()

실전: 만료된 Session 정리

적절한 패턴을 사용해 동시 접근 리스트에서 만료된 Session을 안전하게 삭제합니다.

import java.util.*;
import java.util.concurrent.*;

class SessionManager {
    private final CopyOnWriteArrayList<Session> sessions = new CopyOnWriteArrayList<>();

    record Session(String id, long expiresAt) {
        boolean isExpired() { return System.currentTimeMillis() > expiresAt; }
    }

    void add(Session s) { sessions.add(s); }

    // Safe to call from multiple threads while iterating
    void purgeExpired() {
        sessions.removeIf(Session::isExpired);
    }

    List<Session> active() {
        return sessions.stream().filter(s -> !s.isExpired()).toList();
    }
}

빠른 확인

순회 중인 ArrayList를 수정하면 어떤 예외가 발생합니까?

복습: 즉시 실패형과 안전형 반복자

핵심 내용:

  • 즉시 실패형 반복자(ArrayList, HashMap)는 동시 수정이 발생하면 ConcurrentModificationException을 발생시킵니다.
  • 안전형 반복자(CopyOnWriteArrayList, ConcurrentHashMap)는 예외를 발생시키지 않고 수정을 허용합니다.
  • 즉시 실패형 순회 중 삭제하는 안전한 방법은 Iterator.remove()뿐입니다.
  • removeIf()는 Iterator.remove()를 직접 사용하는 방식보다 현대적이고 깔끔한 대안입니다.
  • CopyOnWriteArrayList는 읽기가 많고 쓰기가 드문 상황에 안전합니다.
  • ConcurrentHashMap은 약한 일관성을 유지합니다. CME가 발생하지 않으며 동시 수정 내용을 볼 수도 있고 보지 못할 수도 있습니다.

자주 묻는 질문

“빠른 실패 반복자와 안전한 반복자” 강의는 무료인가요?

네 — “빠른 실패 반복자와 안전한 반복자” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Java Academy 강의 전체를 잠금 해제할 수 있습니다. Java Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“빠른 실패 반복자와 안전한 반복자”에서 뭘 배우나요?

ConcurrentModificationException과 빠른 실패 동작을 이해하고 스냅샷 반복자를 사용할 시점을 학습합니다. 브라우저에서 직접 실행하는 실습 코드로 Java Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Java Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Java Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.

“빠른 실패 반복자와 안전한 반복자” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Java Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Java Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. Iterable과 Iterator 계약
  2. 사용자 정의 반복자 구현
  3. ListIterator와 양방향 순회
  4. 빠른 실패 반복자와 안전한 반복자
← Java Academy(으)로 돌아가기