이터레이터 패턴
컬렉션의 내부 표현을 노출하지 않고 순회 상태를 별도 반복자에 두는 행위 패턴.
· 마지막 업데이트: 2026-10-25 · 6 min read
개요
재생 목록을 순서대로 읽는 코드가 내부 배열의 인덱스를 직접 다루면, 저장 방식을 바꿀 때 호출부도 고쳐야 한다. 무엇을 담는지와 어디까지 읽었는지를 나누면 순회 방법을 감출 수 있다.
이터레이터(Iterator), 즉 반복자는 다음 항목으로 이동하고 현재 항목을 읽는 역할이다. 목록은 항목을 보관하고, 각 반복자는 자기 순회 위치를 가진다.
UML
Playlist가 반복자를 생성한다. 호출부는 IEnumerator<string>의 MoveNext()와 Current를 사용하며, 예제의 yield return을 C# 컴파일러가 반복자 구현으로 바꾼다.
역사
GoF는 1994년 Design Patterns에서 Iterator를 행위 패턴으로 정리했다. 집합체의 내부 표현을 공개하지 않고 요소에 차례로 접근하는 구조다.1
왜 사용하는가?
호출부는 내부 배열을 받지 않고 순서대로 항목을 읽는다. 같은 목록을 여러 곳에서 읽어도 반복자마다 위치를 따로 둘 수 있다.
지연 조회에서는 순회 중에도 I/O나 실패가 일어날 수 있다. 이 예제는 생성 시 복사한 문자열 배열만 읽으며, 변경 중인 목록이나 원격 스트림의 동작을 보장하지 않는다.
예제
Playlist를 foreach로 순회한다. 내부 배열은 호출부에 노출하지 않는다.
using System;
using System.Collections.Generic;
using System.Linq;
var playlist = new Playlist(new[] { "intro", "outro" });
foreach (string track in playlist) Console.WriteLine(track);
// 내부 배열을 감추고 항목을 순회하는 약속만 제공한다.
sealed class Playlist : IEnumerable<string>
{
private readonly string[] tracks;
public Playlist(IEnumerable<string> tracks) => this.tracks = tracks.ToArray();
// 새 호출마다 별도 순회 위치를 가진 생성기를 반환한다.
public IEnumerator<string> GetEnumerator()
{
foreach (string track in tracks) yield return track;
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}
intro
outroforeach는 반복자 프로토콜을 사용한다. yield return은 항목을 하나씩 제공할 구현을 간단히 작성하는 C# 문법이다.
범위 넓힌 예제
같은 목록에서 반복자를 두 개 만들고 서로 다른 속도로 이동한다.
using System;
using System.Collections.Generic;
using System.Linq;
var playlist = new Playlist(new[] { "intro", "outro" });
using var first = playlist.GetEnumerator();
using var second = playlist.GetEnumerator();
if (first.MoveNext()) Console.WriteLine("first: " + first.Current);
if (second.MoveNext()) Console.WriteLine("second: " + second.Current);
if (first.MoveNext()) Console.WriteLine("first: " + first.Current);
Console.WriteLine("first has next: " + first.MoveNext());
Console.WriteLine("second: " + second.Current);
// 내부 배열을 감추고 항목을 순회하는 약속만 제공한다.
sealed class Playlist : IEnumerable<string>
{
private readonly string[] tracks;
public Playlist(IEnumerable<string> tracks) => this.tracks = tracks.ToArray();
// 새 호출마다 별도 순회 위치를 가진 생성기를 반환한다.
public IEnumerator<string> GetEnumerator()
{
foreach (string track in tracks) yield return track;
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}
first: intro
second: intro
first: outro
first has next: False
second: intro첫 반복자를 한 칸 더 움직여도 두 번째 반복자의 Current는 바뀌지 않는다. MoveNext()가 false를 반환한 뒤에는 Current를 읽지 않는다.
TypeScript 예제
Playlist는 항목을 보관하고, 순회할 때 만든 반복자는 현재 위치를 보관한다. 위치를 컬렉션 하나에 저장하지 않아야 두 순회가 서로의 진행을 바꾸지 않는다.
// 내부 배열을 감추고 항목을 순회하는 약속만 제공한다.
class Playlist implements Iterable<string> {
private readonly tracks: readonly string[];
constructor(tracks: readonly string[]) { this.tracks = [...tracks]; }
// 새 호출마다 별도 순회 위치를 가진 생성기를 반환한다.
*[Symbol.iterator](): Generator<string, void, unknown> {
for (const track of this.tracks) yield track;
}
}
function step(iterator: Iterator<string>): string {
const result = iterator.next();
return result.done ? "(end)" : result.value;
}
const playlist = new Playlist(["intro", "outro"]);
const first = playlist[Symbol.iterator]();
const second = playlist[Symbol.iterator]();
console.log("first: " + step(first));
console.log("first: " + step(first));
console.log("second: " + step(second));
console.log("second: " + step(second));first: intro
first: outro
second: intro
second: outroC#의 GetEnumerator()에 대응하는 진입점이 Symbol.iterator다. 생성기 함수를 호출할 때마다 별도 반복자가 만들어지며, 끝난 뒤에는 next()의 done을 확인한다.
같이 보기
C# 예제는 각각 독립된 .NET 10 콘솔 프로그램이다.
TypeScript 예제는 ES2022 이상으로 컴파일한 JavaScript를 Node.js에서 실행한다.