Iterator Pattern
컬렉션 내부 구조를 노출하지 않고 원소를 순서대로 꺼내는 방법을 제공하는 패턴. 현대 언어는 거의 내장하고 있다.
난이도 입문 · 선행 없음
한 줄 요약
컬렉션이 어떻게 생겼는지 모른 채 원소들을 순서대로 받아갈 수 있게 해주는 인터페이스를 제공한다. 거의 모든 현대 언어가 언어 차원에서 이 패턴을 내장하고 있다.
어떤 문제를 푸는가
배열·연결 리스트·트리에 들어 있는 원소를 순회하는 코드를 작성한다. Iterator 없이 짜면 컬렉션 종류마다 순회 코드가 다르다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 배열
for (int i = 0; i < arr.size(); ++i) doSomething(arr[i]);
// 연결 리스트
Node* node = list.head;
while (node) { doSomething(node->value); node = node->next; }
// 트리 (in-order 순회)
void traverse(TreeNode* n) {
if (!n) return;
traverse(n->left);
doSomething(n->value);
traverse(n->right);
}
문제:
- 클라이언트가 컬렉션 내부 구조를 알아야 한다 (인덱스? next 포인터? 재귀?).
- 컬렉션을 다른 자료구조로 바꾸면 모든 순회 코드가 깨진다.
- 같은 알고리즘(예: “원소 중 짝수만 세기”)을 자료구조마다 또 짜야 한다.
패턴 적용 후
순회 책임을 별도 객체(Iterator)에 분리한다. 클라이언트는 hasNext() / next()만 안다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
#include <memory>
template<typename T>
class Iterator {
public:
virtual bool hasNext() = 0;
virtual T next() = 0;
virtual ~Iterator() = default;
};
template<typename T>
class Iterable {
public:
virtual std::unique_ptr<Iterator<T>> iterator() = 0;
virtual ~Iterable() = default;
};
// 배열 기반 컬렉션과 그 이터레이터
class IntArray : public Iterable<int> {
std::vector<int> data;
public:
IntArray(std::initializer_list<int> v) : data(v) {}
class ArrayIterator : public Iterator<int> {
const std::vector<int>& data;
size_t idx = 0;
public:
ArrayIterator(const std::vector<int>& d) : data(d) {}
bool hasNext() override { return idx < data.size(); }
int next() override { return data[idx++]; }
};
std::unique_ptr<Iterator<int>> iterator() override {
return std::make_unique<ArrayIterator>(data);
}
};
// 클라이언트는 자료구조를 모름
int sum(Iterable<int>& collection) {
auto it = collection.iterator();
int total = 0;
while (it->hasNext()) total += it->next();
return total;
}
int main() {
IntArray nums{1, 2, 3, 4, 5};
std::cout << sum(nums) << std::endl; // 15
}
달라진 점:
sum은 컬렉션 내부를 모른다. 연결 리스트로 바꿔도sum코드 무손상.- 자료구조마다 순회 알고리즘을 분리할 수 있다 (트리는 in-order/pre-order/post-order 등 여러 이터레이터).
- 동시에 여러 순회를 진행할 수 있다 (이터레이터 객체가 따로 상태를 가짐).
구조
1
2
3
4
5
6
7
Iterable (interface) Iterator (interface)
iterator() ─────────────▶ hasNext()
next()
▲ ▲
│ │
Collection ─────────────▶ ConcreteIterator
(내부 위치를 보유)
- Iterator:
hasNext/next인터페이스 - ConcreteIterator: 순회 상태(인덱스, 노드 포인터 등)를 가진 구현체
- Iterable:
iterator()를 제공하는 컬렉션 인터페이스 - ConcreteCollection: 실제 자료구조
언어 차원의 Iterator
이 패턴은 거의 모든 언어가 표준화·내장했다. 직접 구현할 일은 사실상 없다.
C++ — begin() / end()
C++ STL iterator는 GoF 형태와 다르다. hasNext/next 대신 포인터 같은 인터페이스(++, *, ==)로 시작과 끝의 이터레이터를 비교한다.
1
2
3
4
5
6
std::vector<int> v{1, 2, 3, 4, 5};
for (auto it = v.begin(); it != v.end(); ++it) {
std::cout << *it << " ";
}
// 또는 range-based for (C++11+)
for (int x : v) std::cout << x << " ";
장점: 포인터를 그대로 이터레이터로 쓸 수 있어 효율적. 카테고리(input/output/forward/bidirectional/random-access)로 알고리즘이 요구사항을 표현.
Java — Iterator / Iterable
GoF에 가장 가까운 형태. Iterable을 구현하면 for-each로 순회 가능.
1
2
3
for (Integer x : list) {
System.out.println(x);
}
Python — __iter__ / __next__, generator
1
2
3
4
5
6
7
8
9
10
11
for x in collection:
print(x)
# generator로 이터레이터 손쉽게 만들기
def first_n_evens(n):
i, count = 0, 0
while count < n:
if i % 2 == 0:
yield i
count += 1
i += 1
JavaScript — Symbol.iterator
1
2
3
4
5
6
for (const x of collection) console.log(x);
// generator로 정의
function* range(n) {
for (let i = 0; i < n; i++) yield i;
}
직접 GoF Iterator를 구현해야 하는 경우는 거의 없다. 언어 표준 형식을 따르는 게 정답. 표준을 따르면
for-each·map·filter같은 언어 기능과 자동으로 호환된다.
외부 이터레이터 vs 내부 이터레이터
| 외부 (External) | 내부 (Internal) | |
|---|---|---|
| 형태 | while (it.hasNext()) ... | collection.forEach(fn) |
| 누가 순회 제어 | 클라이언트 | 컬렉션 자신 |
| 일시 중단 | 쉬움 (이터레이터 상태 보존) | 어려움 |
| 코드 단순함 | 좀 더 장황 | 함수형으로 깔끔 |
둘 다 GoF가 인정하는 변형. 함수형 스타일이 일반화되면서 내부 이터레이터가 많이 쓰인다.
실전 사례 — 직접 구현하면 좋은 경우
언어 내장 이터레이터가 있어도 직접 만들 가치 있는 상황:
- 무한 시퀀스: 피보나치, 소수 등. 메모리 절약을 위해 lazy 생성.
- 외부 데이터 스트림: DB 커서, 페이지네이션 API. 결과를 미리 다 받지 않고 필요할 때 다음을 가져옴.
- 트리/그래프 순회 변형: BFS/DFS/in-order/level-order 등 여러 순회 전략을 이터레이터로 분리.
- 무거운 자원: 파일 라인 단위 읽기, 네트워크 응답 청크.
안티패턴 / 주의
- 언어 표준이 있는데 직접 인터페이스를 새로 만들지 마라.
MyIterator보다 표준Iterator/Iterable을 구현해야 언어 기능과 합쳐진다. - 순회 중 컬렉션 변경: 거의 모든 언어에서 UB 또는 예외 (Java
ConcurrentModificationException). 변경이 필요하면 사본을 만들거나 명시적으로 지원하는 컨테이너 사용. - 이터레이터가 무거운 자원을 들고 있으면 해제 정책 명확히: 파일·DB 커서를 들고 있는 이터레이터는 RAII /
Closeable/with로 닫는다. - 중첩 순회 시 이터레이터 충돌: 같은 컬렉션의 이터레이터를 두 개 동시에 가져가서 한쪽이 변경하면 다른 쪽이 깨진다. 위 STL의 invalidation 규칙을 보면 종류가 많다.
스스로 점검
1. C++ STL iterator와 GoF Iterator의 가장 큰 차이는?
답
STL은 hasNext/next 대신 포인터처럼 동작한다 (++, *, ==, begin/end 비교). 카테고리(input/forward/random-access)로 요구사항을 표현. 효율적이지만 형태는 더 추상적.
2. 피보나치 무한 시퀀스를 표현하려면 어떤 형태가 자연스러운가?
답
lazy generator. 미리 다 만들지 않고 next()마다 다음 값을 계산. Python yield, C++20 coroutine, JS generator(function*)가 모두 같은 발상.
3. 이터레이터를 직접 만들 때 GoF의 MyIterator 인터페이스를 새로 만들지 말라는 이유는?
답
언어 표준 Iterator/Iterable을 구현해야 for-each, map, filter 같은 언어 기능과 자동으로 호환된다. 새 인터페이스를 만들면 표준 알고리즘에 못 끼운다.