컴포지트 패턴
🌲 컴포지트 패턴이란
객체들을 트리 구조로 구성하여 부분과 전체를 나타내는 계층구조로 만들수 있다. 클라이언트에서 개별 객체와 다른 객체들오 구성된 복합 객체(composite)를 똑같은 방법으로 다룰 수 있다.
식당 메뉴를 예로들어 생각해본다면 중첩되어 있는 메뉴 그룹과 메뉴 항목을 똑같은 구조 내에서 처리할수 있게끔 하는 것이다.
메뉴와 메뉴항목을 같은 구조에 집어넣어서 부분-전체 계층구조를 생성할수 있다.
이런 복합구조를 사용하면 복합 객체와 개별 객체에 대해 구분없이 똑같은 작업을 적용할 수 있다.
⛵️ 컴포지트 패턴의 이해
🚀 활용 예시
컴포지트 패턴을 메뉴에 적용시켜본다면…
우선 구성요소 인터페이스를 만드는 것부터 시작해보자.
이 인터페이스는 메뉴와 메뉴 항목 모두에 적용되는 공통 인터페이스 역할을 하며, 이 인터페이스가 있어야만 그 들을 똑같은 방법으로 처리할 수 있다.
즉 메뉴와 메뉴 항목에 대해서 같은 메소드를 호출 할 수 있게 된다.
Component 추상클래스 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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();
}
}
Component 를 상속받는 Leaf 객체 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class MenuItem extends MenuComponent { //MenuItem은 Leaf에 해당한다.
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() { //중요 - Composite 객체에서 공유하게 된다.
System.out.println(getName());
if (isVegetarian()) System.out.println("(v)");
System.out.println(getPrice());
System.out.println(getDescription());
}
}
Component 를 상속받는 Composite 객체 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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;
}
public void add(MenuComponent menuComponent) {
menuComponents.add(menuComponent);
}
public void remove(MenuComponent menuComponent) {
menuComponents.remove(menuComponent);
}
public MenuComponent getChild(int i) {
return menuComponents.get(i);
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public void print() { //중요 - Leaf 객체에서 공유하게 된다.
System.out.println(getName());
System.out.println(getDescription());
System.out.println("----------------------------------------------------");
Iterator<MenuComponent> iterator = menuComponents.iterator(); //Menu 정보 뿐아니라 Menu안의 아이템까지 출력
while (iterator.hasNext()) {
MenuComponent menuComponent = iterator.next();
menuComponent.print();
}
}
}
Client 객체 구현
1
2
3
4
5
6
7
8
9
10
11
12
public class Waitress {
MenuComponent allMenus;
public Waitress(menuComponent allMenus) {
this.allMenus = allMenus;
}
public void printMenu() {
allMenus.print();
}
}
테스트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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("파스타", "마리나라 소스 스파게티.", true, 3.89));
dinerMenu.add(dessertMenu);
dessertMenu.add(new menuItem("애플 파이", "바삭바삭한 크러스트에 바닐라아이스크림이", true, 1.59));
Waitress waitress = new Waitress(allMenus);
waitress.printMenu();
}
}
🧨 컴포지트패턴 vs 단일역할원칙
한 클래스에서 한 역할만 맡아야 한다고 했는데, 여기서는 계층구조를 관리하는 역할과 메뉴관련 작업을 처리하는 역할 두 가지를 처리하고 있다
컴포지트 패턴은 단일 역할 원칙을 깨면서, 대신에 투명성을 확보하기 위한 패턴이다.
🛠 컴포지트 + 이터레이터 패턴 적용
컴포지트 패턴 내에서 이터레이터 패턴을 활용해보자
Composite, Leaf 객체에 이터레이터 적용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Menu extends MenuComponent {
//나머지 코드는 그대로
public Iterator<MenuComponent> createIterator() {
return new CompositeIterator(menucomponents.iterator());
}
}
public class MenuItem extends MenuComponent {
//나머지 코드는 그대로
public Iterator<MenuComponent> createIterator() {
return new NullIterator();//널 반복자.
}
}
NullIterator 란
MenuItem에 대해서는 반복작업을 할 대상이없다. 그래서 createIterator()메소드를 구현하기가 애매해진다.
- 그냥 null을 리턴한다. 그렇다면 클라이언트에서 리턴값이 널인지 아닌지를 판단하기 위해 조건문을 써야하는 단점이있다.
- hasNext()가 호출되었을 때 무조건 false 를 리턴하는 반복자를 리턴한다.
두번째 방법은 여전히 반복자를 리턴할수 있기 때문에 클라이언트에서는 리턴된 객체가 널 객체인지에 대해 신경 쓸 필요가 없다.
이렇게 아무일도 하지 않는 반복자를 NullIterator라고 부른다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class NullIterator implements Iterator<Object> {
@Override
public boolean hasNext() {
return false;
}
@Override
public Object next() {
return null;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
Iterator 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class CompositeIterator implements Iterator {
Stack<Iterator<MenuComponent>> stack = new Stack();
public CompositeIterator(Iterator iterator) {
stack.push(iterator);
}
public MenuComponent next() {
Iterator<MenuComponent> iterator = stack.peek();
MenuComponent component = iterator.next();
if (component instanceof Menu) stack.push(((Menu) component).iterator());
return component;
}
public boolean hasNext() {
if (stack.empty()) return false;
Iterator<MenuComponent> iterator = stack.peek();
if (!iterator.hasNext()) {
stack.pop();
return hasNext();
} else {
return true;
}
}
public void remove() {
throw new UnsupporttedOperationException();
}
}
Iterator 를 적용한 Client 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Waitress {
MenuComponent allMenus;
public Waitress(MenuComponent allMenus) {
this.allMenus = allMenus;
}
public void printMenu() {
allMenus.print();
}
public void printVegetarianMenu() { //이 메소드는 '채식메뉴 인 것'만 추려낸다
Iterator<MenuComponent> iterator = allMenus.createIterator();
while (iterator.hasNext()) {
Menucomponent menuComponent = iterator.next();
try {
if (menuComponent.isVegetarian()) menuComponent.print();
} catch (UnsupportedOpoerationException e) {
}
}
}
}
try 문을 쓴 이유
위와 같이
try/catch코드를 쓴이유는instanceof를 써서 실행중에 형식을 확인하여isVegetarian()을 호출할 수도 있다. 하지만 그렇게하면Menu와MenuItem을 똑같이 다루지 않게 되는 셈이되어 투명성을 잃어버리게 된다.Menu의isVegetarian()메소드에서 무조건false를 리턴하도록 하는 방법도 있다. 이렇게하면 코드도 간단해지고 투명성도 계속 유지할수 있다.하지만 위 예제에서는
Menu에서는 그 메소드가 지원되지 않는다는 것을 분명하게 나타내기 위해서 사용되었다.

