안녕하세요! GoF 디자인 패턴 연재, 이번 시간에는 행위 패턴(Behavioral Patterns)의 한 종류로, 컬렉션의 내부 구현을 숨긴 채 그 안의 모든 항목에 접근할 수 있는 방법을 제공하는 이터레이터(Iterator) 패턴에 대해 알아보겠습니다.


정의

이터레이터(Iterator) 패턴어떤 컬렉션(Aggregate) 객체의 내부 표현(배열, 리스트, 트리 등)을 외부에 노출시키지 않고, 그 안에 포함된 요소들을 순차적으로 접근할 수 있는 방법을 제공하는 패턴입니다. '이터레이터'는 '반복자'라는 뜻으로, 컬렉션을 순회하는 역할을 전담하는 객체입니다.

 

TV 리모컨을 생각해봅시다. 우리는 '다음 채널(+)', '이전 채널(-)' 버튼만 누르면 채널을 순서대로 탐색할 수 있습니다. TV 내부에서 채널 목록이 배열로 관리되는지, 리스트로 관리되는지, 혹은 다른 복잡한 구조로 되어 있는지 전혀 알 필요가 없습니다. 리모컨의 버튼이 바로 이터레이터의 next(), hasNext() 와 같은 역할을 하는 것입니다.

 

이처럼 이터레이터 패턴은 '데이터의 집합(컬렉션)'과 '데이터를 순회하는 행위'를 분리하여, 클라이언트가 컬렉션의 내부 구조에 얽매이지 않고 일관된 방식으로 요소에 접근할 수 있게 해줍니다. 다음과 같이 구성됩니다

  • Iterator (이터레이터): 컬렉션을 순회하는 데 필요한 메서드(예: hasNext(), next())의 인터페이스를 정의합니다.
  • ConcreteIterator (구체적인 이터레이터): Iterator 인터페이스를 구현하며, 특정 컬렉션을 순회하는 로직을 가집니다. 현재 순회 중인 위치를 기억하고 추적합니다.
  • Aggregate (집합체): 이터레이터 객체를 생성하는 메서드(예: createIterator())의 인터페이스를 정의합니다.
  • ConcreteAggregate (구체적인 집합체): Aggregate 인터페이스를 구현하며, ConcreteIterator의 인스턴스를 생성하여 반환합니다.

사용예시

다양한 책(Book)들을 보관하고 있는 서재(BookCollection)를 순회하는 예제를 통해 이터레이터 패턴을 구현해 보겠습니다.

1. 이터레이터 (Iterator) 및 집합체 (Aggregate) 인터페이스 정의

먼저 표준 인터페이스들을 정의합니다. (Java에서는 java.util.Iteratorjava.lang.Iterable이 이미 존재하지만, 패턴 학습을 위해 직접 만들어 보겠습니다.)

// Iterator 인터페이스
public interface Iterator<T> {
    boolean hasNext();
    T next();
}

// Aggregate 인터페이스
public interface Aggregate {
    Iterator createIterator();
}

2. 구체적인 집합체 (ConcreteAggregate) 클래스 구현

책들을 배열로 관리하는 BookCollection 클래스를 구현합니다.

// 간단한 Book 클래스
class Book {
    private String name;
    public Book(String name) { this.name = name; }
    public String getName() { return name; }
}

// ConcreteAggregate 클래스
public class BookCollection implements Aggregate {
    private Book[] books;
    private int last = 0;

    public BookCollection(int maxSize) {
        this.books = new Book[maxSize];
    }

    public Book getBookAt(int index) {
        return books[index];
    }

    public void addBook(Book book) {
        if (last < books.length) {
            this.books[last] = book;
            last++;
        }
    }

    public int getLength() {
        return last;
    }

    @Override
    public Iterator createIterator() {
        return new BookIterator(this);
    }
}

3. 구체적인 이터레이터 (ConcreteIterator) 클래스 구현

BookCollection을 순회하는 BookIterator를 구현합니다.

// ConcreteIterator 클래스
public class BookIterator implements Iterator<Book> {
    private BookCollection bookCollection;
    private int index;

    public BookIterator(BookCollection bookCollection) {
        this.bookCollection = bookCollection;
        this.index = 0;
    }

    @Override
    public boolean hasNext() {
        return index < bookCollection.getLength();
    }

    @Override
    public Book next() {
        if (this.hasNext()) {
            Book book = bookCollection.getBookAt(index);
            index++;
            return book;
        }
        return null; // 실제로는 NoSuchElementException을 던지는 것이 좋음
    }
}

4. 클라이언트 (Client) 코드

클라이언트는 BookCollection의 내부가 배열인지 전혀 몰라도, 이터레이터를 통해 모든 책을 순회할 수 있습니다.

public class Library {
    public static void main(String[] args) {
        // 1. 집합체 생성 및 책 추가
        BookCollection bookCollection = new BookCollection(5);
        bookCollection.addBook(new Book("디자인 패턴의 이해"));
        bookCollection.addBook(new Book("클린 코드"));
        bookCollection.addBook(new Book("객체지향의 사실과 오해"));

        // 2. 이터레이터 생성
        Iterator<Book> it = bookCollection.createIterator();

        // 3. 이터레이터를 사용하여 순회
        while (it.hasNext()) {
            Book book = it.next();
            System.out.println(book.getName());
        }
        
        // 실행 결과:
        // 디자인 패턴의 이해
        // 클린 코드
        // 객체지향의 사실과 오해
    }
}


결론

이터레이터는 현대언어에서 다양한 자료 형을 순환하기 위한 직관적인 패턴으로 사용됩니다. 다음의 경우에 이터레이터의 구현을 검토해 볼 수 있습니다

 

  • 컬렉션의 내부 구조를 클라이언트로부터 숨기고 싶을 때: 클라이언트는 Iterator 인터페이스만 알면 되므로, 컬렉션의 내부 구현이 변경되어도 클라이언트 코드는 영향을 받지 않습니다.
  • 하나의 컬렉션에 대해 여러 가지 순회 방법을 제공하고 싶을 때: 정방향 이터레이터, 역방향 이터레이터, 특정 조건에 맞는 요소만 반환하는 필터링 이터레이터 등 다양한 이터레이터를 만들 수 있습니다.
  • 서로 다른 구조를 가진 여러 컬렉션에 대해 동일한 방식(인터페이스)으로 순회하고 싶을 때

그러나 간단한 순회 로직을 위해 여러 클래스(Iterator, ConcreteIterator 등)를 추가로 만들어야 하므로, 경우에 따라 과설계(Over-engineering)가 될 수 있습니다.

 

이터레이터 패턴은 컬렉션과 순회 로직을 분리하여 시스템의 결합도를 낮추고 유연성을 높이는 매우 중요한 패턴입니다. Java의 for-each 구문도 내부적으로는 이 이터레이터 패턴을 기반으로 동작합니다. 다음시간에는 자주 등장하는 문제상황을 규격화한, 인터프리터 패턴을 알아보겠습니다

+ Recent posts