코드 한 줄의 기록

Java Iterator와 Fail-Fast, 컬렉션 수정 시 주의할 점들 본문

JAVA

Java Iterator와 Fail-Fast, 컬렉션 수정 시 주의할 점들

CodeByJin 2025. 10. 27. 18:46
반응형

자바로 개발하다 보면 리스트나 셋 같은 컬렉션을 순회하면서 요소를 추가하거나 삭제해야 할 때가 있다. 그런데 이 과정에서 ConcurrentModificationException이라는 예외를 만나본 적 있지 않은가? 처음엔 당황스럽지만, 이 예외는 자바 컬렉션의 안전장치로서 중요한 역할을 한다. 오늘은 Iterator의 개념부터 fail-fast 메커니즘, 그리고 컬렉션을 안전하게 수정하는 방법까지 함께 살펴보려 한다.

Iterator란 무엇인가?

Iterator는 자바 컬렉션 프레임워크에서 컬렉션의 요소들을 순차적으로 읽어오기 위한 표준 인터페이스다. 쉽게 말해, 리스트나 셋처럼 여러 데이터를 담고 있는 자료구조를 하나씩 탐색할 수 있게 해주는 도구라고 생각하면 된다.
Iterator는 다음 세 가지 핵심 메서드를 제공한다.

  • hasNext(): 다음에 읽어올 요소가 있는지 확인한다.
  • next(): 다음 요소를 반환하고 커서를 한 칸 이동시킨다.
  • remove(): next()로 가져온 마지막 요소를 컬렉션에서 삭제한다.
List<String> list = new ArrayList<>();
list.add("사과");
list.add("바나나");
list.add("포도");

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String fruit = iterator.next();
    System.out.println(fruit);
}

 
Iterator의 진짜 장점은 모든 컬렉션 타입에 공통으로 사용할 수 있다는 점과, 순회 중 안전하게 요소를 삭제할 수 있다는 점이다.

Fail-Fast 메커니즘의 이해

자바 컬렉션의 대부분(ArrayList, HashMap 등)은 fail-fast 방식으로 동작한다. 문제 발생 시 즉시 예외를 던져 이상한 상태로 진행되지 않게 한다.

modCount와 expectedModCount

Fail-fast의 핵심은 modCountexpectedModCount이다.

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

 
Iterator 생성 후 컬렉션 구조가 변경되면 위 메서드에서 예외를 던진다.

왜 이렇게 엄격할까?

순회 중 구조가 변경되면 인덱스가 밀려서 엉뚱한 데이터를 읽을 수 있다. Fail-fast는 “더 이상 안전하지 않으니 중단하라”는 경고다.

ConcurrentModificationException 발생 상황

1. 단일 스레드 환경

for (String item : list) {
    if (item.equals("B")) {
        list.remove(item);  // 예외 발생
    }
}

 
향상된 for문은 내부적으로 Iterator를 사용하므로, 직접 컬렉션을 수정하면 예외가 발생한다.
 

2. 멀티스레드 환경

List<String> list = new ArrayList<>();
new Thread(() -> {
    for (String s : list)
        System.out.println(s);
}).start();

new Thread(() -> list.add("D")).start();

 
다른 스레드가 동시에 수정 시 예외 발생.

안전하게 컬렉션 수정하는 방법

1. Iterator.remove() 사용

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    if (iterator.next().equals("B")) {
        iterator.remove();
    }
}

 
2. 역순 for문 사용

for (int i = list.size() - 1; i >= 0; i--) {
    if (list.get(i).equals("B")) list.remove(i);
}

 
3. removeIf()

list.removeIf(item -> item.equals("B"));

 
4. 복사본 후 변경

List<String> copy = new ArrayList<>(list);
for (String s : copy) {
    if (s.equals("B")) list.remove(s);
}

 
5. CopyOnWriteArrayList 사용

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for (String s : list) {
    if (s.equals("B")) list.remove(s); // 안전
}

 
fail-safe 방식으로 수정 시에도 예외가 발생하지 않는다.

향상된 for문의 함정

for (String s : list) {  // 내부적으로 iterator 사용
    list.remove(s);      // 예외 발생
}

Stream 주의

list.stream()
    .forEach(s -> list.remove(s)); // 위험

 

Stream의 forEach()는 내부적으로 Iterator를 사용한다. 대신 filter()를 이용하자.

ListIterator로 양방향 순회

ListIterator<String> it = list.listIterator();
while (it.hasNext()) {
    if (it.next().equals("B")) {
        it.set("BB");
        it.add("B2");
    }
}

 

컬렉션 순회 중에는 Iterator.remove() 또는 removeIf() 사용

향상된 for문은 내부적으로 Iterator 사용

멀티스레드 환경에서는 CopyOnWriteArrayList 고려

자료구조 특성에 맞게 순회 방식을 선택
 
이 개념들을 이해하면 컬렉션을 다룰 때 훨씬 안전하고 예측 가능한 코드를 작성할 수 있다.

Java Queue, Deque, PriorityQueue 완벽 가이드

코딩 테스트를 준비하다 보면 Queue 관련 자료구조를 정말 많이 사용하게 됩니다. 특히 BFS 알고리즘이나 우선순위 처리 같은 문제에서 필수적이죠. 저도 처음엔 이 세 가지가 뭐가 다른지 헷갈렸

byteandbit.tistory.com

반응형