10. Iterator Pattern

2021. 10. 15. 17:27Computer Sciences/Design Patterns

문제

우리는 프로그래밍을 하면서 여러 자료구조를 사용한다. 간단하게는 배열부터 연결 리스트, 힙, 트리도 사용하기도 한다. 그런데 이런 자료구조에 저장한 데이터를 모두 접근하고 싶다면 어떻게 할까? 모든 자료구조마다 for 문을 돌리면서 탐색해야 할까? 또 자료구조마다 탐색 방법도 다른데 어떻게 해야 될까? 이러한 문제를 이터레이터 패턴으로 해결할 수 있다.

우리는 팬케이크 가게를 운영한다. 우리는 ArrayList를 사용해서 메뉴를 관리한다. 그런데 사업이 잘 되어 다른 가게와 합병하게 되었다. 그런데 이 가게는 배열로 메뉴를 관리하고 있었다. 그래서 기존에 우리가 메뉴를 접근할 때 사용하던 방식과 달라 혼란이 발생했다.

public class MenuItem {

    String name;
    String description;
    boolean vegetarian;
    double price;

    public MenuItem(String name,
                                    String description,
                                    boolean vegetarian,
                                    double price) {
        this.name = name;
        this.description = description;
        this.vegetarian = vegetarian;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public String getDescription() {
        return description;
    }

    public boolean getVegetarian() {
        return vegetarian;
    }

    public double getPrice() {
        return price;
    }
}
public class PancakeHouseMenu {

    ArrayList<MenuItem> menuItems;

    public PancakeHouseMenu() {
        menuItems = new ArrayList<>();

        addItem("팬케이크 세트 A",
                        "우리 가게 대표 메뉴"
                        true,
                        2.99);

        addItem("팬케이크 세트 B",
                        "두 번째로 출시한 팬케이크 세트 메뉴"
                        true,
                        2.99);

        addItem("레귤러 팬케이크 세트",
                        "달걀 후라이와 소시지가 들어가 있는 팬케이크"
                        false,
                        3.19);
    }

    public void addItem(String name, String description,
                                            boolean vegetarian, double price) {
        MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
        menuItems.add(menuItem);
    }

    public ArrayList<MenuItem> getMenuItems() {
        return menuItems;
    }
}
public class DinerMenu {

    static final int MAX_ITEMS = 6;
    int numberOfItems = 0;
    MenuItem[] menuItems;

    public DinerMenu() {
        menuItems = new MenuItem[MAX_ITEMS];

        addItem("점심 세트 A",
                        "우리 가게 대표 메뉴"
                        true,
                        2.95);

        addItem("점심 세트 B",
                        "두 번째로 출시한 점심 세트 메뉴"
                        true,
                        2.95);

        addItem("핫도그 세트",
                        "양파, 치즈, 많은 양념이 들어간 핫도그"
                        false,
                        3.15);
    }

    public void addItem(String name, String description,
                                            boolean vegetarian, double price) {
        MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
        if (numberOfItems >= MAX_ITEMS) {
            System.err.println("죄송합니다 메뉴가 꽉 찼습니다.");
        } else {
            menuItems[numberOfItems] = menuItem;
            numberOfItems++;
        }
    }

    public MenuItem[] getMenuItems() {
        return menuItems;
    }
}

이렇게 만들면 뭐가 문제일까? 이 메뉴들을 서빙하는 웨이트리스 클래스를 생각해보자. 만약 손님이 두 가게에 있는 메뉴를 물어보면 어떻게 알려줘야 할까? 모든 요소에 접근하는 방식이 통일되어 있지 않으므로 각 자료구조를 for 문을 돌면서 일일이 알려줘야 할 것이다. 웨이트리스는 고단해진다. 합병이 미워진다.

public void printMenu() {

    for(int i = 0; i < pancakeMenuItems.length; i++) {
        MenuItem menuItem = pancakeMenuItems.get(i);
        System.out.println(menuItem.getName());
        System.out.println(menuItem.getDescription());
        System.out.println(menuItem.getVegetarian());
        System.out.println(menuItem.getPrice());
    }

    for(int i = 0; i < dinerMenuItems.length; i++) {
        MenuItem menuItem = dinerMenuItems.get(i);
        System.out.println(menuItem.getName());
        System.out.println(menuItem.getDescription());
        System.out.println(menuItem.getVegetarian());
        System.out.println(menuItem.getPrice());
    }
}

만약 다른 자료구조가 추가되면 그 방식에 맞춰서 일일이 모든 과정을 작성해야 할 것이다. 업무에 지친 웨이트리스를 위해서 이러한 문제를 통일시키도록 하자.

문제 해결

우리는 반복을 위한 인터페이스와 이를 구현할 클래스를 만들 것이다.

public interface Iterator {

    boolean hasNext();

    Object next();
}
public class DinerMenuIterator implements Iterator {

    MenuItem[] items;
    int position = 0;

    public DinerMenuItemIterator(MenuItem[] items) {
        this.items = items;
    }

    @Override
    public Object next() {
        MenuItem menuItem = items[position];
        position += 1;
        return menuItem;
    }

    @Override
    public boolean hasNext() {
        if (position == items.length || items[position] == null) {
            return false;
        } elses {
            return true;
        }
    }
}

이를 기존의 DinerMenu에 적용시켜 보자.

public class DinerMenu {

    static final int MAX_ITEMS = 6;
    int numberOfItems = 0;
    MenuItem[] menuItems;

    // 생성자

    // 기타 메서드

    ~~public MenuItem[] getMenuItems() {
        return menuItems;
    }~~

    public Iterator createIterator() {
        return new DinerMenuIterator(menuItems);
    }
}

이제 PancakeHouseMenu에도 적용시켜 보자.

public class PancakeHouseMenuIterator implements Iterator {

    ArrayList<MenuItem> items;
    int position = 0;

    public DinerMenuItemIterator(ArrayList<MenuItem> items) {
        this.items = items;
    }

    @Override
    public Object next() {
        MenuItem menuItem = items.get(position);
        position += 1;
        return menuItem;
    }

    @Override
    public boolean hasNext() {
        if (position == items.size() || items.get(position) == null) {
            return false;
        } elses {
            return true;
        }
    }
}
public class PancakeHouseMenu {

    ArrayList<MenuItem> menuItems;

    // 생성자

    // 기타 메서드

    ~~public ArrayList<MenuItem> getMenuItems() {
        return menuItems;
    }~~

    public Iterator createIterator() {
        return new PancakeHouseMenuIterator(menuItems);
    }
}

이제 웨이트리스는 아래와 같이 일을 할 수 있게 됐다!

PancakeHouseMenu pancakeMenu = new PancakeHouseMenu();
DinerMenu dinerMenu = new DinerMenu();

Iterator pancakeMenuIter = pancakeMenu.createIterator();
Iterator dinerMenuIter = dinerMenu.createIterator();

printMenu();

// 두 메뉴를 같은 방식으로 처리하여 웨이트리스가 출력을 고민할 필요가 없어졌다.

public void printMenu() {

    while (pancakeMenuIter.hasNext()) {
        MenuItem menuItem = pancakeMenuIter.next();
        System.out.println(menuItem.getName());
        System.out.println(menuItem.getDescription());
        System.out.println(menuItem.getVegetarian());
        System.out.println(menuItem.getPrice());
    }

    while (dinerMenuIter.hasNext()) {
        MenuItem menuItem = dinerMenuIter.next();
        System.out.println(menuItem.getName());
        System.out.println(menuItem.getDescription());
        System.out.println(menuItem.getVegetarian());
        System.out.println(menuItem.getPrice());
    }
}

이를 좀 더 예쁘게 고쳐볼까?

public void printMenu() {

    printMenu(pancakeMenuIter);
    printMenu(dinerMenuIter);
}

public void printMenu(Iterator iterator) {
        while (iterator.hasNext()) {
        MenuItem menuItem = iterator.next();
        System.out.println(menuItem.getName());
        System.out.println(menuItem.getDescription());
        System.out.println(menuItem.getVegetarian());
        System.out.println(menuItem.getPrice());
    }
}

처음에 썼던 for 문에 비해 훨씬 간결하고 객체지향적으로 바뀐 것을 확인할 수 있다.

이를 클래스 다이어그램으로 그려보면 아래와 같다.

이터레이터 패턴

컬렉션 구현 방법을 노출시키지 않으면서 그 집합체 안에 들어있는 모든 항목에 접근할 수 있게 해 주는 방법을 제공하는 패턴이다. 우리는 맨 처음에 웨이트리스가 모든 메뉴에 일일이 접근해서 알려주도록 했다. 이는 요소에 접근하는 방법을 모두 노출 시키고 자료구조가 추가될 때마다 그에 맞춰서 다시 구현 방법을 모색해야 했다. 그러나 이터레이터 패턴을 적용시킨 후로는 추가된 자료구조는 Iterator 인터페이스만 구현하면 웨이트리스가 이를 참조해서 hasNext()next()만 실행해주면 모든 요소에 접근할 수 있도록 만들었다. 이로써 다형성을 적용하고 OCP를 지키면서 프로그래밍할 수 있었다. 이러한 패턴은 이미 자바에서 java.util.Iterator 패키지로 지원하고 있으니 실제 개발 시에는 이를 사용하는 편이 더 편할 것이다.

단일 역할 원칙

그렇다면 왜 굳이 인터페이스를 분리해서 구현할까? 그냥 집합체 자체에서 제공하는 것이 더 편하지 않을까? 그냥 메서드 하나만 구현해주면 되는 거 아닌가? 이런 의문이 들 수 있다.

위와 같이 하는 방식이 좋지 않은 이유는 코드가 변경되는 이유가 클래스의 역할과 다른 역할을 처리하도록 하게 된다. 즉 두 가지 이유로 인해 그 클래스가 변경될 수 있다는 것이다. 먼저 컬렉션이 어떤 이유로 바뀌게 되면 그 클래스가 바뀌어야 한다. 그리고 반복하는 기능이 바뀌었을 때도 해당 클래스를 변경해야 한다. 이런 이유로 인해 "변경"이라는 주제와 관련된 디자인 원칙이 적용된다.

바로 단일 역할 원칙(Single Responsiblity Principle)이다. 클래스를 변경하는 이유는 한 가지 뿐이어야 한다는 이유이다. 즉 하나의 클래스는 하나의 역할만 부여해야 한다는 원칙이다. 이렇게 함으로써 클래스 컬렉션이 변경될 땐 해당 클래스만, 이터레이터 기능이 변경되면 이터레이터 클래스만 변경하도록 한다. 이로 인해 유지보수성이 좋아지고 코드를 관리하기 좋아진다.

'Computer Sciences > Design Patterns' 카테고리의 다른 글

12. State Pattern  (0) 2021.10.15
11. Composite Pattern  (0) 2021.10.15
9. Template Method Pattern  (0) 2021.10.14
8. Facade Pattern  (0) 2021.10.14
7. Adapter Pattern  (0) 2021.10.14