2주차 Unit 6.2 — Iterator 패턴의 본질

Psj·2026년 5월 18일

F-lab

목록 보기
74/230

Unit 6.2 — Iterator 패턴의 본질

F-LAB JAVA · 2주차 · Phase 6 · Reflection & Iterator


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • Iterator 패턴 이 해결한 문제는 무엇인가?
  • for-each 가 내부적으로 무엇을 호출하나?
  • Iterator vs ListIterator 의 차이는?
  • fail-fastfail-safe 의 차이는?
  • ConcurrentModificationException 는 정확히 언제 발생하나?
  • Spliterator 와 Stream의 관계는?
  • ILIC 코드에서 Iterator 잘못 쓰면 어떤 사고가?

🎯 핵심 한 문장

Iterator는 "컬렉션 내부 구조를 노출하지 않고 요소를 하나씩 꺼내는 통일된 방법"이다.
ArrayList든 LinkedList든 HashSet이든 — Iterator 인터페이스 하나로 같은 코드로 순회 가능.
for-each 문은 결국 Iterator 호출의 syntactic sugar이며,
Java 8의 Stream도 Iterator의 진화형(Spliterator) 위에서 동작한다.

비유 — TV 리모컨

시스템비유
Iterable (컬렉션)TV 모델마다 다른 내부 구조
IteratorTV마다 다른 리모컨
hasNext()"다음 채널 있나?" 버튼
next()"다음 채널로" 버튼
for-each"채널 순회" 매크로 버튼
fail-fast시청 중 누가 TV 만지면 알람

→ TV 종류와 무관하게 같은 인터페이스로 다룸.


🧭 9개 섹션 로드맵

1. Iterator 패턴이 해결한 문제
2. Iterable과 Iterator의 분리
3. for-each의 실체
4. fail-fast — ConcurrentModificationException
5. ListIterator — 양방향 순회
6. Spliterator — Stream의 기반
7. ILIC 실무 — Iterator 활용
8. 흔한 실수 + 디버깅
9. 면접 + 자기 점검

1️⃣ Iterator 패턴이 해결한 문제

1.1 문제 — 컬렉션마다 다른 순회 방식

// ArrayList 순회 (인덱스)
ArrayList<Shipment> arr = ...;
for (int i = 0; i < arr.size(); i++) {
    Shipment s = arr.get(i);
}

// LinkedList 순회 (Node 따라가기)
LinkedList<Shipment> link = ...;
Node<Shipment> cur = link.first;
while (cur != null) {
    Shipment s = cur.item;
    cur = cur.next;
}

// HashSet 순회 (해시 테이블 순회)
HashSet<Shipment> set = ...;
// ... 더 복잡

→ 각 컬렉션마다 내부 구조에 의존하는 코드.
→ 컬렉션 바꾸면 코드 다시 작성.

1.2 해결 — 통일된 인터페이스

// 모든 컬렉션에 같은 코드
Iterator<Shipment> it = collection.iterator();
while (it.hasNext()) {
    Shipment s = it.next();
}

ArrayList든, LinkedList든, HashSet이든 동일.
Iterator 패턴의 본질: 내부 구조 캡슐화.

1.3 디자인 패턴 — GoF Iterator

GoF Iterator Pattern:
  목적:
    집합 객체의 내부 구조를 노출하지 않고
    그 원소들에 순차적으로 접근하는 방법 제공
  
  구성:
    - Iterator (인터페이스)
    - ConcreteIterator (구현체)
    - Aggregate (컬렉션)
    - ConcreteAggregate (구현체)

자바 표준 라이브러리는 이 패턴을 언어 차원에 통합.

1.4 두 가지 인터페이스

// 1. Iterable — "순회 가능한 것"
public interface Iterable<T> {
    Iterator<T> iterator();   // Iterator 만들어줘
}

// 2. Iterator — "순회 도구"
public interface Iterator<E> {
    boolean hasNext();
    E next();
    default void remove() { ... }
}

분리의 이점:

  • 한 컬렉션에서 여러 Iterator 동시 사용 가능
  • 각 Iterator는 자기 위치 기억
  • 컬렉션과 순회 상태 분리

1.5 시각화

ArrayList:
  ┌────────────────────────┐
  │ elementData: [A,B,C,D] │
  │ size: 4                 │
  │                         │
  │ iterator() ─────────────┼──► 새 Iterator 객체
  └────────────────────────┘    ┌──────────────┐
                                 │ cursor: 0     │
                                 │ list: ref    │
                                 │ hasNext()    │
                                 │ next()       │
                                 └──────────────┘

→ Iterator 객체는 컬렉션 위에서 움직이는 커서.


2️⃣ Iterable과 Iterator의 분리

2.1 Iterable 인터페이스

public interface Iterable<T> {
    Iterator<T> iterator();
    
    default void forEach(Consumer<? super T> action) {
        for (T t : this) {
            action.accept(t);
        }
    }
    
    default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

Iterable을 구현하면 for-each 사용 가능.

2.2 Iterator 인터페이스

public interface Iterator<E> {
    boolean hasNext();
    E next();
    
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
    
    default void forEachRemaining(Consumer<? super E> action) {
        while (hasNext()) action.accept(next());
    }
}

핵심 두 메서드:

  • hasNext(): 다음 요소 있나?
  • next(): 다음 요소 가져오기 + 커서 이동

2.3 Iterator의 상태

ArrayList<String> list = new ArrayList<>(List.of("A", "B", "C"));
Iterator<String> it = list.iterator();

it.hasNext();  // true
it.next();     // "A", cursor → 1

it.hasNext();  // true
it.next();     // "B", cursor → 2

it.hasNext();  // true
it.next();     // "C", cursor → 3

it.hasNext();  // false
it.next();     // NoSuchElementException

→ Iterator는 자기 위치를 기억.
→ 한 컬렉션에서 여러 Iterator → 각자 별개 위치.

2.4 자기 점검 답변

Iterable과 Iterator를 분리한 이유는?

:
1. 다중 Iterator 지원 — 같은 컬렉션에서 여러 순회 동시
2. 상태 분리 — 순회 상태가 컬렉션 외부에
3. 컬렉션은 자료구조, Iterator는 순회 알고리즘 — 책임 분리
4. 재진입 가능iterator() 호출 시마다 새 순회

2.5 ArrayList의 Iterator 구현

// ArrayList 내부 (간략화)
private class Itr implements Iterator<E> {
    int cursor;       // 다음 호출에서 반환할 인덱스
    int lastRet = -1; // 마지막 반환한 인덱스
    int expectedModCount = modCount;

    public boolean hasNext() {
        return cursor != size;
    }

    public E next() {
        checkForComodification();
        int i = cursor;
        if (i >= size) throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length) throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }

    public void remove() {
        if (lastRet < 0) throw new IllegalStateException();
        checkForComodification();
        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

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

모든 자바 컬렉션의 Iterator는 비슷한 패턴.


3️⃣ for-each의 실체

3.1 단순한 문법

for (Shipment s : shipments) {
    process(s);
}

3.2 컴파일 후 실체

위 코드는 컴파일 시 다음으로 변환:

Iterator<Shipment> it = shipments.iterator();
while (it.hasNext()) {
    Shipment s = it.next();
    process(s);
}

for-each는 Iterator 호출의 syntactic sugar.

3.3 배열의 for-each

int[] arr = {1, 2, 3};
for (int n : arr) { ... }

배열은 Iterable 아님. 컴파일러가 다음으로 변환:

for (int i = 0; i < arr.length; i++) {
    int n = arr[i];
    // ...
}

배열의 for-each는 인덱스 루프.
→ 객체 컬렉션의 for-each와 동작 다름.

3.4 자기 점검 답변

for-each는 무엇을 호출하는가?

:

  • 객체 컬렉션 (Iterable): iterator() 메서드 → Iterator 사용
  • 배열: 인덱스 루프 (length 사용)
  • 컴파일러가 자동 변환

3.5 forEach 메서드 — Java 8+

shipments.forEach(s -> process(s));
shipments.forEach(this::process);

Iterable.forEach(Consumer):

  • Iterator 사용
  • 람다로 더 간결

내부 구현:

default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

→ 결국 같은 Iterator 사용. 표현만 다름.

3.6 break, continue, return

for (Shipment s : shipments) {
    if (s.isInvalid()) break;        // 종료
    if (s.isSkippable()) continue;   // 다음
    if (s.isFinal()) return;          // 메서드 종료
    process(s);
}

이런 제어가 forEach 람다에선 어려움:

shipments.forEach(s -> {
    if (s.isInvalid()) break;   // ❌ 컴파일 에러
    if (s.isSkippable()) return; // ✓ 람다 종료 (=continue 효과)
});

→ break, 외부 return은 람다에서 불가.
→ 복잡한 제어 필요하면 for-each가 더 적합.


4️⃣ fail-fast — ConcurrentModificationException

4.1 시나리오 — 순회 중 변경

List<Shipment> list = new ArrayList<>();
list.add(s1); list.add(s2); list.add(s3);

for (Shipment s : list) {
    if (s.isExpired()) {
        list.remove(s);   // ❌ ConcurrentModificationException
    }
}

→ 순회 중 컬렉션 변경 → ConcurrentModificationException.

4.2 왜 발생하나 — modCount

// ArrayList 내부
protected transient int modCount = 0;

public boolean add(E e) {
    modCount++;   // 변경 카운트 증가
    // ...
}

public boolean remove(Object o) {
    modCount++;
    // ...
}

Iterator 내부:

private class Itr implements Iterator<E> {
    int expectedModCount = modCount;   // Iterator 생성 시 캐싱
    
    public E next() {
        checkForComodification();   // 변경 여부 검사
        // ...
    }
    
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

→ Iterator는 "내가 만들어졌을 때의 modCount"를 기억.
→ 다음 호출 시 현재 modCount와 비교.
→ 다르면 누가 컬렉션 변경했다는 의미 → 예외.

4.3 fail-fast의 의도

fail-fast = "빨리 실패"

목적:
  컬렉션 변경을 일찍 감지해서
  예측 불가능한 동작 방지

대안:
  - 변경을 무시 (silent failure)
  - 변경에 적응 (복잡, 비용)

자바의 선택:
  → 즉시 예외 발생 → 버그 조기 발견

4.4 fail-fast의 한계 — 보장 X

ConcurrentModificationException은:
  - "best effort" 검사
  - 100% 보장 X
  - 변경이 modCount에 반영되는 타이밍에 따라 다름

특히 멀티스레드:
  - 두 스레드가 동시 변경 시 race condition
  - 예외 안 발생할 수도
  - 또는 다른 이상한 상황

→ 검출 못 했다고 안전하다는 의미 아님. 그냥 멀티스레드 컬렉션 쓰지 말 것.

4.5 해결 — Iterator.remove()

// ❌ list.remove()
for (Shipment s : list) {
    if (s.isExpired()) {
        list.remove(s);
    }
}

// ✓ Iterator.remove()
Iterator<Shipment> it = list.iterator();
while (it.hasNext()) {
    Shipment s = it.next();
    if (s.isExpired()) {
        it.remove();   // ✓ 안전
    }
}

Iterator.remove() 동작:

  • list.remove(lastRet) 수행
  • modCount 증가
  • expectedModCount도 갱신
  • → 다음 next() 시 충돌 안 남

4.6 Java 8+의 removeIf

list.removeIf(Shipment::isExpired);

→ Iterator 사용 + 안전.
→ 가장 간결.

내부 구현:

default boolean removeIf(Predicate<? super E> filter) {
    boolean removed = false;
    final Iterator<E> each = iterator();
    while (each.hasNext()) {
        if (filter.test(each.next())) {
            each.remove();
            removed = true;
        }
    }
    return removed;
}

4.7 fail-safe — 대안

일부 컬렉션은 fail-safe 방식:

// ConcurrentHashMap, CopyOnWriteArrayList 등
ConcurrentHashMap<Long, Shipment> map = new ConcurrentHashMap<>();
// ... 데이터

for (Map.Entry<Long, Shipment> e : map.entrySet()) {
    map.put(2L, newShipment);   // 변경 OK
}
// 예외 없음

fail-safe:

  • Iterator가 컬렉션의 스냅샷 사용
  • 변경 후에도 순회 계속
  • 단, 변경 내용은 반영 안 될 수 있음 (Weakly Consistent)

→ ConcurrentHashMap, CopyOnWriteArrayList, ConcurrentLinkedQueue 등.

4.8 자기 점검 답변

ConcurrentModificationException 은 정확히 언제 발생?

:
1. Iterator로 순회 중
2. 컬렉션이 Iterator 외부에서 변경됨
3. 다음 next(), hasNext(), remove() 호출 시
4. modCount != expectedModCount 검사 실패
5. 즉시 예외

fail-fast 메커니즘.


5️⃣ ListIterator — 양방향 순회

5.1 List 전용 Iterator

public interface ListIterator<E> extends Iterator<E> {
    // Iterator의 메서드 + 추가:
    boolean hasPrevious();
    E previous();
    int nextIndex();
    int previousIndex();
    void set(E e);     // 현재 위치 수정
    void add(E e);     // 현재 위치 추가
}

List 전용. Set/Map은 ListIterator 없음.

5.2 양방향 순회

List<Shipment> list = ...;
ListIterator<Shipment> it = list.listIterator();

while (it.hasNext()) {
    Shipment s = it.next();   // 앞으로
}

// 끝까지 갔다가 역순회
while (it.hasPrevious()) {
    Shipment s = it.previous();   // 뒤로
}

5.3 시작 위치 지정

List<Shipment> list = ...;
ListIterator<Shipment> it = list.listIterator(5);   // 인덱스 5부터

it.next();   // 인덱스 5의 요소

→ 임의 위치에서 순회 시작 가능.

5.4 수정과 추가

ListIterator<Shipment> it = list.listIterator();
while (it.hasNext()) {
    Shipment s = it.next();
    if (s.needsUpdate()) {
        it.set(updated);   // 현재 위치 교체
    }
    if (s.needsExtra()) {
        it.add(extra);   // 현재 위치에 추가
    }
}

set: 마지막 next/previous로 반환한 위치 교체.
add: 현재 cursor 위치에 삽입.

5.5 LinkedList의 진짜 강점

LinkedList<Shipment> list = ...;
ListIterator<Shipment> it = list.listIterator(0);

while (it.hasNext()) {
    if (조건) {
        it.add(newItem);   // O(1)
    }
}

→ LinkedList + ListIterator = 진짜 O(1) 삽입.
→ Unit 5.4의 진짜 강점.

5.6 ArrayList vs LinkedList의 ListIterator

ArrayList.listIterator(i):
  - 즉시 인덱스 i 위치
  - O(1)

LinkedList.listIterator(i):
  - 인덱스 i까지 사슬 따라가기
  - O(n)
  - 한 번 위치 설정 후엔 next/previous O(1)

5.7 ILIC에서의 활용

// 일반적 사용: 거의 안 함
// 대부분 for-each, Stream, removeIf 사용

// 특수 시나리오:
ListIterator<Task> it = tasks.listIterator();
while (it.hasNext()) {
    Task t = it.next();
    if (t.shouldReplace()) {
        it.set(t.replace());
    }
    if (t.shouldExpand()) {
        it.add(t.subTask1());
        it.add(t.subTask2());
    }
}

→ 순회 중 추가 작업 필요 시.
→ 박승제씨가 직접 쓸 일은 드물지만 알아둘 것.


6️⃣ Spliterator — Stream의 기반

6.1 등장 배경

Java 8 Stream 도입:
  - 병렬 처리 (parallelStream)
  - 함수형 스타일

문제:
  Iterator로는 병렬 처리 어려움
  → 새로운 인터페이스 필요
  
해결:
  Spliterator (Splittable Iterator)

6.2 Spliterator 인터페이스

public interface Spliterator<T> {
    boolean tryAdvance(Consumer<? super T> action);   // next처럼
    Spliterator<T> trySplit();                         // 분할!
    long estimateSize();
    int characteristics();
}

핵심 메서드 trySplit():

  • 컬렉션을 둘로 나눔
  • 각 부분에 새 Spliterator 반환
  • 병렬 처리 가능

6.3 동작 예시

List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8);

Spliterator<Integer> sp = list.spliterator();
Spliterator<Integer> sp2 = sp.trySplit();

// sp: 5, 6, 7, 8
// sp2: 1, 2, 3, 4

→ 두 부분을 각 스레드에서 병렬 처리 가능.

6.4 Stream과의 관계

list.stream()
    .filter(...)
    .map(...)
    .forEach(...);

내부:

1. list.spliterator() 호출
2. Stream 파이프라인 구성
3. 종료 연산 (forEach) 시:
   - 순차: Spliterator를 Iterator처럼 사용
   - 병렬 (parallelStream): trySplit() 반복 호출하여 분할
4. 각 부분 처리
5. 결과 합치기 (reduce)

Stream의 기반은 Spliterator.

6.5 Characteristics

public static final int ORDERED;     // 순서 있음 (List)
public static final int DISTINCT;    // 중복 없음 (Set)
public static final int SORTED;      // 정렬됨 (TreeSet)
public static final int SIZED;       // 크기 알려짐
public static final int IMMUTABLE;   // 변경 불가
public static final int CONCURRENT;  // 멀티스레드 안전

→ Stream이 최적화 결정에 사용.
→ 예: SORTED면 sorted() 연산 skip.

6.6 자기 점검 답변

Spliterator는 왜 등장했나?

:
1. Stream의 병렬 처리 지원
2. Iterator만으로는 분할 어려움
3. trySplit으로 컬렉션을 나누고 병렬 처리
4. characteristics로 Stream 최적화


7️⃣ ILIC 실무 — Iterator 활용

7.1 일상적 사용 — for-each / Stream

// ✓ for-each
for (Shipment s : shipments) {
    process(s);
}

// ✓ Stream
shipments.stream()
    .filter(Shipment::isActive)
    .forEach(this::process);

// ✓ forEach
shipments.forEach(this::process);

이것만으로 충분.

7.2 순회 중 삭제 — removeIf

// ✓ 가장 권장
shipments.removeIf(Shipment::isExpired);

// 또는 Stream
List<Shipment> active = shipments.stream()
    .filter(s -> !s.isExpired())
    .toList();

→ ConcurrentModificationException 회피.

7.3 멀티스레드 환경

// ❌ HashMap 공유
private final Map<Long, Shipment> cache = new HashMap<>();

void process(Shipment s) {
    cache.put(s.getId(), s);
    
    for (Map.Entry<Long, Shipment> e : cache.entrySet()) {
        // ...
    }
}

→ 두 스레드 동시 호출 시 깨짐.

// ✓ ConcurrentHashMap
private final ConcurrentHashMap<Long, Shipment> cache = new ConcurrentHashMap<>();

void process(Shipment s) {
    cache.put(s.getId(), s);
    
    for (Map.Entry<Long, Shipment> e : cache.entrySet()) {
        // weakly consistent — 안전하지만 변경 반영 보장 X
    }
}

7.4 ILIC의 Stream 활용

// 그룹핑
Map<String, List<Shipment>> byRoute = shipments.stream()
    .collect(Collectors.groupingBy(Shipment::getRoute));

// 카운팅
Map<String, Long> countByStatus = shipments.stream()
    .collect(Collectors.groupingBy(
        s -> s.getStatus().name(),
        Collectors.counting()));

// 합계
BigDecimal totalFreight = shipments.stream()
    .map(Shipment::getFreight)
    .reduce(BigDecimal.ZERO, BigDecimal::add);

// 정렬 + 변환
List<ShipmentResponse> sorted = shipments.stream()
    .sorted(Comparator.comparing(Shipment::getCreatedAt).reversed())
    .map(ShipmentResponse::from)
    .toList();

→ Stream + 람다 = 모던 자바 컬렉션 처리.

7.5 병렬 Stream — 신중히

// 병렬 처리
shipments.parallelStream()
    .forEach(this::process);

언제 도움 되나?

  • 큰 컬렉션 (수천+)
  • 무거운 연산
  • CPU bound 작업

언제 도움 안 됨?

  • 작은 컬렉션 (수십)
  • 가벼운 연산 (오버헤드 ↑)
  • 순서 중요한 작업
  • 공유 자원 접근

→ ILIC 일반 코드에선 순차 stream으로 충분.
→ 병렬은 측정 후 도입.

7.6 외부 데이터 순회

// JPA Pagination
Pageable pageable = PageRequest.of(0, 100);
Page<Shipment> page;
do {
    page = repository.findAll(pageable);
    page.forEach(this::process);
    pageable = pageable.next();
} while (page.hasNext());

// 또는 Stream (Spring Data)
try (Stream<Shipment> stream = repository.findAllByActiveTrue()) {
    stream.forEach(this::process);
}

→ Iterator/Stream 패턴 활용.

7.7 박승제씨가 만나는 Iterator

직접 사용:
  - 거의 for-each / Stream으로 충분
  - 가끔 Iterator.remove() 필요

이해해야 하는 곳:
  - fail-fast 메커니즘 (디버깅)
  - ConcurrentModificationException 원인
  - Stream의 내부 (Spliterator)
  - 멀티스레드 컬렉션

8️⃣ 흔한 실수 + 디버깅

실수 1 — 순회 중 collection.remove()

for (Shipment s : list) {
    if (s.isExpired()) {
        list.remove(s);   // ❌ CME
    }
}

해결:

list.removeIf(Shipment::isExpired);

실수 2 — for-each에서 인덱스 사용

// ❌ for-each에서 인덱스 못 씀
int i = 0;
for (Shipment s : list) {
    if (i == 5) { ... }
    i++;
}

해결:

  • 인덱스 필요하면 일반 for 루프
  • 또는 IntStream
IntStream.range(0, list.size())
    .forEach(i -> {
        Shipment s = list.get(i);
        // ...
    });

실수 3 — Stream 재사용

Stream<Shipment> stream = list.stream();
stream.forEach(...);
stream.forEach(...);   // ❌ IllegalStateException: stream has already been operated upon

Stream은 단일 소비.

해결:

list.stream().forEach(...);
list.stream().forEach(...);   // 새 stream

실수 4 — Iterator 변수 누락

Iterator<Shipment> it = list.iterator();
while (it.hasNext()) {
    Shipment s = list.iterator().next();   // ❌ 매번 새 Iterator!
}

해결:

Iterator<Shipment> it = list.iterator();
while (it.hasNext()) {
    Shipment s = it.next();   // 같은 Iterator
}

실수 5 — null 컬렉션 for-each

List<Shipment> list = getList();   // null 가능
for (Shipment s : list) {           // NPE!
    // ...
}

해결:

List<Shipment> list = getList();
if (list != null) {
    for (Shipment s : list) { ... }
}

// 또는 Optional/getOrDefault
for (Shipment s : Objects.requireNonNullElse(list, List.of())) { ... }

// 또는 Stream의 안전한 처리
Optional.ofNullable(list).orElse(List.of()).forEach(this::process);

실수 6 — ConcurrentHashMap 순회 가정

ConcurrentHashMap<Long, Shipment> map = ...;
int size = map.size();   // 정확한 size?

map.put(...);   // 동시 변경

// size 시점과 순회 시점이 다름 → 다를 수 있음
for (Map.Entry<Long, Shipment> e : map.entrySet()) {
    // ...
}

ConcurrentHashMap의 size, iterator는 weakly consistent.

  • 정확한 스냅샷 보장 X
  • 변경 중인 데이터 일부 반영 가능

→ 정확성이 중요하면 lock 또는 readWriteLock 사용.

실수 7 — Stream 디버깅 어려움

shipments.stream()
    .filter(s -> s.getWeight().compareTo(LIMIT) > 0)
    .map(this::transform)
    .forEach(this::process);

디버깅:

  • 각 단계 출력하기 어려움
  • peek 사용 가능
shipments.stream()
    .filter(s -> s.getWeight().compareTo(LIMIT) > 0)
    .peek(s -> log.debug("After filter: {}", s))
    .map(this::transform)
    .peek(s -> log.debug("After map: {}", s))
    .forEach(this::process);

디버깅 도구

# IntelliJ의 Stream Debugger
# 디버그 모드에서 Stream 단계별 확인 가능

# 로그 추가
.peek(s -> log.debug(...))

9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
Iterator 패턴이 해결한 문제?컬렉션 내부 노출 없이 통일 순회
for-each의 실체?iterator() 호출 + while(hasNext())
ConcurrentModificationException?순회 중 외부 변경 시. modCount 검사
안전한 순회 중 삭제?Iterator.remove() 또는 removeIf
fail-fast vs fail-safe?즉시 예외 vs 스냅샷 사용
ListIterator vs Iterator?양방향, set/add, List 전용
Spliterator의 등장?Stream 병렬 처리 위해
Stream의 기반은?Spliterator
ConcurrentHashMap 순회?weakly consistent
Stream 재사용?불가. 단일 소비

9.2 자기 점검 체크리스트

기본 이해

  • Iterator 패턴의 의미를 안다
  • for-each가 Iterator로 변환됨을 안다
  • modCount + expectedModCount 메커니즘을 안다
  • fail-fast의 한계를 안다
  • ListIterator의 추가 기능을 안다

실전 적용

  • for-each / Stream으로 일상 처리
  • removeIf로 안전 삭제
  • ConcurrentModificationException 회피
  • 멀티스레드 컬렉션 적절히 선택
  • Stream 디버깅 가능

면접 대비 — 5분 답변

  • Iterator 패턴의 본질
  • for-each 컴파일 결과
  • ConcurrentModificationException 원인
  • Spliterator와 Stream
  • 멀티스레드 컬렉션 순회

🎯 핵심 요약 — 3줄 정리

1. Iterator 패턴 = 컬렉션 내부 은닉 + 통일 순회

  • Iterable.iterator() → Iterator
  • 자기 위치 기억하는 커서
  • for-each는 syntactic sugar

2. fail-fast 메커니즘

  • modCount + expectedModCount 비교
  • 순회 중 외부 변경 → ConcurrentModificationException
  • 해결: Iterator.remove() 또는 removeIf

3. ILIC 실무

  • 일상: for-each / Stream / forEach
  • 순회 중 삭제: removeIf
  • 멀티스레드: ConcurrentHashMap (weakly consistent)
  • 디버깅 시 ConcurrentModificationException 원인 이해

📚 다음으로...

Phase 7 — Buffer

이번 Phase 6에서 자료구조의 동적 다룸을 봤다면, 마지막 Phase 7은 Buffer 메커니즘.

  • 1주차 NIO Channel/Buffer 와 통합
  • ByteBuffer, CharBuffer, IntBuffer
  • Direct Buffer vs Heap Buffer
  • position, limit, capacity, mark
  • 운영에서 Buffer 활용

→ 2주차의 마지막 Phase.

2주차 진행 상황

✅ Phase 1 — 자바 변수 ↔ 메모리 매핑 (1.1 ~ 1.6 완주)
✅ Phase 2 — JVM 메서드 실행 메커니즘 (2.1 ~ 2.4 완주)
✅ Phase 3 — 바이트코드와 상수 풀 (3.1 ~ 3.4 완주, 정점)
✅ Phase 4 — G1 GC 심화 (4.1 ~ 4.5 완주, 운영 마스터)
✅ Phase 5 — 컬렉션 내부 구조 (5.1 ~ 5.4 완주, 자료구조 마스터)
🚀 Phase 6 — Reflection & Iterator
  ✅ Unit 6.1 Reflection의 본질
  ✅ Unit 6.2 Iterator 패턴의 본질 ← 여기
⏭ Phase 7 — Buffer (마지막)

작성한 2주차 학습자료

누적 25개 Unit
2주차 약 92% 완주
profile
Software Developer

0개의 댓글