반복자 패턴

Bo Ram·2025년 3월 26일

반복자 패턴이란?

반복자 패턴은 데이터를 어떻게 저장하고 관리하는지(컬렉션의 내부 구조)데이터를 어떻게 순서대로 꺼내서 사용하는지(순회 로직) 를 깔끔하게 분리해주는 디자인 패턴

코딩을 하다 보면 다양한 자료구조를 사용하게 된다. 예를 들어, ArrayList, 배열, HashMap 등을 사용할 때가 많다. 이런 데이터들을 하나씩 꺼내서 처리해야 할 때 코드가 점점 복잡해지는 경우가 있다.

이럴 때 해결책이 될 수 있는 것이 바로 반복자(Iterator) 패턴이다. 이 패턴을 사용하면 반복문을 단순화하고, 다양한 컬렉션에 대한 처리를 통일된 방식으로 할 수 있다.

컬렉션마다 반복 코드를 다 짜야 할까? (문제 상황)

예를 들어, 아침 메뉴는 ArrayList에 담겨 있고, 점심 메뉴는 배열에 저장되어 있다고 해보자. 이때 각 컬렉션에 대해 반복문을 따로 작성해야 한다.

// ArrayList 메뉴 출력
for (int i = 0; i < arrayListMenu.size(); i++) {
    MenuItem item = arrayListMenu.get(i);
    // 출력
}

// 배열 메뉴 출력
for (int i = 0; i < arrayMenu.length; i++) {
    MenuItem item = arrayMenu[i];
    if (item != null) { // 배열은 null 체크
        // 출력
    }
}

이렇게 반복문을 각 컬렉션에 맞게 작성해야 한다. 그러나 다음과 같은 문제가 있다.

  1. 새로운 메뉴 타입이 추가되면 출력 코드를 수정해야 한다.
  2. 출력 로직이 각 컬렉션에 맞는 방식(get(), size(), [], length)을 알아야 한다.
  3. 비슷한 반복 코드가 중복된다.

반복자 패턴의 구성 요소

  1. Iterator (반복자 인터페이스):

    • hasNext(), next() 등의 기본 명령 규칙을 정의한 인터페이스이다.
  2. ConcreteIterator (구체적인 반복자):

    • 실제로 컬렉션을 순차적으로 처리하고, 요소를 하나씩 반환하는 클래스이다.
  3. Aggregate (집합체 인터페이스):

    • 컬렉션이 반복자를 생성하는 메서드(createIterator())를 제공하는 인터페이스이다.
  4. ConcreteAggregate (구체적인 집합체):

    • 실제 데이터를 가지고 있는 클래스이다. createIterator() 메서드를 통해 자신의 데이터를 처리할 반복자를 반환한다.

예시 적용

식당 메뉴를 출력하는 예시를 반복자 패턴으로 바꿔보자.

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

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

// ConcreteAggregate & ConcreteIterator (배열 기반)
public class DinerMenu implements Menu {
    MenuItem[] menuItems;
    // ... (생략)
    @Override
    public Iterator createIterator() { return new DinerMenuIterator(menuItems); }
}

public class DinerMenuIterator implements Iterator {
    // ... (배열 순회 로직)
}

// ConcreteAggregate & ConcreteIterator (ArrayList 기반)
public class PancakeHouseMenu implements Menu {
    ArrayList<MenuItem> menuItems;
    // ... (생략)
    @Override
    public Iterator createIterator() { return new PancakeHouseMenuIterator(menuItems); }
}

public class PancakeHouseMenuIterator implements Iterator {
    // ... (ArrayList 순회 로직)
}

// 클라이언트 코드 (Waitress)
public class Waitress {
    Menu pancakeHouseMenu;
    Menu dinerMenu;
    // ... (생성자)

    public void printMenu() {
        Iterator pancakeIterator = pancakeHouseMenu.createIterator();
        Iterator dinerIterator = dinerMenu.createIterator();

        System.out.println("--- 아침 메뉴 ---");
        printMenuItems(pancakeIterator);
        System.out.println("\n--- 점심 메뉴 ---");
        printMenuItems(dinerIterator);
    }

    private void printMenuItems(Iterator iterator) {
        while (iterator.hasNext()) {
            MenuItem menuItem = (MenuItem) iterator.next();
            // ... (메뉴 항목 출력)
        }
    }
}

Waitress 클래스의 printMenuItems 메서드는 이제 Iterator만 알면 되므로, 실제 데이터가 ArrayList인지 배열인지 전혀 신경 쓸 필요가 없다. 이 덕분에 printMenu 메서드는 간결해지고, 새로운 메뉴 타입이 추가되어도 기존 코드를 수정할 필요가 없다.

반복자 패턴의 장점

  1. 느슨한 결합: 클라이언트는 컬렉션의 타입을 모르고, 반복자만 알면 된다. 컬렉션의 변화가 클라이언트 코드에 영향을 주지 않는다.
  2. 책임 분리: 데이터 관리와 반복 로직을 분리하여 코드가 명확하고 관리하기 쉽다.
  3. 다양한 반복 방법: 필요에 따라 역순으로 반복하거나 특정 조건에 맞는 요소만 꺼낼 수 있는 반복자를 만들 수 있다.
  4. 통일된 사용법: 모든 컬렉션에서 hasNext(), next()라는 동일한 방식으로 반복할 수 있어 코딩이 편리하다.

반복자 패턴 사용 시 고려할 점

반복자 패턴이 항상 최선의 선택은 아니다. 간단한 컬렉션에서는 굳이 반복자 패턴을 사용할 필요가 없을 수 있다. 또한, Java의 java.util.Iterator나 Python의 iterable/iterator처럼 이미 내장된 반복자 기능이 있는 언어에서는 굳이 직접 구현할 필요는 없다.
하지만 반복자 패턴의 원리를 이해하면 내장 기능을 더욱 잘 활용할 수 있으며, 필요한 경우 맞춤형 반복자를 만들 수 있다.

profile
사부작ㅤ사부작

0개의 댓글