PostCover

디자인 패턴을 쉽게 풀어보자 - 반복자 패턴

GoF의 23가지 디자인 패턴 중 하나인 '반복자 패턴(Iterator Pattern)'을 쉽게 풀어보기

반복자(Iterator) 패턴

반복자(Iterator) 패턴은 컬렉션 객체의 내부 구조를 노출하지 않고도 그 안의 요소를 순차적으로 접근할 수 있게 해주는 행동 디자인 패턴입니다. 이 패턴을 사용하면 다양한 자료구조에서 일관된 방식으로 요소를 순회할 수 있습니다.

문제점: 컬렉션 구조와 순회 코드의 결합

컬렉션 객체의 요소를 순회하는 로직이 컬렉션 클래스에 포함되면, 컬렉션의 내부 구조에 의존하는 코드가 발생합니다.

이로 인해

  1. 컬렉션이 변경될 때 순회 코드도 변경해야 합니다.
  2. 동일한 방식으로 다른 종류의 컬렉션을 순회하기 어렵습니다.
class Book {

    private String title;

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

    public String getTitle() {
        return title;
    }
}

class BookShelf {

    private Book[] books;
    private int count = 0;

    public BookShelf(int size) {
        books = new Book[size];
    }

    public void addBook(Book book) {
        books[count++] = book;
    }

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

    public int getCount() {
        return count;
    }
}

public class Main {

    public static void main(String[] args) {
        BookShelf bookShelf = new BookShelf(3);
        bookShelf.addBook(new Book("Design Patterns"));
        bookShelf.addBook(new Book("Refactoring"));
        bookShelf.addBook(new Book("Clean Code"));

        // 컬렉션의 내부 구조에 의존한 순회 코드
        for (int i = 0; i < bookShelf.getCount(); i++) {
            System.out.println(bookShelf.getBookAt(i).getTitle());
        }
    }
}

문제점

  • 컬렉션의 내부 구조에 의존: BookShelf의 배열 인덱스와 크기를 직접 사용합니다.
  • 유연성 부족: 만약 BookShelf의 내부 구조가 배열에서 리스트로 바뀐다면 순회 코드를 수정해야 합니다.
  • 다른 컬렉션과의 일관성 문제: 다른 자료구조를 순회할 때도 매번 다른 방식으로 코드를 작성해야 합니다.

해결 방법: 반복자 패턴 적용

반복자 패턴을 사용하면 컬렉션 객체와 순회 로직을 분리합니다. **반복자 객체(Iterator)**가 순회 작업을 전담하여 컬렉션의 내부 구조를 클라이언트에게 노출하지 않습니다.

Iterator 인터페이스 정의

interface Iterator<T> {
    boolean hasNext();
    T next();
}

구체적인 Iterator 구현

class BookShelfIterator implements Iterator<Book> {

    private BookShelf bookShelf;
    private int index = 0;

    public BookShelfIterator(BookShelf bookShelf) {
        this.bookShelf = bookShelf;
    }

    @Override
    public boolean hasNext() {
        return index < bookShelf.getCount();
    }

    @Override
    public Book next() {
        return bookShelf.getBookAt(index++);
    }
}

Iterable 인터페이스와 컬렉션 클래스 구현

interface Iterable<T> {
    Iterator<T> iterator();
}

class BookShelf implements Iterable<Book> {

    private Book[] books;
    private int count = 0;

    public BookShelf(int size) {
        books = new Book[size];
    }

    public void addBook(Book book) {
        books[count++] = book;
    }

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

    public int getCount() {
        return count;
    }

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

클라이언트 코드

public class Main {

    public static void main(String[] args) {
        BookShelf bookShelf = new BookShelf(3);
        bookShelf.addBook(new Book("Design Patterns"));
        bookShelf.addBook(new Book("Refactoring"));
        bookShelf.addBook(new Book("Clean Code"));

        // 반복자를 사용해 일관된 방식으로 순회
        Iterator<Book> iterator = bookShelf.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next().getTitle());
        }
    }
}

반복자 패턴의 동작 원리

  • 컬렉션 객체는 iterator() 메서드를 통해 반복자 객체를 반환합니다.
  • 반복자 객체는 hasNext()next() 메서드를 통해 요소를 순회합니다.
  • 클라이언트는 컬렉션의 내부 구조를 몰라도 동일한 방식으로 요소를 접근할 수 있습니다.

반복자 패턴의 장단점

장점

  • 내부 구조 은닉: 컬렉션의 내부 구조를 클라이언트에게 노출하지 않습니다.
  • 일관된 인터페이스: 다양한 자료구조에 대해 동일한 방식으로 순회할 수 있습니다.
  • 유연한 확장성: 새로운 자료구조를 추가해도 순회 방식은 변경되지 않습니다.
  • 단일 책임 원칙(SRP) 준수: 컬렉션은 데이터 저장에 집중하고, 반복자는 순회에 집중합니다.

단점

  • 복잡성 증가: 단순한 컬렉션에서는 반복자 객체를 별도로 구현해야 하므로 코드가 복잡해질 수 있습니다.
  • 성능 저하 가능성: 반복자를 생성하는 데 약간의 오버헤드가 발생할 수 있습니다.

결론

Iterator 패턴은 컬렉션의 내부 구조에 상관없이 일관된 방식으로 요소를 순회할 수 있도록 도와주는 유용한 패턴입니다. 이 패턴은 컬렉션과 순회 로직을 분리해 유연성과 확장성을 높이며, 단일 책임 원칙(SRP)을 준수합니다.

하지만, 단순한 자료구조에서는 반복자를 별도로 구현하는 것이 과도한 복잡성을 유발할 수 있으므로 상황에 따라 적절히 사용하는 것이 좋습니다. 이 패턴은 특히 컬렉션 프레임워크나 데이터베이스 처리, 파일 시스템 탐색 등에서 자주 활용됩니다.