목차
Iterator Pattern: 저장 구조와 순회 방법 분리하기
반복자 패턴은 컬렉션의 내부 저장 방식을 호출자에게 드러내지 않고 원소를 하나씩 읽게 한다. 배열을 리스트나 트리로 바꿔도 호출자는 hasNext()와 next()라는 순회 계약을 사용할 수 있다. 반복자는 현재 위치를 가진 객체이므로, 새 반복자를 만들면 처음부터 다시 순회할 수 있다.

원본 그림의 Aggregate는 반복자를 만드는 집합체, ConcreteIterator는 순회 위치를 관리하는 구현체다. Java에는 이미 Iterable<T>와 Iterator<T>가 있으므로 직접 인터페이스를 정의하기보다 표준 계약을 활용하는 편이 일반적이다.
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
record Book(String name) { }
final class BookShelf implements Iterable<Book> {
private final List<Book> books = new ArrayList<>();
void appendBook(Book book) {
books.add(book);
}
@Override
public Iterator<Book> iterator() {
return books.iterator();
}
}
class Main {
public static void main(String[] args) {
BookShelf shelf = new BookShelf();
shelf.appendBook(new Book("아파치"));
shelf.appendBook(new Book("스타워즈"));
for (Book book : shelf) {
System.out.println(book.name());
}
}
}
shelf.iterator()가 별도 반복자를 반환하고 for-each 문은 내부적으로 hasNext()와 next()를 호출한다. 출력은 아파치, 스타워즈 순서다. 저장소가 리스트라는 사실을 숨길 수 있지만, 순서까지 자동으로 보장하는 것은 아니다. 순서를 바꿀 수 있는 자료구조로 교체한다면 순회 순서가 공개 계약인지 먼저 확인해야 한다.
직접 반복자를 구현할 때 지켜야 할 계약
기존 예제는 BookShelf<E> 안에 List<Book>을 저장하고 (E) 캐스팅을 사용했다. BookShelf<String>도 만들 수 있는 모양이어서 실행 중 형변환 오류가 날 수 있다. 원소 타입을 Book으로 고정하거나 저장소 자체를 List<E>로 만들어야 한다. 또한 원소가 다 떨어진 뒤 next()를 호출하면 NoSuchElementException을 던져야 한다. hasNext()는 다음 원소를 소비하면 안 된다.
순회 중 컬렉션을 수정하는 정책도 정해야 한다. ArrayList의 반복자는 구조 변경을 감지해 ConcurrentModificationException을 던질 수 있지만, 이는 동시성 제어를 보장하는 장치가 아니다. 변경을 허용해야 한다면 복사본을 순회하거나 컬렉션이 제공하는 반복자 연산을 사용한다. 반복자 객체를 직접 구현하는 비용은 저장 구조에 특수한 순서나 필터링 규칙이 필요할 때 정당화된다.