11. Composite Pattern

2021. 10. 15. 18:58Computer Sciences/Design Patterns

문제

이터레이터 패턴에서 사용한 예시를 다시 써보자. 두 가게를 합병한 후로 장사가 탄탄대로를 달려서 이젠 점심 메뉴에 디저트 메뉴도 넣자는 기획이 들어왔다. 즉 점심 메뉴 안에 디저트 메뉴가 포함되는 것이다. 이러한 구조는 어떤 자료구조를 사용해야 할까? 메뉴 안에 메뉴... 트리 구조가 적당할 것 같다. 그림을 그려보면 대충 다음과 같다.

기존의 이터레이터 패턴을 유지하면서 모든 메뉴를 순회하도록 변경할 수 있을까? 정답부터 말하자면 그렇다. 컴포지트 패턴을 적용하면 이를 해결할 수 있다.

컴포지트 패턴

객체들을 트리 구조로 구성하여 부분과 전체를 나타내는 계층 구조로 만드는 패턴이다. 이 패턴을 이용하면 클라이언트에서 개별 객체와 다른 객체들로 구성된 복합 객체를 똑같은 방법으로 다룰 수 있다. 이번 예제에서는 Menu를 순회하면서 MenuItem에 접근하다가 Menu가 나오면 그 Menu를 다시 순회하며 MenuItem을 접근하는 방식으로 흘러간다. 이를 일반화하여 클래스 다이어그램으로 나타내면 다음과 같다.

이를 우리 프로젝트에 적용하면 다음과 같다.

  • Waitress
    • MenuComponent 인터페이스를 이용하여 MenuMenuItem에 모두 접근하게 된다.
  • MenuComponent
    • MenuComponentMenuItemMenu 모두에 적용되는 인터페이스를 나타낸다. 여기에서는 기본 메서드를 정의하기 위해 인터페이스가 아닌 추상 클래스를 사용했다.
  • MenuItem
    • MenuItem에서 쓰일 메서드들은 필요에 맞게 오버라이딩하고 나머지는 주어진 구현을 그대로 사용한다.
  • Menu
    • Menu에서 쓰일 메서드들은 필요에 맞게 오버라이딩하고 나머지는 주어진 구현을 그대로 사용한다.

이제 코드로 옮겨보자.

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

    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 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());
    }
}
public class Menu extends MenuComponent {

    ArrayList<MenuComponent> menuComponents = new ArrayList<>();
    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);
    }

    @Override
    public MenuComponent getChild(int i) {
        return (MenuComponent) menuComponent.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("--------------------------");

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

거의 다 끝났다. 이제 마지막으로 우리 웨이트리스 코드를 만들자.

public class Waitress {

    MenuComponent allMenus;

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

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

이를 동작시킬 테스트 코드를 빠르게 짜보자. 중간에 디저트 메뉴가 추가됨을 확인하기 위해 저녁 메뉴도 간단하게 넣어서 테스트하자.

public class MenuTest {

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

        pancakeHouseMenu.add(new MenuItem(
                        "팬케이크 세트 A", "우리 가게 대표 메뉴", true, 2.99));
        pancakeHouseMenu.add(new MenuItem(
                        "팬케이크 세트 B", "두 번째로 출시한 팬케이크 세트 메뉴", true, 2.99));
        pancakeHouseMenu.add(new MenuItem(
                        "레귤러 팬케이크 세트", "달걀 후라이와 소시지가 들어가 있는 팬케이크", false, 3.19));

        dinerMenu.add(new MenuItem(
                        "점심 세트 A", "우리 가게 대표 메뉴", true, 2.95));
        dinerMenu.add(new MenuItem(
                        "점심 세트 B", "두 번째로 출시한 점심 세트 메뉴", true, 2.95));
        dinerMenu.add(new MenuItem(
                        "핫도그 세트", "양파, 치즈, 많은 양념이 들어간 핫도그", false, 3.15));

        cafeMenu.add(new MenuItem(
                        "아메리카노", "원두의 향이 느껴지는 아메리카노", true, 1.29));

        dinerMenu.add(dessertMenu);

        dessertMenu.add(new MenuItem(
                        "애플 파이", "바삭바삭한 애플 파이", true, 1.59));

        Waitress waitress = new Waitress(allMenus);

        waitress.printMenu();
    }
}

실행한 결과는 다음과 같다.

단일 책임 원칙을 위반한 것 아닌가?

맞다. 컴포지트 패턴은 단일 책임 원칙을 깨면서 투명성을 확보하기 위한 패턴이라고 할 수 있다. 투명성이란 뭘 뜻할까? Component 인터페이스에 자식들을 관리하기 위한 기능과 리프 노드로써의 기능을 전부 집어넣음으로써 클라이언트에서 복합 객체와 리프 노드를 똑같은 방식으로 처리할 수 있도록 한다. 어떤 원소가 복합 객체인지 리프 노드인지가 클라이언트 입장에서는 투명하게 느껴지는 것이다.

Component 클래스에는 두 종류의 기능이 모두 들어가 있으므로 안정성은 조금 떨어진다. 사용하지 말아야 할 기능을 호출할 수도 있기 때문이다. 이러한 문제는 설계 결정 사항에 포함된다. 안정성 측면을 고려한다면 여러 역할로 분리해서 다른 인터페이스로 구현할 수도 있다. 하지만 이렇게 설계한다면 각 인스턴스를 확인하기 위해 조건문 또는 instanceof와 같은 코드가 추가될 것이다. 아니면 우리가 한 것처럼 예외를 던질 수도 있다.

질문으로 돌아와보면 지적한 내용은 상황에 따라 원칙을 적절하게 사용해야 한다는 것을 알 수 있다. 원칙은 지켰을 때 더 좋은 가이드라인 정도인 것이지 반드시 지켜야 하는 법칙이 아니다. 많은 경우에는 원칙을 따르는 편이 더 좋으나 항상 그 원칙이 우리가 설계하고 있는 디자인에 어떤 영향을 끼칠지 생각해보아야 한다.

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

13. Bridge Pattern  (0) 2021.11.14
12. State Pattern  (0) 2021.10.15
10. Iterator Pattern  (0) 2021.10.15
9. Template Method Pattern  (0) 2021.10.14
8. Facade Pattern  (0) 2021.10.14