Iterator Pattern

사내에서 디자인 패턴 스터디를 진행하게 되어서, 관련 내용을 이 블로그에 하나씩 정리해나가려 한다. 스터디 교재가 "Java 언어로 배우는 디자인 패턴 입문" 이기에 포스트의 주 참고 자료 역시 이 책이 될 것이다. 단순하게 이 책의 내용을 가져다 블로그에 옮기는 수준이 될 수도 있지만, 스스로 정리하고 익힌다는 측면에선 분명 도움이 될 것이기에, 수고스럽더라도 포스팅을 만들어나가려 한다.

첫번째로 정리할 패턴은 Iterator Pattern (이터레이터 패턴)이다.

이터레이터 패턴은, 간단히 정의하자면, 컬렉션 객체에 대한 반복 작업을 추상화할 수 있는 패턴이다. 이 한 문장으로는 이해하기가 힘들겠지만, 조금씩 자세한 내용을 들여다 보도록 하자.

우선, Iterator 패턴의 클래스 다이어그램부터 한번 보자. Iterator 패턴의 클래스 다이어그램

난 아직 클래스 다이어그램에 익숙치가 않아서 그림만 봐서는 잘 이해가 되진 않지만, 이것도 계속 보고 익히면 다이어그램만 봐도 대충 어떤 구조로 프로그램이 설계되어 있는지 알 수 있을 것이다. 빨리 그런 날이 왔으면... ㅎ

클래스 다이어그램을 보면 알겠지만, 이터레이터 패턴에는 크게 네 개의 객체가 등장하는데, 다음과 같은 역할을 한다.

  • Aggregate: Iterator 객체를 만들어 제공하는 역할을 정의한 인터페이스.
  • Iterator: 실제 컬렉션의 아이템들을 하나씩 순회하는 역할을 정의하는 인터페이스.
  • ConcreteAggregate: Aggregate 객체를 실제로 구현한 객체로 내부적으로 컬렉션 멤버를 가지고 있으며, 이를 순회할 수 있는 Iterator 객체를 만드는 역할을 수행한다.
  • ConcreteIterator: Iterator 인터페이스를 실제로 구현한 객체로 컬렉션을 순회하면서, 컬렉션에 들어있는 모든 객체에 접근할 수 있는 방법을 제공한다.

이제, 각 구성원들에 대한 실제 코드를 보자. 사실 이런 내용은 코드를 봐야 제일 이해하기 쉽다.

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

Aggregate 인터페이스는 Iterator 객체를 반환하는 함수를 정의한다.

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

단순한 형태의 Iterator 인터페이스로, 집합을 순차적으로 조회할 수 있는 메소드만을 가지고 있는 형태이다.

Book 클래스
public class Book {
    private String name;

    public Book(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}

책을 나타내는 간단한 클래스이다. 이쯤되면 이 Book 인스턴스를 원소로 가지는 집합을 만들 것이고, 이 집합은 당연히 Iterator 인터페이스를 상속해서 Book 인스턴스들을 조회할 수 있도록 할 것임을 예상할 수 있다.

BookShelf 클래스
public class BookShelf implements Aggregate {
    private List<Book> books;

    public BookShelf() {
        this.books = new ArrayList<Book>();
    }

    public Book getBookAt(int index) {
        return this.books.get(index);
    }

    public void appendBook(Book book) {
        this.books.add(book);
    }

    public int getLength( ) {
        return this.books.size();
    }

    @Override
    public Iterator<Book> iterator() {
        return new ListIterator<Book>(this.books);
    }
}

BookShelf 클래스는 Aggregate 인터페이스를 구현한다. 내부적으로 Book 객체를 원소로 하는 List 객체를 가지고 있고, 이를 이용하여 Iterator 객체를 제공한다.

마지막으로, Iterator 인터페이스를 구현한 클래스이다.

ListIterator 클래스
public class ListIterator<T> implements Iterator {
    private List<T> list;
    private int size;
    private int currentIndex;

    public ListIterator (List<T> list) {
        this.list = list;
        this.size = list.size();
        this.currentIndex = 0;
    }

    @Override
    public boolean hasNext() {
        return currentIndex < size;
    }

    @Override
    public T next() {
        T item = this.list.get(this.currentIndex);
        this.currentIndex++;
        return item;
    }
}

ListIterator 클래스는 Iterator 인터페이스를 구현하고 있다. 생성자에서 받은 List 객체를 내부적으로 가지면서, List 객체 구성원들을 순차적으로 탐색할 수 있는 메소드를 제공한다.

아래는 위의 클래스들을 사용하는 클라이언트 코드이다.

Main 클래스
public static void main(String[] args) {
    BookShelf bookShelf = new BookShelf();
    bookShelf.appendBook(new Book("Around the World in 80 Days"));
    bookShelf.appendBook(new Book("Bible"));
    bookShelf.appendBook(new Book("Cinderella"));
    bookShelf.appendBook(new Book("Daddy-Long-Legs"));

    Iterator<Book> iterator = bookShelf.iterator();
    while (iterator.hasNext()) {
        Book book = iterator.next();
        System.out.println(book.getName());
    }
}

코드는 간단하기에 이해하기 어렵지 않다. 하지만 여기서 의문이 드는 건 왜 굳이 복잡하게 이런 패턴을 쓰는가이다. 사실, 간단한 반복문은 그냥 for 문을 돌려도 되니깐...

하지만, Iterator 패턴이 가진 장점은 분명하다. 그것은 실제로 집합을 가진 객체(BookShelf 클래스)와 집합 원소들을 탐색하는 영역(ListIterator)이 분리되어 있다는 것이며, 이는 집합을 가진 객체가 제대로 된 Iteraotr 객체를 반환한다는 가정 하에 내부적으로 집합을 다루는 자료구조를 다른 어떤 무엇인가로 바꾸어도 (예를 들면, List에서 Hash로 변경) 클라이언트 코드는 변경하지 않아도 된다는 것을 의미한다.

글로 정리한다는게 정말 쉽지 않은 일이라는 걸 다시 한번 느낀다. 다음 패턴은 언제 정리할 수 있을려나 ㅠㅠ

어쨌든, 첫번째 패턴 정리는 이것으로 끝~!!