객체지향 및 기반 언어/디자인 패턴

[Design Patterns] Chapter 09 반복자 패턴과 컴포지트 패턴

박지환 2022. 10. 23. 12:34

반복자 패턴의 정의

반복자 패턴(Iterator Pattern)은 컬렉션의 구현 방법을 노출하지 않으면서 집합체 내의 모든 항목에 접근하는 방법을 제공한다.

반복자 패턴 UML

Iterator 인터페이스를 통해 컬렉션 순회에 대한 일관된 인터페이스를 제공하고, 이를 Aggregate 인터페이스를 구현한 ConcreteAggregate 구상 클래스가 createIterator()를 통해 ConcreteIterator 구상 클래스를 만들고, 이를 이용하여 컬렉션을 순회한다.

반복자 패턴 예제 코드

public interface Menu {

    public Iterator<MenuItem> createIterator();
}
public class CafeMenu implements Menu {

    Map<String, MenuItem> menuItems = new HashMap<String, MenuItem>();

    public CafeMenu() {
        addItem("베지 버거와 에어 프라이", "통밀빵, 상추, 토마토, 감자 튀김이 첨가된 베지 버거", true, 3.99);
        addItem("오늘의 스프", "샐러드가 곁들여진 오늘의 소프", false, 3.69);
        addItem("부리토", "통 핀토콩과 살사, 구아카몰이 곁들여진 푸짐한 부리토", true, 4.29);
    }

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

    @Override
    public Iterator<MenuItem> createIterator() {
        return menuItems.values().iterator();
    }
}
public class DinerMenu implements Menu {

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

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

        addItem("채식주의자용 BLT", "통밀 위에 (식물성)베이컨, 상추, 토마토를 얹은 메뉴", true, 2.99);
        addItem("BLT", "통밀 위에 베이컨, 상추, 토마토를 얹은 메뉴", false, 2.99);
        addItem("오늘의 스프", "감자 샐러드를 곁들인 오늘의 스프", false, 3.29);
        addItem("핫도그", "사워크라우트, 갖은 양념, 양파, 치즈가 곁들여진 핫도그", false, 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.err.println("죄송합니다, 메뉴가 꽉 찼습니다. 더 이상 추가할 수 없습니다.");
        } else {
            menuItems[numberOfItems] = menuItem;
            numberOfItems = numberOfItems + 1;
        }
    }

    @Override
    public Iterator<MenuItem> createIterator() {
        return new DinnerMenuIterator(menuItems);
    }

    // 기타 메소드
}
public class PancakeHouseMenu implements Menu {

    ArrayList<MenuItem> menuItems;

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

        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);
    }

    @Override
    public Iterator<MenuItem> createIterator() {
        return menuItems.iterator();
    }

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

Menu 인터페이스는 Aggregate 인터페이스이며, 이를 CafeMenu, DinerMenu, PancakeHouseMenu이라는 ConcreteAggregate 구상 클래스가 createIterator()를 구현한다.

public class Waitress {

    Menu pancakeHouseMenu;
    Menu dinerMenu;
    Menu cafeMenu;

    public Waitress(Menu pancakeHouseMenu, Menu dinerMenu, Menu cafeMenu) {
        this.pancakeHouseMenu = pancakeHouseMenu;
        this.dinerMenu = dinerMenu;
        this.cafeMenu = cafeMenu;
    }

    public void printMenu() {
        Iterator<MenuItem> pancakeIterator = pancakeHouseMenu.createIterator();
        Iterator<MenuItem> dinerIterator = dinerMenu.createIterator();
        Iterator<MenuItem> cafeIterator = cafeMenu.createIterator();

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

    public void printMenu(Iterator<MenuItem> iterator) {
        while (iterator.hasNext()) {
            MenuItem menuItem = iterator.next();
            System.out.print(menuItem.getName() + ", ");
            System.out.print(menuItem.getPrice() + " -- ");
            System.out.println(menuItem.getDescription());
        }
    }

    // 기타 메소드
}
    public static void main(String[] args) {
        PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
        DinerMenu dinerMenu = new DinerMenu();
        CafeMenu cafeMenu = new CafeMenu();

        Waitress waitress = new Waitress(pancakeHouseMenu, dinerMenu, cafeMenu);

        waitress.printMenu();
    }
}

Waitress 클래스는 메뉴 리스트들을 CafeMenu, DinerMenu, PancakeHouseMenu 클래스가 구현해 놓은 createIterator() 메소드를 통해 가져온 Iterator를 통해 일관된 방식으로 가져올 수 있다.

단일 책임 원칙

디자인 원칙 9: 단일 역할 원칙(Single Responsibility Principle, SRP) 어떤 클래스가 바뀌는 이유는 하나뿐이어야 한다.

클래스에서 맡고 있는 모든 역할/책임은 나중에 코드 변화를 불러올 수 있고 이는 클래스를 사용하는 모든 부분에 영향을 미친다. 역할/책임이 2개 이상 있으면 바뀔 수 있는 부분이 2개 이상이 되므로, 클래스 변경의 이유가 2가지 이상이 된다. 따라서 하나의 클래스는 하나의 역할만 맡아야 한다.

Iterable 인터페이스와 Collection 인터페이스, 그리고 향상된 for 순환문

자바 컬렉션 프레임워크(Java Collection Framework)의 Collection 인터페이스는 Iterable 인터페이스를 구현한다. 또한 Collection 인터페이스에는 객체로 구성된 그룹을 조작하는 여러 유용한 메소드들이 포함되어 있다 (size(), toArray() 등)

Iterable 인터페이스에는 iterator() 메소드가 존재하고, 이를 구현한 Collection 인터페이스는 iterator() 메소드를 통해 Iterator를 리턴한다. 이를 통해 모든 컬렉션은 반복자를 통한 순회를 수행할 수 있다.

또한 자바의 향상된 for 순환문 for (자료형 변수명 : 배열명) { }을 사용할 수 있다.

컴포지트 패턴의 정의

컴포지트 패턴(Composite Pattern)으로 객체를 트리구조로 구성해서 부분-전체 계층구조(part-while hierarchy)를 구현한다. 컴포지트 패턴을 사용하면 클라이언트에서 개별 객체와 복합 객체를 똑같은 방법을 다룰 수 있다.

컴포지트 패턴 UML

Component 인터페이스는 복합 객체 내의 모든 객체의 인터페이스를 정의하며, 이를 구현하는 Leaf 인터페이스와 Composite 인터페이스는 각자에게 필요한 인터페이스만 구현한다. 클라이언트는 최종적으로 Component 인터페이스를 이용하여 복합 객체 내의 모든 객체들을 조작할 수 있다.

컴포지트 패턴 예제 코드

public abstract class MenuComponent {

    public void add(MenuComponent menuComponent) {
        throw new UnsupportedOperationException();
    }

    public void remove(MenuComponent menuComponent) {
        throw new UnsupportedOperationException();
    }

    public String getName() {
        throw new UnsupportedOperationException();
    }

    public String getDescription() {
        throw new UnsupportedOperationException();
    }

    public double getPrice() {
        throw new UnsupportedOperationException();
    }

    public boolean isVegetarian() {
        throw new UnsupportedOperationException();
    }

    public void print() {
        throw new UnsupportedOperationException();
    }
}
public class Menu extends MenuComponent {

    List<MenuComponent> menuComponents = new ArrayList<MenuComponent>();
    String name;
    String description;

    public Menu(String name, String description) {
        this.name = name;
        this.description = description;
    }

    @Override
    public void add(MenuComponent menuComponent) {
        menuComponents.add(menuComponent);
    }

    @Override
    public void remove(MenuComponent menuComponent) {
        menuComponents.remove(menuComponent);
    }

    public MenuComponent getChild(int i) {
        return menuComponents.get(i);
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getDescription() {
        return description;
    }

    @Override
    public void print() {
        System.out.print("\n" + getName());
        System.out.println(", " + getDescription());
        System.out.println("----------------------");

        for (MenuComponent menuComponent : menuComponents) {
            menuComponent.print();
        }
    }
}
public class MenuItem extends MenuComponent {

    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;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getDescription() {
        return description;
    }

    @Override
    public boolean isVegetarian() {
        return vegetarian;
    }

    @Override
    public double getPrice() {
        return price;
    }

    @Override
    public void print() {
        System.out.print(" " + getName());
        if (isVegetarian()) {
            System.out.print("(v)");
        }
        System.out.println(", " + getPrice());
        System.out.println("        " + getDescription());
    }
}

MenuComponent 추상 클래스는 Component 추상 클래스이며, 이를 MenuIteam Leaf 클래스와 Menu Composite 클래스가 상속받아 각자에게 필요한 요소들만 구현한다.

public class Waitress {
    MenuComponent allMenus;

    public Waitress(MenuComponent allMenus) {
        this.allMenus = allMenus;
    }

    public void printMenu() {
        allMenus.print();
    }
}
public class MenuTestDrive {

    public static void main(String[] args) {
        MenuComponent pancakeHouseMenu = new Menu("펜케이크 하우스 메뉴", "아침 메뉴");
        MenuComponent dinerMenu = new Menu("객체마을 식당 메뉴", "점심 메뉴");
        MenuComponent cafeMenu = new Menu("카페 메뉴", "저녁 메뉴");
        MenuComponent dessertMenu = new Menu("디저트 메뉴", "디저트를 즐겨보세요");

        MenuComponent allMenus = new Menu("전체 메뉴", "전체 메뉴");

        allMenus.add(pancakeHouseMenu);
        allMenus.add(dinerMenu);
        allMenus.add(cafeMenu);

        // 메뉴 항목 추가
        dinerMenu.add(new MenuItem("채식주의자용 BLT", "통밀 위에 (식물성)베이컨, 상추, 토마토를 얹은 메뉴", true, 2.99));
        dinerMenu.add(new MenuItem("BLT", "통밀 위에 베이컨, 상추, 토마토를 얹은 메뉴", false, 2.99));
        dinerMenu.add(new MenuItem("오늘의 스프", "감자 샐러드를 곁들인 오늘의 스프", false, 3.29));
        dinerMenu.add(new MenuItem("핫도그", "사워크라우트, 갖은 양념, 양파, 치즈가 곁들여진 핫도그", false, 3.05));

        dinerMenu.add(dessertMenu);

        dessertMenu.add(new MenuItem("애플 파이", "바삭바삭한 크러스트에 바닐라 아이스크림이 얹혀 있는 애플 파이", true, 1.59));

        cafeMenu.add(new MenuItem("베지 버거와 에어 프라이", "통밀빵, 상추, 토마토, 감자 튀김이 첨가된 베지 버거", true, 3.99));
        cafeMenu.add(new MenuItem("오늘의 스프", "샐러드가 곁들여진 오늘의 소프", false, 3.69));
        cafeMenu.add(new MenuItem("부리토", "통 핀토콩과 살사, 구아카몰이 곁들여진 푸짐한 부리토", true, 4.29));

        pancakeHouseMenu.add(new MenuItem("K&B 팬케이크 세트", "스크램블드 에그와 토스트가 곁들여진 펜케이크", true, 2.99));
        pancakeHouseMenu.add(new MenuItem("레귤러 팬케이크 세트", "달걀 후라이와 소시지가 곁들여진 펜케이크", false, 2.99));
        pancakeHouseMenu.add(new MenuItem("블루베리 펜케이크", "신선한 블루베리와 블루베리 시럽으로 만든 펜케이크", true, 3.49));
        pancakeHouseMenu.add(new MenuItem("와플", "와플, 취향에 따라 블루베리나 딸기를 얹을 수 있습니다.", true, 3.59));

        Waitress waitress = new Waitress(allMenus);

        waitress.printMenu();
    }
}

Waitress 클래스는 MenuComponent의 print() 메소드를 호출함으로써 MenuComponent 객체 아래에 트리 구조로 구성되어 있는 MenuComponent 개별 객체와 복합 객체를 print() 메소드로 일관된 방식으로 모두 호출 할 수 있다.

컴포지트 패턴의 투명성

컴포지트 패턴은 단일 역할 원칙을 깨는 대신, 투명성(transparency)를 확보하는 패턴이다 (클라이언트가 단일 인터페이스를 통해서 하위 인터페이스들을 투명하게 볼 수 있음)

이렇게 상황에 따라 디자인 원칙을 적절하게 사용해야 한다. 디자인 원칙을 따르면 좋지만, 그 원칙이 디자인에 어떤 영향을 끼칠지를 고민하고 원칙을 적용해야 한다.

때로는 일부러 원칙에 위배되는 방식으로 디자인을 하는 경우도 있다. (컴포지트 패턴처럼)

컴포지트 패턴의 특징

컴포지트 패턴은 GUI에 활용되며, 상위 구성 요소와 하위 구성 요소들이 컴포지트 패턴으로 디자인 되어있다. 이를 통해 복합 객체인 구성 요소에게 명령을 (화면에 나타나는 명령 등) 내리면 그 안에 있는 다른 구성 요소들에게도 같은 명령을 반복해서 전달한다.

또한 복합 구조가 너무 복잡하거나, 복합 객체 전체를 도는 데 너무 많은 자원이 들면, 복합 노드를 캐싱하는 방법으로 성능을 향상시킬 수 있다.

핵심 정리

  • 반복자를 사용하면 내부 구조를 드러내지 않으면서도 클라이언트가 컬렉션 안에 들어있는 모든 원소에 접근하도록 할 수 있음.
  • 반복자 패턴을 사용하면 집합체를 대상으로 하는 반복 작업을 별도의 객체로 캡슐화할 수 있음.
  • 반복자 패턴을 사용하면 컬렉션에 있는 모든 데이터를 대상으로 반복 작업을 하는 역할을 컬렉션에서 분리할 수 있음.
  • 반복자 패턴을 쓰면 반복 작업에 똑같은 인터페이스를 적용할 수 있으므로 집합체에 있는 객체를 활용하는 코드를 만들 때 다형성을 활용할 수 있음.
  • 한 클래스에는 될 수 있으면 한 가지 역할만 부여하는 것이 좋음.
  • 컴포지트 패턴은 개별 객체와 복합 객체를 모두 담아 둘 수 있는 구조를 제공함.
  • 컴포지트 패턴을 사용하면 클라이언트가 개별 객체와 복합 객체를 똑같은 방법으로 다룰 수 있음.
  • 복합 구조에 들어있는 것을 구성 요소라고 부름. 구성 요소에는 복합 객체와 잎 객체가 있음.
  • 컴포지트 패턴을 적용할 때는 여러 가지 장단점을 고려해야 함. 상황에 따라 투명성과 안정성 사이에서 적절한 균형을 찾아야 함.

 

GitHub Code Reference: https://github.com/Krapi0314/Design_Patterns

 

GitHub - Krapi0314/Design_Patterns: Java Implementation of GoF's Design Patterns

Java Implementation of GoF's Design Patterns. Contribute to Krapi0314/Design_Patterns development by creating an account on GitHub.

github.com

Reference: http://www.yes24.com/Product/Goods/108192370

 

헤드 퍼스트 디자인 패턴 - YES24

유지관리가 편리한 객체지향 소프트웨어 만들기!“『헤드 퍼스트 디자인 패턴(개정판)』 한 권이면 충분하다.이유 1. 흥미로운 이야기와 재치 넘치는 구성이 담긴 〈헤드 퍼스트〉 시리즈! 하나

www.yes24.com