Backend/Design Pattern

[CS스터디-헤드퍼스트 디자인 패턴] 3. 반복자 패턴

Emil :) 2023. 1. 15. 06:24
728x90
반응형

목차

이 글은 Notion에서 작성 후 재편집한 포스트입니다.

서론


이번 포스팅에선 반복자 패턴에 대해 다뤄보도록 하겠습니다.

반복자 패턴은 접근기능과 자료구조를 분리시켜서 객체화 하여, 서로 다른 구조를 가지고 있는 저장 객체에 대해서 접근하기 위해 인퍼페이스를 통일시키고 싶을 때 사용됩니다.

 

참고


https://www.coupang.com/vp/products/6403382250?itemId=13700178772&vendorItemId=80951605437&src=1042503&spec=10304982&addtag=400&ctag=6403382250&lptag=10304982I13700178772&itime=20221210115503&pageType=PRODUCT&pageValue=6403382250&wPcid=15730553344490846187221&wRef=&wTime=20221210115503&redirect=landing&gclid=Cj0KCQiA1sucBhDgARIsAFoytUsR3Vik_OevxodcLNtgECFd34jaUngo-H_vE0iSiNhcnK5-8bdglOUaAhjOEALw_wcB&campaignid=18626086777&adgroupid=&isAddedCart= \

 

헤드 퍼스트 디자인 패턴:14가지 GoF 필살 패턴!

COUPANG

www.coupang.com

https://lktprogrammer.tistory.com/40

 

08 반복자 패턴 (Iterator Pattern)

반복자 패턴 (Iterator Pattern) 접근기능과 자료구조를 분리시켜서 객체화합니다. 서로 다른 구조를 가지고 있는 저장 객체에 대해서 접근하기 위해서 interface를 통일시키고 싶을 때 사용하는 패턴

lktprogrammer.tistory.com

 

본론


반복자 패턴의 정의


반복자 패턴의 사전적 정의는 다음과 같습니다.

반복자 패턴(iterator pattern)은 객체 지향 프로그래밍에서 반복자를 사용하여 컨테이너를 가로지르며 컨테이너의 요소들에 접근하는 디자인 패턴이다. 반복자 패턴은 컨테이너로부터 알고리즘을 분리시키며, 일부의 경우 알고리즘들은 필수적으로 컨테이너에 특화되어 있기 때문에 분리가 불가능하다.

말만 봐서는 사실 무슨 말인지 잘 모르겠습니다.
예제를 통해서 학습하고 이해한 결과, "다른 자료형이어도 반복이 가능한 클래스를 구현해 for문 사용량을 줄인다" 가 핵심인 것 같습니다.

자세한 내용은 아래 예제를 보며 따라와주시면 됩니다.

 

객체마을 식당과 팬케이스 하우스 예제를 통한 반복자 패턴 이해


책에 있는 내용을 각색하여 작성해보겠습니다.

여기 팬케이크 하우스를 운영하는 '루'와 객체마을 식당을 운영하는 '멜'이 있습니다.
이 2개 식당이 서로 합병되어, 아침은 팬케이크 하우스 메뉴를, 점심은 객체마을 식당 메뉴를 한 곳에서 먹을 수 있게 되었습니다.

하지만 여기서 문제가 발생합니다.

팬케이스 하우스와 객체마을 식당 모두 메뉴를 어떤 식으로 구현해야 하는지 협의된 내용이 없기 때문입니다.
또한 팬케이크 하우스는 메뉴에 들어갈 내용을 ArrayList로 저장했었고, 객체마을 식당 배열에 저장했는데, 둘 다 이를 바꿀 생각은 없습니다. 왜냐하면 그동안 작업한게 너무 많은데, 이걸 다 바꾸려니 보통 일이 아니기 때문이죠.

하지만 어쩌겠습니까, 일은 해야죠..

다 그렇게 살아가는거지 뭐

우선  루와 멜은 메뉴 항목을 나타내는 MenuItem 클래스의 구현 방법에 대해 합의했습니다.
각 메뉴에 있는 항목과 MenuItem 클래스 코드는 다음과 같습니다.

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 void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public boolean isVegetarian() {
        return vegetarian;
    }

    public void setVegetarian(boolean vegetarian) {
        this.vegetarian = vegetarian;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }
}

이제 루와 멜이 어떤 문제를 겪고 있는지 알아 볼 차례입니다.
둘 다 메뉴에 항목들을 저장하려고 적지 않은 시간을 투자해서 쓸만한 코드를 만들어 놨습니다.
게다가 그 메뉴에 의존하는 다른 코드도 많이 만들었습니다.

// 루의 팬케이크집 메뉴

public class PancakeHouseMenu {
    List<MenuItem> menuItems;

    public PancakeHouseMenu() {
        // 메뉴 항목이 들어갈 ArrayList
        menuItems = new ArrayList<>();
        addItem("K&B 팬케이크 세트", "스크램블 에그와 토스트가 곁들여진 팬케이크", true, 2.99);
        addItem("레귤러 팬케이크 세트", "어쩌고저쩌고", false, 2.99);
        addItem("블루베리 팬케이크", "이거 쓰긴 귀찮다", true, 3.49);
        addItem("와플", "취향에 따라 블루베리나 딸기를 얹을 수 있는 와플", true, 3.59);
    }

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

    public List<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("통밀 위에 콩고기 베이컨", "이럴거면 그냥 고기먹어라", true, 2.99);
        addItem("통밀 위에 베이컨, 상추, 토마토를 얹은 메뉴", "어쩌고저쩌고", false, 2.99);
        addItem("오늘의 스프", "이거 쓰긴 귀찮다", true, 3.29);
        addItem("핫도그", "취향에 따라 블루베리나 딸기를 얹을 수 있는 와플", true, 3.05);
    }

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

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

이제 이 둘을 하나로 합치는 과정을 진행해야 합니다.
팬케이크 식당과 객체마을 식당을 합친 곳에선 새로운 종업원을 채용해야 합니다.
종업원의 자격 요건은 다음과 같습니다.

printMenu(){ // 메뉴에 있는 모든 항목을 출력 }
printBreakFastMenu() { // 아침 식사 항목만 출력 }
printLunchMenu() { // 점심 식사 항목만 출력 }
printVegetarianMenu() { // 채식주의자용 메뉴 항목만 출력 }
isItemVegetarian(name) { // 해당 항목이 채식주의자용이면 true, 아니면 false return }

그런데.. 우리가 지금 맞닥뜨린 문제는 팬케이크 식당, 객체마을 식당의 메뉴 형식이 다른겁니다.
예를들어, 위에 있는 prineMenu() 를 구현하려면 다음과 같이 각 식당별 객체를 생성 후, for문을 돌면서 출력해야합니다.

PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
ArrayList<MenuItem> breakfastItems = pancakeHouseMenu.getMenuItems();

DinerMenu dinerMenu = new DinerMenu();
MenuItem[] lunchItems = dinerMenu.menuItems();

for(int i = 0; i < breakfastItems.size(); i++){
    // 하나하나 for문 돌면서 출력
}

for(int i = 0; i < lunchItems.size(); i++){
    // 하나하나 for문 돌면서 출력
}

음.. 그냥 한눈에 봐도 뭔가 이건 아니다 라는 생각이 드는 코드입니다.

 

해결방안 모색하기


디자인 패턴을 공부하면서 가장 중요한 내용은 "바뀌는 부분을 캡슐화하라" 입니다. 지금 문제에서 바뀌는 부분은 반복 작업 처리 방법입니다. 왜냐하면 메뉴에서 리턴하는 객체 컬렉션의 형식이 다르기 때문입니다.

위의 printMenu() 를 구현한 코드를 보면, ArrayList와 배열의 자료형에서 오는 차이로, 나중에 또 다른 자료형이 추가되면 또 for문을 구현해줘야 하는 문제점이 있었습니다.
이를 해결하려면, 반복 작업 처리 방법을 캡슐화한 Iterator라는 객체를 만들어서 사용하는 방법을 사용할 수 있습니다.

// breakfastMenu에 들어있는 MenuItem에 반복자(Iterator 객체)를 요구합니다.
Iterator iterator = breakfastMenu.createIterator();

while (iterator.hasNext()) {
    MenuItem menuItem = iterator.next();
}

// 배열에도 적용
Iterator iterator = lunchMenu.createIterator();

while (iterator.hasNext()) {
    MenuItem menuItem = iterator.next();
}

코드에 작성되지않은 breakfastMenu가 있어서 이해가 잘 안되실 수 있는데, 문맥적인 의미로 받아들여주시면 됩니다.
breakfastMenu와 lunchMenu는 각각 아침, 점심 메뉴를 가지고있는 객체라고 생각해주세요. 

이 코드만 봐서는 사실 감이 제대로 오질 않습니다.
실제 코드를 작성하면서 이해도를 높여봅시다.

 

1) 설계 및 코드 구현


객체마을 식당 메뉴에 반복자를 추가해주도록 하겠습니다.
DinerMenu 클래스에 반복자를 추가하기 전에 먼저 iterator 인터페이스를 정의해야 합니다.

public interface Iterator {
    boolean hasNext();
    MenuItem next();
}

이제 DinerMenu 클래스에 사용할 구상 Iterator 클래스를 만들어야 합니다.

public class DinerMenuIterator implements Iterator{

    MenuItem[] items;
    int position = 0;

    // 생성자는 반복작업을 수행할 메뉴 항목 배열을 인자로 받아들임
    public DinerMenuIterator(MenuItem[] items) {
        this.items = items;
    }

    public boolean hasNext() {
        // 배열에 있는 모든 원소를 돌았는지 확인 후, 더 돌아야 할 원소가 있으면 true return
        // 객체마을 식당 주방장이 최대 개수가 정해진 배열을 만들었으므로,
        // 현재 position이 배열의 끝인지 외에도 다음 항목이 null인지도 체크가 필요
        // 그래야 원소가 더 남아있는지 확인할 수 있기 때문이다.
        if (position >= items.length || items[position] == null) {
            return false;
        } else {
            return true;
        }
    }

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

마찬가지로 팬케이크 식당 Iterator도 만들어줍니다.

public class PancakeHouseMenuIterator implements Iterator{
    List<MenuItem> items;
    int position = 0;

    public PancakeHouseMenuIterator(List<MenuItem> items) {
        this.items = items;
    }

    @Override
    public boolean hasNext() {
        if (position >= items.size()) {
            return false;
        } else {
            return true;
        }
    }

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

 이제 반복자도 만들어줬습니다. DinerMenu 클래스에 적용해봅시다.

public class DinerMenu {
    static final int MAX_ITEMS = 6;
    int numberOfItems = 0;
    MenuItem[] menuItems;

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

        addItem("통밀 위에 콩고기 베이컨", "이럴거면 그냥 고기먹어라", true, 2.99);
        addItem("통밀 위에 베이컨, 상추, 토마토를 얹은 메뉴", "어쩌고저쩌고", false, 2.99);
        addItem("오늘의 스프", "이거 쓰긴 귀찮다", true, 3.29);
        addItem("핫도그", "취향에 따라 블루베리나 딸기를 얹을 수 있는 와플", true, 3.05);
    }

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

    // 더이상 필요없음, 내부 구조를 드러내는 단점이 있기에 오히려 지우는게 나음
//    public MenuItem[] getMenuItems() {
//        return menuItems;
//    }

    // 여기서 사용되는 Iterator는 import하는게 아닙니다, 주의
    public Iterator createIterator() {
        return new DinerMenuIterator(menuItems);
    }

}

거의 다 왔습니다, 반복자 코드를 종업원에게도 적용해줘야 합니다.
다음과 같습니다,

public class Waitress {
    PancakeHouseMenu pancakeHouseMenu;
    DinerMenu dinerMenu;

    public Waitress(PancakeHouseMenu pancakeHouseMenu, DinerMenu dinerMenu) {
        this.pancakeHouseMenu = pancakeHouseMenu;
        this.dinerMenu = dinerMenu;
    }

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

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

    // 항목이 남아있는지 체크하고, 메뉴를 가져오는 반복메소드
    private void printMenu(Iterator iterator) {
        while (iterator.hasNext()) {
            MenuItem menuItem = (MenuItem) iterator.next();
            System.out.print(menuItem.getName() + ", ");
            System.out.print(menuItem.getPrice() + " -- ");
            System.out.println(menuItem.getDescription());
        }
    }
}

이제 테스트 코드를 돌려봅시다.

public class MenuTestDrive {
    public static void main(String[] args) {
		PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
		DinerMenu dinerMenu = new DinerMenu();

        Waitress waitress = new Waitress(pancakeHouseMenu, dinerMenu);

        waitress.printMenu();
    }
}
갑자기 왜 깨지지.. 하튼 잘됩니다..

 

2) 다 된건가요?


지금까지 작업된 내용들은 정리해보면 다음과 같습니다.

  • 메뉴를 제공해주는 PancakeHouseMenu와 DinerMenu
  • 반복자 메소드인 PancakeHouseMenuIterator와 DinerMenuIterator
  • 반복자 메소드의 인터페이스인 Iterator
  • 메뉴를 전달해주는 종업원 역할을 하는 Waitress

기능적인 역할은 훌륭히 수행해주고 있지만, 아직 다음과 같은 문제점들이 남아있습니다.

  1. PancakeHouseMenu와 DinerMenu의 인터페이스가 완전히 똑같음에도 아직 인터페이스는 통일되지 않음
  2. 기존에 있는 Java의 Iterator를 안쓰고 굳이 만들어서 썼음(반복자 패턴 이해를 위해)

나머지 작업들은 그렇게 복잡하진 않습니다. 코드로 빠르게 확인해보시죠.

// PancakeHouseMenu.java

package iterator_pattern;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class PancakeHouseMenu {
    List<MenuItem> menuItems;

    // 기존 코드와 동일
    // 자바껄로 변경
    public Iterator<MenuItem> createIterator() {
        return menuItems.iterator();
    }

    // 기타 메뉴 관련 메소드..

}
// DinerMenuIterator.java

package iterator_pattern;

public class DinerMenuIterator implements Iterator{

    //기존 코드와 동일

    public void remove(){
        throw new UnsupportedOperationException("메뉴 항목은 지우면 안됩니다.");
    }
}

거의 끝나갑니다. 인터페이스를 통일해주는 작업을 진행해주기 위해 Menu라는 인터페이스를 작성해줍니다.

// Menu.java

mport java.util.Iterator;

public interface Menu {
    Iterator<MenuItem> createIterator();
}

그리고 Waitress도 수정해줍니다.

public class Waitress {
//    PancakeHouseMenu pancakeHouseMenu;
//    DinerMenu dinerMenu;

//    public Waitress(PancakeHouseMenu pancakeHouseMenu, DinerMenu dinerMenu) {
//        this.pancakeHouseMenu = pancakeHouseMenu;
//        this.dinerMenu = dinerMenu;
//    }

    // 위 코드를 아래 코드로

    Menu pancakeHouseMenu;
    Menu dinerMenu;

    public Waitress(Menu pancakeHouseMenu, Menu dinerMenu) {
        this.pancakeHouseMenu = pancakeHouseMenu;
        this.dinerMenu = dinerMenu;
    }
    
 .. 아래는 동일

이렇게 되면 Menu라는 동일한 인터페이스를 구현할 수 있으므로, PancakeHouseMenu와 DinerMenu에서 Menu 인터페이스를 구현하게 됩니다. 즉, createIterator()를 구현해야 합니다.

위와 같은 수정을 통해 반복되는 코드를 줄이고, 인터페이스 공유를 통해 코드의 직관성을 향상시켰습니다.

 

결론


이렇게 해서 반복자 패턴의 학습이 끝났습니다.
처음에 사전적 정의만 봤을 때는 뭔소린가.. 싶었는데, 예제를 보며 풀어가니 이해가 되는 부분이 많았습니다.

반복자 패턴을 한마디로 정리하자면

컬렉션의 구현 방법을 노출하지 않으면서 집합체 내의 모든 항목에 접근하는 방법을 제공하는 것.

다음 포스팅은 본 예제의 확장편인 컴포지트 패턴에 대해 포스팅하도록 하겠습니다.

구독 및 하트는 정보 포스팅 제작에 큰 힘이됩니다♡

728x90
반응형