포스트

Iterator Pattern

컬렉션 내부 구조를 노출하지 않고 원소를 순서대로 꺼내는 방법을 제공하는 패턴. 현대 언어는 거의 내장하고 있다.

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 같은 언어 기능과 자동으로 호환된다. 새 인터페이스를 만들면 표준 알고리즘에 못 끼운다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.