BackEnd/Design Patterens

[Design Patterns] Composite Pattern : 컴포지트 패턴

샤아이인 2022. 1. 13.

Head First Design Patterns 책을 읽으며 정리한 내용입니다. 문제가 될 시 글을 내리도록 하겠습니다!

Composite Pattern 이란?

Composite Pattern - 객체들을 트리 구조로 구성하여 부분과 전체를 나타내는 계층구조로 만들 수 있습니다.
이 패턴을 이용하면 Client에서 개별 객체와 다른 객체들로 구성된 복합객체(composite)를 똑같은 방법으로 다룰 수 있게됩니다.

 

패턴 소개

다음과 같이 Iterator 를 활용하여 메뉴를 출력하는 코드가 있다고 해봅시다.

이 코드들은 이전 글인 Iterator 에서 나왔던 코드들 입니다. 기억 하시죠?

import java.util.Iterator;

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 pancakeIterator = pancakeHouseMenu.createIterator();
        Iterator dinerIterator = dinerMenu.createIterator();
        Iterator cafeIterator = cafeMenu.createIterator();

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

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

Iterator를 활용하여 Waitress 와 컬렉션의 구현부를 분리했다는 장점이 있지만, 아직도 코드중복이 3번이나 되고 있군요?

PrintMenu()만 해도 반복자를 3번 반환받으며, 3번 출력하고 있습니다. 이는 OCP(open closed princple)에 위배되는 군요...

 

여러 메뉴를 한번에 관리할 수 있는 방법이 필요합니다!

 

ArrayList로 메뉴들을 묶어서 그 반복자를 이용하여 각 메뉴에 대해 순환을 돌면 좋지않을까요? 다음과 같이 말이죠!

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

public class Waitress {
    ArrayList<Menu> menus;

    public Waitress(ArrayList<Menu> menus) {
        this.menus = menus;
    }

    public void printMenu() {
        Iterator<Menu> menuIterator = menus.iterator();
        while(menuIterator.hasNext()){
            Menu menu = menuIterator.next();
            printMenu(menu.createIterator());
        }
    }

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

이번에는 디저트 서브메뉴를 추가해달라는 요청이 들어왔습니다.

DinerMenu 컬랙션의 원소로 디저트메뉴를 집어넣을수 있으면 좋을것 같군요? 다음과 같이 말이죠

DinerMenu 안에 서브메뉴가 들어갈 수 있으면 좋겠지만... 형식이 다르기때문에 MenuItem으로 구성된 메뉴에 서브메뉴를 집어넣을 수가 없습니다. DinerMenu는 MenuItem 만 원소로 갖을 수 있는데, 서브메뉴는 MenuItems 이기 때문이죠...

새로운 디자인이 필요하겠군요?

 

새로운 디자인에는 어떤것들이 필요할까요?

▶ 메뉴, 서브메뉴, 메뉴 항목 등을 모두 집어넣을 수 있는 트리

▶ 각 메뉴에 있는 모든 항목에 대해서 순회하면서 어떤 작업을 할 수 있는 방법을 제공해야한다. 마치 반복자 처럼 말이죠

▶ 유연한 방법으로 아이템들에 대하여 반복작업이 가능해야 합니다. 가령 디저트 메뉴에만 반복작업을 한다던가? 아님 점심메뉴 들에만 반복작업을 한다던가? 와 같이 말이죠

 

트리의 모형이 다음과 같을것 입니다. 눈으로 확인해 보시죠?

출처 - Composite Pattern의 Part-Whole Hierarchy (Head First)

컴포지트 패턴을 이용하면 중첩되어 있는 메뉴 그룹과 메뉴 아이템을 똑같은 구조내에서 처리할 수 있습니다.

메뉴와 메뉴아이템을 똑같은 구조에 집어넣어 부분-전체 계층구조(part-whole hierarchy)를 생성할 수 있습니다.

부분-전체 계층구조란 부분(메뉴, 메뉴아이템)들이 모여있지만, 모든 것을 하나로 묶어 전체로 다를 수 있는 구조를 의미합니다.

 

다시 설명해 보면...

메뉴, 서브메뉴, 서브서브메뉴 등과 함께 메뉴항목으로 구성된 트리 구조가 있다고 하면 각각이 모두 복합 객체가 될 수 있다는 의미입니다.

각 메뉴안에 다른 메뉴 및 아이템들이 있을 수 있으니까요! 또한 객체도 메뉴라고 할 수 있습니다. 다른 객체가 들어있지 않을 뿐인거죠!

 

예를 들어 위의 그림을 보면 각 PancakeHouseMenu, DinerMenu, CafeMenu 안에 다른 item들이 있고, DinerMenu같은 경우 다른 메뉴인 디저트메뉴 까지 있는상황입니다.

 

우선 다이어그램을 살펴볼까요?

Client 입장에서는 Component의 인터페이스만 알면 객체들을 조작할 수 있습니다. 구현과 분리된 거죠!!

Component에서는 복합객체와 leaf에 대한 인터페이스를 정의합니다.

 

그런데 구성이 좀 복잡한것 같군요? Component, Composite, 트리 등.. 뭐이리 복잡하죠? 전 한눈에 안들어오더군요?

복합객체(composite)에는 구성요소(component)가 들어있습니다. 구성요소는 2종류가 있죠. 

leaf와 복합객체(composite) 입니다.

재귀적이라는 점이 느껴지시나요? 복합객체가 자신의 구성요소로 복합객체를 갖고있다자나요!!

 

root 노드에서는 복합객체에서 시작해서 복합객체를 따라 계속 아래로 내려가다 마지막에는 leaf 노드에 도달하여 끝나는 트리구조가 형성됩니다.


지금부터는 실제로 코드로 구현해봐야 이해가 될것 같군요?

우선 Component 인터페이스를 만드는것 부터 시작합니다. 이 인터페이스는 메뉴와 메뉴아이템 모두에 적용됩니다. 그래야 다형성을 적용하여 둘다 같은 방법으로 처리가 가능하겠죠?

 

전반적인 숲을 보면 다음 다이어그램과 같습니다.

출처 - Composite Pattern (Head First 397p)
 
MenuComponent부터 구현해야 겠죠?
public abstract class MenuComponent {
    // MenuComponent를 추가하고 제거하고 가져오기 위한 메소드
    public void add(MenuComponent menuComponent) {
        throw new UnsupportedOperationException();
    }
    public void remove(MenuComponent menuComponent) {
        throw new UnsupportedOperationException();
    }
    public MenuComponent getChild(int i) {
        throw new UnsupportedOperationException();
    }

    // MenuItem, Menu 에서 작업을 처리하기 위해 사용하는 메소드.
    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();
    }
}
 

모든 함수들이 UnsupportedOperationException() 을 throw 하고 있습니다. 왜 이렇게 만든것 일까요?

어떤 메소드는 MenuItem에서만 쓸 수 있고, 또 어떤 메소드는 Menu에서만 쓸수 있기 때문에 기본적으로 모두 예외를 던지도록 만들었습니다.

이렇게 하면 자기 역할에 맞지 않는 메소드는 오버라이딩을 하지 않고 기본구현을 그대로 사용하게 될 것 입니다.

 

 

다음으로는 MenuItem 클래스를 만들어 봅시다! 이 클래스는 leaf node에 해당한다는점, 복합객체의 원소에 해당하는 행동을 구현해야 한다는점 잘 기억합니다!

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

    public String getName() { return name; }
    public String getDescription() { return description; }
    public double getPrice() { return price; }
    public boolean isVegetarian() { return vegetarian; }
    public void print() { // MenuComponent에 있던 메소드를 오버라이딩 하였습니다!
        System.out.print("  " + getName());
        if (isVegetarian()) {
            System.out.print("(v)");
        }
        System.out.println(", " + getPrice());
        System.out.println("     -- " + getDescription());
    }
}
 

 

이번에는 복합객체 클래스인 Menu를 준비해야합니다. 복합객체 클래스에는 MenuItem, Menu 모두 저장할 수 있습니다.

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

public class Menu extends MenuComponent {
    ArrayList<MenuComponent> menuComponents = new ArrayList<MenuComponent>();
    String name;
    String description;

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

    public void add(MenuComponent menuComponent) { menuComponents.add(menuComponent); }
    public void remove(MenuComponent menuComponent) { menuComponents.remove(menuComponent); }
    public MenuComponent getChild(int i) {return ( MenuComponent)menuComponents.get(i); }
    public String getName() { return name; }
    public String getDescription() { return description; }

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

위의 코드에서 add, remove는 Menu를 추가하는 코드입니다. Menu, MenuItem 모두 동일한 인터페이스를 구현했기 때문에 다형성을 적용하여 하나의 메소드로 둘다 처리가 가능합니다.

 

다 끝난것 같지만 하나 이상한점이 남아있습니다. 바로 Print() 입니다.

 

Menu는 복합객체 이기 때문에 그안에 Menu, MenuItem 모두 들어올 수 있습니다. 따라서 Print()를 호출하면 그 안에 있는 모든 구성요소들에 맞게 출력되어야 합니다. 다음과 같이 변경해줘야 합니다.

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

        Iterator<MenuComponent> iterator = menuComponents.iterator();
        while (iterator.hasNext()) {
            MenuComponent menuComponent = iterator.next();
            menuComponent.print();
        }
    }
 

Print()가 해당 메뉴의 정보뿐만 아니라, Menu에 들어있는 모든 구성요소에 대한 정보까지 출력하도록 고치면 됩니다.

 

이제 이 모든것을 사용하는 Client 또한 만들어야 겠죠? 간단하게 만들 수 있습니다.

public class Waitress {
    MenuComponent allMenus;

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

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

생성자에서 트리의 최상위 구성요소를 넘겨받으면 됩니다.

 

테스트 코드를 돌려봅시다!

package com.company.Composite;

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); // 최상위 메뉴에 add()를 이용하여 메뉴를 추가합니다.
        allMenus.add(dinerMenu);
        allMenus.add(cafeMenu);

        dinerMenu.add(new MenuItem( // 각 메뉴마다 메뉴아이템들을 추가해주어야 합니다.
                "Vegetarian BLT",
                "(Fakin') Bacon with lettuce & tomato on whole wheat",
                true,
                2.99));

        dinerMenu.add(new MenuItem(
                "Pasta",
                "Spaghetti with Marinara Sauce, and a slice of sourdough bread",
                true,
                3.89));

        dinerMenu.add(dessertMenu); // 저녁 메뉴에 디저트 메뉴를 추가할수도 있죠!

        dessertMenu.add(new MenuItem( // 디저트 메뉴도 메뉴아이템을 추가해줘야겠죠?
                "Apple Pie",
                "Apple pie with a flakey crust, topped with vanilla icecream",
                true,
                1.59));

        Waitress waitress = new Waitress(allMenus); // 다 만든 메뉴를 종업원에게 넘기면 됩니다.
        waitress.printMenu();
    }
}
 

실행결과는 다음과 같습니다.

전체 메뉴, 전체 메뉴
---------------------

팬케이크 하우스 메뉴, 아침 메뉴
---------------------
  K&B's Pancake Breakfast(v), 2.99
     -- Pancakes with scrambled eggs, and toast
  Regular Pancake Breakfast, 2.99
     -- Pancakes with fried eggs, sausage
  Blueberry Pancakes(v), 3.49
     -- Pancakes made with fresh blueberries, and blueberry syrup
  Waffles(v), 3.59
     -- Waffles, with your choice of blueberries or strawberries

객체마을 식당 메뉴, 점심 메뉴
---------------------
  Vegetarian BLT(v), 2.99
     -- (Fakin') Bacon with lettuce & tomato on whole wheat
  BLT, 2.99
     -- Bacon with lettuce & tomato on whole wheat
  Soup of the day, 3.29
     -- A bowl of the soup of the day, with a side of potato salad
  Hotdog, 3.05
     -- A hot dog, with saurkraut, relish, onions, topped with cheese
  Steamed Veggies and Brown Rice(v), 3.99
     -- Steamed vegetables over brown rice
  Pasta(v), 3.89
     -- Spaghetti with Marinara Sauce, and a slice of sourdough bread

디저트 메뉴, 디저트를 즐겨보세요!
---------------------
  Apple Pie(v), 1.59
     -- Apple pie with a flakey crust, topped with vanilla icecream
  Cheesecake(v), 1.99
     -- Creamy New York cheesecake, with a chocolate graham crust
  Sorbet(v), 1.89
     -- A scoop of raspberry and a scoop of lime

카페 메뉴, 저녁 메뉴
---------------------
  Veggie Burger and Air Fries(v), 3.99
     -- Veggie burger on a whole wheat bun, lettuce, tomato, and fries
  Soup of the day, 3.69
     -- A cup of the soup of the day, with a side salad
  Burrito(v), 4.29
     -- A large burrito, with whole pinto beans, salsa, guacamole

Process finished with exit code 0
 

이러한 방식의 구현은 단일 역할 원칙에 위배되는 내용이기는 합니다.

클래스 하나당 하나의 역할만을 수행해야 하는데, 우리의 Component 인터페이스는 자식들을 관리하는 기능 + leaf node의 기능 둘다 갖고있죠...

 

다만 하나를 포기한 대신 얻은것이 있겠죠? 바로 투명성 입니다.

한 인터페이스에 두가지 기능을 전부 집어넣음으로써 클라이언트에 복합객체(composite)와 leaf를 똑같은 방식으로 처리할 수 있도록 하였습니다.

댓글