컬렉션의 내부 구조를 노출하지 않고 원소를 순회하는 패턴.
interface Iterator<T> {
boolean hasNext();
T next();
}
// 클라이언트는 배열인지 트리인지 몰라도 된다
for (Item item : collection) { ... } // 내부적으로 이터레이터를 쓴다
무엇을 얻나
- 순회 방법이 여러 가지여도 같은 인터페이스로 다룬다 (전위·중위·후위 순회)
- 컬렉션이 순회 상태를 갖지 않는다 → 동시에 여러 번 순회할 수 있다
- 컬렉션 구현을 바꿔도 클라이언트가 안 바뀐다
fail-fast
for (String s : list) { if (cond) list.remove(s); } // ConcurrentModificationException
컬렉션은 구조 변경 횟수를 세고, 이터레이터는 만들어질 때의 값을 기억한다
- 다르면 즉시 예외를 던진다
- "조용히 잘못 도는 것보다 즉시 터지는 게 낫다" 는 설계
순회 중 삭제는 이터레이터의 remove 나 removeIf 로 한다
Visitor — 연산을 구조 밖으로
interface Visitor {
void visit(Circle c);
void visit(Rectangle r);
}
interface Shape { void accept(Visitor v); }
class Circle implements Shape {
public void accept(Visitor v) { v.visit(this); } // 자기 타입을 알려 준다
}
// 새 연산을 추가할 때 Shape 들을 안 고쳐도 된다
class AreaVisitor implements Visitor { ... }
class DrawVisitor implements Visitor { ... }
Visitor의 트레이드오프
-
연산 추가는 쉽다 — → 새 Visitor 클래스 하나
-
타입 추가는 어렵다 — → 모든 Visitor 를 고쳐야 한다
-
그래서 "타입은 안정적이고 연산이 자주 느는" 경우에만 값을 한다
- 예: AST · 컴파일러 · 문서 트리
언어에 패턴 매칭이 있으면 Visitor가 대부분 불필요해진다 — 닫힌 타입 계층에 switch 하나로 같은 일을 하고, 타입 추가 시 컴파일러가 누락을 잡아 준다.
면접 함정
- ❌ "Iterator는 for문의 다른 형태" → 내부 구조를 감추는 것이 목적이다. 인덱스가 없는 구조(트리·해시)에서 진가가 드러난다.
- ❌ "Visitor가 더 좋은 설계" → 타입이 늘어나는 도메인에서는 최악이다.
직접 만들 일이 있나
// 커스텀 자료구조를 for-each 로 순회 가능하게 만들 때
class Tree<T> implements Iterable<T> {
public Iterator<T> iterator() {
return new Iterator<>() {
public boolean hasNext() { ... }
public T next() { ... }
};
}
}
for (T item : tree) { ... } // 이제 이렇게 쓸 수 있다
지연 순회
// 전체를 만들지 않고 필요한 만큼만 생성한다
Iterator<Integer> infinite = new Iterator<>() {
int n = 0;
public boolean hasNext() { return true; }
public Integer next() { return n++; }
};
무한 수열을 다룰 수 있다는 것이 이터레이터의 숨은 강점이다 — 컬렉션을 반환했다면 불가능하다.
더블 디스패치 — Visitor의 원리
visit 이 두 번의 다형 호출로 이뤄진다
- shape.accept(visitor) — ① shape 의 실제 타입이 결정된다
- visitor.visit(this) ② visitor 의 실제 타입 + this 의 정적 타입으로 오버로드 결정
자바는 단일 디스패치만 지원하므로 이 우회가 필요하다 패턴 매칭이 있는 언어에서는 이 복잡함이 필요 없다