文書の過去の版を表示しています。
目次
the Definition of Composite Pattern
컴포지트 패턴의 정의는 다음과 같습니다.
컴포지트 패턴을 이용하면 객체들을 트리 구조로 구성하여 부분과 전체를 나타내는 계층구조로 만들 수 있습니다. 이 패턴을 이용하면 클라이언트에서 개별 객체와 다른 객체 들로 구성된 복합 객체(composite)를 똑같은 방법으로 다룰 수 있습니다.
가령 메뉴를 기준으로 생각해 봅시다. 이 패턴을 이용하면 중첩되어 있는 메뉴 그룹과 메뉴 항목을 똑같은 구조내에서 처리할 수 있습니다. 메뉴와 메뉴 항목을 같은 구조에 집어 넣어서 부분-전체 계층구조(part-whole hierachy)를 생성할 수 있습니다. 여기서 부분-전체 계층구조란, 부분(메뉴 및 메뉴 항목)들이 모여있지만, 모든 것을 하나로 묶어서 전체로 다룰 수 있는 구조를 뜻합니다.
일단 메뉴를 이런 방식으로 만들고 나면 컴포지트 패턴을 써서 객별 객체와 복합객체들을 똑같은 식으로 다룰 수 있습니다. 조금 어렵죠? 다시 설명해 보자면, 메뉴, 서브메뉴, 서브서브메뉴 등과 함께 메뉴 항목으로 구성된 트리 구조가 있다고 하면 각각이 모두 복합 객체가 될 수 있다는 것입니다. 각 메뉴 안에 다른 메뉴 및 메뉴 항목이 또 들어있을 수 있으니까요. 개별 개체도 결국 메뉴라고 할 수 있습니다. 다른 객체가 들어있지 않을 뿐 이죠. 앞으로 배우게 되겠지만, 컴포지트 패턴을 따르는 디자인을 사용하면 간단한 코드만 가지고도 (인쇄같은) 똑같은 작업을 전체 메뉴 구조에 대해서 반복해서 적용할 수 있습니다.
이제 컴포지트 패턴의 클래스 다이어그램을 살표볼 차례입니다.
Class Diagram
컴포지트 패턴을 메뉴에 어떻게 적용할 수 있을까요? 우선 구성요소 인터페이스를 만드는 것 부터 시작해야 됩니다. 이 인터페이스는 메뉴와 메뉴 항목 모두에 적용되는 공통 인터페이스 역할을 하며, 이 인터페이스가 있어야만 그 둘을 똑같은 방법으로 처리할 수 있습니다. 즉, 메뉴와 메뉴 항목에 대해서 같은 메소드를 호출 할 수 있게 되죠.
사실 메소드 중에는 메뉴 항목에 대해서 호출하는게 말이 안되는 것도 있을 것이고, 메뉴에 대해 호출하면 이상한 메소드도 있을 것입니다. 하지만 그런 문제를 해결하는 방법은 잠시 후에 알아보도록 하겠습니다. 일단 지금은 메뉴를 어떤 식으로 컴포지트 패턴에 끼워 맞출 수 있을지 생각해 봅시다.
MenuComponent
이제 MenuComponent 추상 클래스부터 시작해 봅시다. 메뉴 구성요소는 잎 노드와 복합노드 모두에서 쓰이는 인터페이스 역할을 한다는 점을 꼭 기억해 둡시다. 어쩌면 “이 MenuComponent에서 두가지 역할을 맡고 있는 것이 아닌가?”라는 생각을 하실지 모르겠습니다. 물론 그렇게 생각할 수 있지만, 그 점에 대해서는 나중에 다시 생각해 보도록 하겠습니다. 일단 지금은 MenuItem(잎)이나 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(); } }
MenuItem Class
이제 MenuItem 클래스를 만들어 봅시다. 이 클래스는 컴포지트 패턴 다이어그램에서 앞에 해당하는 클래스라는 점, 그리고 복합 객체의 원소에 해당하는 행동을 구현해야 한다는 점을 잘 기억해 둡시다.
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 Iterator createIterator() { return new NullIterator(); } public String getName(){ return name; } public String getDescription() { return description; } public double getPrice() { return price; } public boolean isVegetarian() { return vegetarian; } public void print() { System.out.print(" " + getName()); if(isVegetarian()) { System.out.print("(v)"); } System.out.println(", " + getPrice()); System.out.println(" -- " + getDescription()); } }
복합 객체에 대한 반복자를 구현하기 위해서는 모든 구성요소에 createIterator()메소드 를 추가해야 합니다. 잉? 그런데 NullIterator는 도대체 뭘까요? 잠시 후에 알아보도록 하겠습니다.
Menu Class
MenuItem도 다 준비됐으니 이제 복합 객체 클래스인 Menu만 준비하면 됩니다. 복합 객체 클래스에는 MenuItem는 물론 다른 Menu도 저장할 수 있습니다. MenuComponent에 있는 메소드 가운데 getPrice()와 isVegetarian()메뉴에서는 별 의미가 없기 때문에 구현하지 않습니다.
public class Menu extends MenuComponent { ArrayList menuComponents = new ArrayList(); String name; String description; public Menu(String name, String description) { this.name = name; this.description = description; } public Iterator createIterator(){ return new CompositeIterator(menuComponents.iterator()); } 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 getDescription() { return description; } public void print() { System.out.print("\n" + getName()); System.out.println(", " + getDescription()); System.out.println("---------------------"); Iterator iterator = menuComponents.iterator(); while (iterator.hasNext()) { MenuComponent menuComponent = (MenuComponent) iterator.next(); menuComponent.print(); } } }
getPrice()와 isVegetarian()메소드는 Menu에는 어울리지 않는 메소드이므로 그냥 구현하지 않습니다. (어쩌면 isVegetarian은 메뉴에도 적용할 수 있을 것 같지만, 여기에서는 그렇게 하지 않겠습니다.) Menu에 대해서 이 메소드를 호출하면 UnsupportedOperationException이 던져질 것입니다.
print()메소드를 살펴보면 Iterator패턴이 사용되었음을 알 수 있습니다. 반복작업을 수행하는 중에 다른 메뉴가 나타난다면 그 메뉴에서 또 다른 반복잡업을 실행하게 됩니다. 서브 메뉴가 여러 단계로 중첩되어 있으면 그런 과정이 여러번 반복되겠죠.
CompositeIterator Class
CompositeIterator는 중책을 맡고 있는 반복자입니다. 복합 객체 안에 들어있는 MenuItem에 대해 반복작업을 할 수 있게 해 주는 기능을 제공하죠. 모든 자식 메뉴들 및 자식의 자식 등도 빠짐없이 챙겨야 합니다.
코드는 다음과 같습니다. 코드 자체는 그리 길지 않지만 조금 이해하기 힘들 수도 있습니다. 계속 머릿속으로 “나는 재귀호출이랑 친하다. 나는 재귀호출이랑 친하다…“라고 되뇌어 봅시다.
import java.util.* public class CompositeIterator implements Iterator { Stack stack = new Stack(); public CompositeIterator(Iterator iterator) { stack.push(iterator); } public Object next() { if (hasNext()) { Iterator iterator = (Iterator) stack.peek(); MenuComponent component = (MenuComponent) iterator.next(); if (component istanceof Menu) { stack.push(component.createIterator()); } return component; } else { return null; } } public boolean hasNext() { if (stack.empty()) { return false; } else { Iterator iterator = (Iterator) stack.peek(); if (!iterator.hasNext()){ stack.pop(); return hasNext(); } else { return true; } } } public void remove() { throw new UnsupportedOperationException(); } }
NullIterator Class
이제 널 반복자(Null Iterator)가 왜 필요한지 알아봐야 할 때가 되었군요. MenuItem에 대해서 생각해 보변, 반복작업을 할 대상이 없다는 것을 알 수 있습니다. 상황이 그렇다 보니 createIterator()메소드를 구현하기가 애매해집니다. 이런 경우에 두가지 방법을 떠올릴 수 있을 것입니다.
첫 번째 방법:
널을 리턴한다.
createIterator()에서 그냥 널을 리턴할 수도 있을 것입니다. 하지만 그렇게 하면 클라이언트에서
리턴된 값이 널인지 아닌지를 판단하기 위한 조건문을 써야 한다는 단점이 있죠.
두 번째 방법:
%%hasNext()%%가 호출되었을 때 무조건 false를 리턴하는 반복자를 리턴한다.
이 방법이 좀 나아 보이는군요. 이렇게 하면 여전히 반복자를 리턴할 수 있기 때문에
클라이언트에서는 리턴된 객체가 널 객체인지에 대해 신경 쓸 필요가 없습니다. “아무 일도 하지 않는” 반복자를 만든다고 생각하면 됩니다.
두 번째 방법이 확실히 좋아 보이는군요. 아무 일도 하지 않는 반복자를 NullIterator라고 부르고, 다음과 같은 식으로 구현합시다.
import java.util.Iterator; public class NullIterator implements Iterator { public Object next() { return null; } public boolean hasNext() { return false; } public void remove() { throw new UnsupportedOperationException(); } }