
반복자 패턴은 일련의 데이터 집합에 대해서 순차적인 접근, 순회 접근을 지원하는 패턴이다.
데이터 집합이란 객체들을 그룹으로 묶어 자료의 구조를 취하는 컬렉션을 말한다. 대표적으로는 리스트, 트리, 그래프, 테이블 등이 있다.
보통 배열이나 리스트 같은 경우 순서가 연속적인 데이터 집합이기 때문에 간단한 인덱싱을 이용한 for문이나 foreach문을 통해 순회할 수 있다. 그러나 해시, 트리와 같은 컬렉션은 데이터 저장 순서가 정해지지 않고 적재되기 때문에, 각 요소들을 어떤 기준으로 접근해야 할 지 애매해진다.
예를 들어 트리 구조가 있다면 어떤 상황에선 깊이를, 어떤 상황에선 너비를 우선해서 순회할 수 있기 때문이다.

이처럼 복잡하게 얽혀있는 자료 컬렉션들을 순회하는 알고리즘 전략을 정의하는 것을 반복자 패턴이라고 한다. 컬렉션 객체 안에 들어있는 모든 원소들에 대한 접근 방식이 공통화 되어 있다면 어떤 종류의 컬렉션에서도 이터레이터만 뽑아내면 여러 전략으로 순회가 가능해서 보다 다형적인 코드를 설계할 수 있게 된다.

이 밖에도 이터레이터 패턴은 별도의 이터레이터 객체를 반환 받아 순회하기 때문에, 집합체 내부 구조를 노출하지 않고 순회할 수 있다는 장점도 있다.

Aggregate (인터페이스) : ConcreateIterator 객체를 반환하는 인터페이스를 제공한다.iterator() : ConcreateIterator 객체를 만드는 팩토리 메서드ConcreateAggregate (클래스) : 여러 요소들이 이루어져 있는 데이터 집합체Iterator (인터페이스) : 집합체 내의 요소들을 순서대로 검색하기 위한 인터페이스를 제공한다.hasNext() : 순회할 다음 요소가 있는지 확인 (true / false)next() : 요소를 반환하고 다음 요소를 반환할 준비를 하기 위해 커서를 이동시킴ConcreateIterator (클래스) : 반복자 객체// 집합체 객체 (컬렉션)
interface Aggregate {
Iterator iterator();
}
class ConcreteAggregate implements Aggregate {
Object[] arr; // 데이터 집합 (컬렉션)
int index = 0;
public ConcreteAggregate(int size) {
this.arr = new Object[size];
}
public void add(Object o) {
if(index < arr.length) {
arr[index] = o;
index++;
}
}
// 내부 컬렉션을 인자로 넣어 이터레이터 구현체를 클라이언트에 반환
@Override
public Iterator iterator() {
return new ConcreteIterator(arr);
}
}
// 반복체 객체
interface Iterator {
boolean hasNext();
Object next();
}
class ConcreteIterator implements Iterator {
Object[] arr;
private int nextIndex = 0; // 커서 (for문의 i 변수 역할)
// 생성자로 순회할 컬렉션을 받아 필드에 참조 시킴
public ConcreteIterator(Object[] arr) {
this.arr = arr;
}
// 순회할 다음 요소가 있는지 true / false
@Override
public boolean hasNext() {
return nextIndex < arr.length;
}
// 다음 요소를 반환하고 커서를 증가시켜 다음 요소를 바라보도록 한다.
@Override
public Object next() {
return arr[nextIndex++];
}
}
public static void main(String[] args) {
// 1. 집합체 생성
ConcreteAggregate aggregate = new ConcreteAggregate(5);
aggregate.add(1);
aggregate.add(2);
aggregate.add(3);
aggregate.add(4);
aggregate.add(5);
// 2. 집합체에서 이터레이터 객체 반환
Iterator iter = aggregate.iterator();
// 3. 이터레이터 내부 커서를 통해 순회
while(iter.hasNext()) {
System.out.printf("%s → ", iter.next());
}
}
집합 내부 컬렉션을 배열로 표현했지만, 배열 뿐만 아니라 여러 복잡한 컬렉션으로도 이터레이터 구현이 가능하다.
반복자 패턴을 사용하면...
플레이어가 인벤토리에 아이템을 지니고 있는데, 이 아이템들을 획득일자 순, 그리고 수량 순에 따라 정렬해서 나열할 수 있게 해달라고 한다. 즉, 두가지 정렬을 구현해야 한다.
public class Item
{
public string Name { get; set; } // 아이템 이름
public DateTime AcquisitionDate { get; set; } // 획득 일자
public int Quantity { get; set; } // 아이템 수량
// 생성자: 수량과 획득 일자 설정 가능
public Item(string name, int quantity, DateTime acquisitionDate)
{
Name = name;
Quantity = quantity;
AcquisitionDate = acquisitionDate;
}
// 기본 생성자: 획득 일자는 현재 시간으로 설정, 수량은 1
public Item(string name)
{
Name = name;
Quantity = 1;
AcquisitionDate = DateTime.Now; // 현재 시각으로 설정
}
}
public class ItemInventory
{
private List<Item> items = new List<Item>(); // 아이템 리스트
public void AddItem(string name, int quantity, DateTime acquisitionDate)
{
items.Add(new Item(name, quantity, acquisitionDate)); // 새로운 아이템 추가
}
public List<Item> GetItems()
{
return items; // 아이템 리스트 반환
}
}
public class InventoryWithSort : UnityEngine.MonoBehaviour
{
void Start()
{
// 1. 인벤토리 생성
ItemInventory itemInventory = new ItemInventory();
// 2. 인벤토리에 아이템 추가 (직접 날짜와 수량을 설정)
itemInventory.AddItem("Sword", 2, new DateTime(2024, 10, 7));
itemInventory.AddItem("Potion", 5, new DateTime(2024, 5, 12));
itemInventory.AddItem("Shield", 1, new DateTime(2024, 8, 23));
itemInventory.AddItem("Bow", 3, new DateTime(2024, 12, 1));
// 3. 아이템 추가 순서대로 조회하기
List<Item> items = itemInventory.GetItems();
UnityEngine.Debug.Log("Items in added order:");
foreach (Item item in items)
{
UnityEngine.Debug.Log($"{item.Name} / {item.Quantity} / {item.AcquisitionDate}");
}
// 4. 아이템 수량별로 정렬해서 조회하기
items.Sort((i1, i2) => i1.Quantity.CompareTo(i2.Quantity)); // 수량별로 정렬
UnityEngine.Debug.Log("Items sorted by quantity:");
foreach (Item item in items)
{
UnityEngine.Debug.Log($"{item.Name} / {item.Quantity} / {item.AcquisitionDate}");
}
// 5. 아이템을 날짜별로 정렬해서 조회하기
items.Sort((i1, i2) => i1.AcquisitionDate.CompareTo(i2.AcquisitionDate)); // 획득 날짜별로 정렬
UnityEngine.Debug.Log("Items sorted by acquisition date:");
foreach (Item item in items)
{
UnityEngine.Debug.Log($"{item.Name} / {item.Quantity} / {item.AcquisitionDate}");
}
}
}
일반적으로 for 문을 돌려 인벤토리 아이템들을 순회하였다. 그러나 이러한 구성 방식은 인벤토리에 들어간 아이템을 순회할 때, 아이템이 어떤 구조로 이루어져있는지를 클라이언트에 노출한다. 따라서 이를 보다 객체 지향적으로 구성하기 위해 이터레이터 패턴을 적용해보자.
현재 필요한 순회 전략으로는 수량 순서대로 조회, 획득일자 순서대로 조회 두 가지가 존재한다. 따라서 이에 대한 이터레이터 클래스 역시 두 가지 생성해주면 된다.
public class Item
{
public string Name { get; set; }
public DateTime AcquisitionDate { get; set; }
public int Quantity { get; set; }
public Item(string name, int quantity, DateTime acquisitionDate)
{
Name = name;
Quantity = quantity;
AcquisitionDate = acquisitionDate;
}
}
public class ItemInventory : IEnumerable<Item>
{
private List<Item> items = new List<Item>();
public void AddItem(string name, int quantity, DateTime acquisitionDate)
{
items.Add(new Item(name, quantity, acquisitionDate));
}
public List<Item> GetItems()
{
return items;
}
// 수량 순서 이터레이터 반환
public IEnumerator<Item> GetQuantityItemIterator()
{
return new QuantityItemIterator(items);
}
// 날짜 순서 이터레이터 반환
public IEnumerator<Item> GetDateItemIterator()
{
return new DateItemIterator(items);
}
// IEnumerable<Item>의 GetEnumerator 구현
public IEnumerator<Item> GetEnumerator()
{
return items.GetEnumerator(); // 기본 발행 순서
}
// IEnumerable의 GetEnumerator 구현 (비제네릭 버전)
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
// 수량 순서 이터레이터
public class QuantityItemIterator : IEnumerator<Item>
{
private List<Item> items;
private int position = -1;
public QuantityItemIterator(List<Item> items)
{
// 수량 순으로 정렬
this.items = new List<Item>(items);
this.items.Sort((i1, i2) => i1.Quantity.CompareTo(i2.Quantity));
}
public bool MoveNext()
{
position++;
return position < items.Count;
}
public void Reset()
{
position = -1;
}
public Item Current
{
get
{
if (position < 0 || position >= items.Count)
throw new InvalidOperationException();
return items[position];
}
}
object IEnumerator.Current => Current;
public void Dispose() { }
}
// 날짜 순서 이터레이터
public class DateItemIterator : IEnumerator<Item>
{
private List<Item> items;
private int position = -1;
public DateItemIterator(List<Item> items)
{
// 날짜 순으로 정렬
this.items = new List<Item>(items);
this.items.Sort((i1, i2) => i1.AcquisitionDate.CompareTo(i2.AcquisitionDate));
}
public bool MoveNext()
{
position++;
return position < items.Count;
}
public void Reset()
{
position = -1;
}
public Item Current
{
get
{
if (position < 0 || position >= items.Count)
throw new InvalidOperationException();
return items[position];
}
}
object IEnumerator.Current => Current;
public void Dispose() { }
}
public class InventoryWithSort : MonoBehaviour
{
void Start()
{
// 1. 인벤토리 생성
ItemInventory itemInventory = new ItemInventory();
// 2. 인벤토리에 아이템 추가
itemInventory.AddItem("Sword", 2, new DateTime(2024, 10, 7));
itemInventory.AddItem("Potion", 5, new DateTime(2024, 5, 12));
itemInventory.AddItem("Shield", 1, new DateTime(2024, 8, 23));
itemInventory.AddItem("Bow", 3, new DateTime(2024, 12, 1));
// 3. 아이템 수량 순서대로 조회
Debug.Log("Items sorted by quantity:");
Print(itemInventory.GetQuantityItemIterator());
// 4. 아이템 날짜 순서대로 조회
Debug.Log("Items sorted by acquisition date:");
Print(itemInventory.GetDateItemIterator());
}
void Print(IEnumerator<Item> iterator)
{
while (iterator.MoveNext())
{
Item item = iterator.Current;
Debug.Log($"{item.Name} / {item.Quantity} / {item.AcquisitionDate}");
}
}
}

수량 순, 획득일자 순으로 잘 출력되는 것을 확인할 수 있다. 그리고 Start문을 보면 알다시피 아이템들을 순회할 때 인벤토리 내부가 어떤 집합체로 구현되어 있는지 알 수 없게 감추고 신경 쓸 필요가 없어졌다. 순회 전략을 각 객체로 나눔으로써 때에 따라 적절한 이터레이터 객체만 받으면 똑같은 이터레이터 순회 코드로 다양한 순회 전략을 구사할 수 있다.
IEnumrable을 사용하지 않고도 반복자 패턴은 구현이 가능하다.
public interface IAggregate<T>
{
IIterator<T> CreateIterator();
}
public interface IIterator<T>
{
void First(); // 이터레이터를 첫 번째 항목으로 이동
bool HasNext(); // 다음 항목이 있는지 확인
T Next(); // 다음 항목을 가져옴
T CurrentItem { get; } // 현재 항목 가져오기
void Reset(); // 이터레이터를 처음 상태로 되돌림
}
public class Item
{
public string Name { get; set; }
public DateTime AcquisitionDate { get; set; }
public int Quantity { get; set; }
public Item(string name, int quantity, DateTime acquisitionDate)
{
Name = name;
Quantity = quantity;
AcquisitionDate = acquisitionDate;
}
}
public class ItemInventory : IAggregate<Item>
{
private List<Item> items = new List<Item>();
public void AddItem(string name, int quantity, DateTime acquisitionDate)
{
items.Add(new Item(name, quantity, acquisitionDate));
}
public List<Item> GetItems()
{
return items;
}
public IIterator<Item> CreateQuantityIterator()
{
return new QuantityItemIterator(this);
}
public IIterator<Item> CreateDateIterator()
{
return new DateItemIterator(this);
}
private class QuantityItemIterator : IIterator<Item>
{
private List<Item> items;
private int position = -1;
public QuantityItemIterator(ItemInventory inventory)
{
items = new List<Item>(inventory.GetItems());
items.Sort((i1, i2) => i1.Quantity.CompareTo(i2.Quantity));
First(); // 초기화 시 첫 번째 아이템으로 설정
}
public void First()
{
position = -1; // 초기화
}
public bool HasNext()
{
return position + 1 < items.Count;
}
public Item Next()
{
if (!HasNext())
throw new InvalidOperationException();
position++;
return items[position];
}
public Item CurrentItem
{
get
{
if (position < 0 || position >= items.Count)
throw new InvalidOperationException();
return items[position];
}
}
public void Reset()
{
First();
}
}
private class DateItemIterator : IIterator<Item>
{
private List<Item> items;
private int position = -1;
public DateItemIterator(ItemInventory inventory)
{
items = new List<Item>(inventory.GetItems());
items.Sort((i1, i2) => i1.AcquisitionDate.CompareTo(i2.AcquisitionDate));
First(); // 초기화 시 첫 번째 아이템으로 설정
}
public void First()
{
position = -1; // 초기화
}
public bool HasNext()
{
return position + 1 < items.Count;
}
public Item Next()
{
if (!HasNext())
throw new InvalidOperationException();
position++;
return items[position];
}
public Item CurrentItem
{
get
{
if (position < 0 || position >= items.Count)
throw new InvalidOperationException();
return items[position];
}
}
public void Reset()
{
First();
}
}
}
public class InventoryWithSort : MonoBehaviour
{
void Start()
{
ItemInventory itemInventory = new ItemInventory();
itemInventory.AddItem("Sword", 2, new DateTime(2024, 10, 7));
itemInventory.AddItem("Potion", 5, new DateTime(2024, 5, 12));
itemInventory.AddItem("Shield", 1, new DateTime(2024, 8, 23));
itemInventory.AddItem("Bow", 3, new DateTime(2024, 12, 1));
// 아이템 수량 순서대로 조회
Debug.Log("Items sorted by quantity:");
Print(itemInventory.CreateQuantityIterator());
// 아이템 날짜 순서대로 조회
Debug.Log("Items sorted by acquisition date:");
Print(itemInventory.CreateDateIterator());
}
void Print(IIterator<Item> iterator)
{
while (iterator.HasNext())
{
Item item = iterator.Next();
Debug.Log($"{item.Name} / {item.Quantity} / {item.AcquisitionDate}");
}
}
}
IEnumrable과 IEnumrator를 사용한 이터레이터 패턴과 IIterator 처럼 인터페이스를 직접 구현한 패턴은 몇 가지 차이점이 있다. 자세히 설명하기엔 내용이 너무 많아서 쉽게 설명하자면 아래와 같다.
IEnumrable과 IEnumrator는 .NET 프레임워크에서 제공하는 기본 인터페이스로 가독성이 좋고 이미 제공되는 메서드가 있기 때문에 사용하기 편리하다는 장점이 있다. 하지만 당연하게도 이미 제공된 기능을 주로 사용하기 때문에 새로운 상태 관리와 같은 복잡한 동작을 별도로 구현하기가 어려울 수 있다.
IIterator는 사용자가 직접 이터레이터의 인터페이스를 정의하여 사용하다보니 필요한 메서드를 자유롭게 선택하여 구현할 수 있다. 당연하게도 이터레이터의 동작을 보다 세밀하게 제어 가능하고 메서드를 유연하게 추가, 삭제할 수 있다. 하지만 구현의 복잡성이 증가할 수 있다.
따라서 대부분의 경우에는 제공되는 기능만으로도 충분한 이터레이션을 제공하는 IEnumrable과 IEnumrator를 사용하는 것이 가독성, 편리성 면에서는 좋다. 하지만 특정 요구사항에 따라서 커스터마이즈 해야하는 유연성이 필요하다면 가독성과 복잡성을 어느 정도 희생하고 IIterator를 사용하는 것도 가능하다.
즉, 상황에 맞게 잘 사용해야한다는 뜻!