Composite Pattern 은 한 인터페이스를 통해 Object Hierarchy 를 정의하여 생성할 수 있을 때 사용되는 패턴이다. 이 패턴을 사용하면 Tree 형태의 Hierarchical 한 구조를 표현할 수 있으며, 그렇게 표현된 Object 들을 동일한 역할체로 취급할 수 있다. 설명을 읽으면 이해가 안될 수 있는 꽤나 복잡한 패턴으로, 코드를 같이 보면서 이해해 나가길 권장한다.
Composite Pattern 은 위와 같이 가상 상위 클래스를 두고, Hierarchy 상 최상단에 위치하는 Root Node 와 가상 클래스를 Inheritance 하며 Associate 하는 형태로 만들어준다. 이는 Decorator Pattern 에서도 봤던 모습이다. 그리고 abstract 상위 클래스를 상속받는 다른 자식 Node 들을 만들어주는 형태를 가진다.
다음과 같은 형태의 비즈니스 상황이 있다고 고려해보자. 모든 Object 종류는 Menu 라는 최상단 Node 에 속하며, 자식 항목들은 MenuItem Object 들로 나뉘어진다 (아마 원래는 MenuItem 이 Menu 를 상속받는 구조였을 것이다). 이 때 이를 위 컴포지트 패턴을 이용해서 구현해보면 다음과 같다.
// 최상위 부모 클래스로 별 의미 없는 가상 Class
// 모든 자식들을 동일하게 묶어주기 위함
// 하지만 각 자식들이 Impl 해야하는 함수들이 다르므로, 의무성을 주지 않기 위해 Concrete 로 생성
// print() 처럼 둘다 가져가지만 다른 일을 수행해야 하는 애들만 두고 abstract 로 묶어줘도 된다
public class MenuComponent {
public MenuChild getChild(int i){
throw new UnsupportedExcepion();
}
public String getName(){
throw new UnsupportedExcepion();
}
public String getDesc(){
throw new UnsupportedExcepion();
}
public double getPrice(){
throw new UnsupportedExcepion();
}
public void print(){
// do Nothing
}
}
우선 가상 Class 의 주된 목적은 모든 Node 들을 동일한 역할체로 취급해주기 위함이다. 그 역할체가 바로 MenuComponent.class 이다. 이 때, abstract 혹은 interface 로 역할체 선언을 하지 않은 이유는, Hierarchy 상 Root Node 와 다른 Child Node 들이 Implement 하는 함수들이 다를 수 있기 때문에, 의무성을 주지 않기 위해서이다.
물론 위 가상클래스가 하는 역할은 오직 같은 역할체로 묶어줄 뿐이다. print() 처럼 같은 일을 수행하지만 Root Node 와 Child Node 들이 다르게 수행해야 하는 것들만 명시해준채로 abstract class 로 만드는게 개인적으로는 더 직관적인 모습이라고 생각한다.
public abstract class MenuComponent {
public abstract void print();
}
// HIERARCHY 상 ROOT NODE
public class Menu extends MenuComponent {
private List<MenuComponent> menuComponents = new ArrayList<>();
// 같긴 하지만 일반적인 MENU 단위의 NAME & DESC 와는 논리적으로 다를 수 있다
private String name;
private String desc;
public Menu(String name, String desc){
this.name = name;
this.desc = desc;
}
public void add(MenuComponent menuComponent){
this.menuComponents.add(menuComponent);
}
public void remove(MenuComponent menuComponent){
this.menuComponents.remove(menuComponent);
}
public MenuComponent getChild(int i){
this.menuComponents.get(i);
}
// @Override name and desc
@Override
public void print(){ // 일반 Menu 와 하는 일이 달라야 한다
Iterator iterator = menuComponents.iterator();
while(iterator.hasNext()){
((MenuComponent) iterator.next()).print();
}
}
public class MenuItem extends MenuComponent {
private String name;
private String desc;
private double price;
public MenuItem(String name, String desc, double price){
this.name = name;
this.desc = desc;
this.price = price;
}
// GETTERS
@Override
public void print(){
System.out.println("name = " + getName());
System.out.println("price = " + getPrice());
System.out.println("desc = " + getDesc());
}
}
위와 같이 MenuComponent 로 묶어줌으로써 Root Node 클래스에서는 자식들을 하나의 역할체로 통일된 자료구조에 담을 수 있고, polymorphism 을 활용하여 각자가 수행할 추상 함수들을 쉽게 수행시킬 수 있다.
참고로 이 부분을 위해서는 Iterator 가 등장하는데, Composite Pattern 과 Iterator 는 긴밀한 연관성을 가지고 있다. 이터레이터 패턴의 주된 장점인 내부적인 자료구조의 상황을 노출하지 않고도 hasNext() 와 next() 를 가지고 일관된 접근을 할 수 있다는 점이 동일하기 때문이다. 이를 위해선 Iterator 와 조금 복잡한 연동이 필요하긴 하다.
이제 다음과 같이 Test 해보자.
public static void main(String [] args){
Menu noodleRestMenu = new Menu("Noodle Restaurant Menu", "Breakfast");
Menu dinerMenu = new Menu("Diner Menu", "Lunch");
Menu cafeMenu = new Menu("Cafe Menu", "Dessert");
// Root Node 를 원하는대로 만들어주고 Menu 함수의 child 를 수행한다
Menu allMenues = new Menu("ALL Restaurant Menu", "All Restaurant Menues");
allMenues.add(noodleMenu);
allMenues.add(dinerMenu);
allMenues.add(cafeMenu);
// MenuComponent 중 Menu 라는 새로운 Parent 를 No SQL 처럼 새로 달아주고 싶다
Menu allDessertMenuOfDiner = new Menu("Dessert menu of Diner", "Desserts");
dinerMenu.add(allDessertMenuOfDiner);
// 이젠 알맞은 MenuItem Child 들을 적절한 Root Node 에 매핑해주면 Tree 가 완성된다
// 여기서 Root Node 란 단순히 Menu 객체를 말한다. 순수 Root 는 당연히 allMenues Object 이다
noodleRestMenu.add(new MenuItem(..));
allDessertMenuOfDiner.add(new MenuItem(..));
// ...
}
위와 같이 Root 역할을 하는 Menu 클래스끼리 매핑도 되고, Child 역할을 하는 MenuItem 도 적절히 Root Node 에 바인딩 시킬 수 있다. 이는 마치 NoSQL 의 구조와 매우 유사하다 (No SQL에서도 Root 에서 시작하는 Collection 이 있고, 또 다른 Node 에서 다시 Collection 을 추가할 수 있다).
참고로 위와 같이 실행해보면 왜 저자가 처음에 일반 Class 로 abstract 함수처럼 구현했는지 알 수 있다. 모든 객체를 역할체로 선언해도 add(), remove() 와 같은 함수를 수행할 수 있도록 하기 위해서이다. 하지만 직관성을 위해 나는 추상으로 활용하였다.
위에서 print 를 수행해본다면, 각 Menu 와 MenuItem 이 수행할 print 함수 에 대해서 DFS 방식으로 print() 를 수행하는 모습을 확인할 수 있다. Root Node 에 자료구조를 잘 제시한다면 (PQ 등) 원하는 방식으로 MenuComponent 들을 순회하도록 제어할 수도 있다.
사실 가장 헷갈리는 부분은 가상 클래스이다. 위에 잠깐 얘기했던 MenuComponent 같은 가상 클래스를 Concrete 하게 할지, 아니면 역할체로 선언할지에 대해서 조금만 더 자세히 알아보자. 결국 MenuComponent 가 Parent 가 될지 Child 가 될지 모르는데, Child Node 들을 관리하는 add(), remove(), getChild() 를 어떻게 활용해야 할지에 대한 문제이다.
좌측 방식처럼 Parent Node(Composite Class) 가 Node 측면에서 할 수 있는 모든 일들을 가상 부모 클래스에 명시해두는 방식을 Transparency 방식이라고 한다. 해당 방식은 Child Class 는 사용하지 않겠지만, Parent Class 와 더불어 동일하게 <Component> 라는 객체로 묶일 수 있다는 장점이 있어서, Client 측면에서 유지 / 보수에 유리하다 (Composite Class 에 대한 의존도가 낮아짐).
하지만 위 방식은 Component Class 에서 default 로 수행되는 방식 (Child 에서 Call 하는 잘못된 요청) 에는 Exception 을 발생시켜야 한다. 따라서 정말 Composite Class 들과 Child Class 들이 같은 Component 로서 polymorphism 에 따라 수행해야할 함수만 명시해두어서 정말 abstract 한 가상 Class 를 상속시키는 방식을 Safety 방식이라고 한다. 이렇게 하면 client 측면에서는 Composite Class 에 대해 의존도가 강해진다 (직접 Composite Class 로서 add(), remove() 를 수행해야 하기 때문). 하지만 예외를 발생시켜야 하는 일이 사라지며, 조금 더 가상 Class 에 맞는 구조를 가져갈 수 있다.
저자는 좌측, 나는 우측 방향을 선택한 것임을 알 수 있다. 어떤 방식을 채택할건지는 개발자의 기준, 비즈니스 로직 내에서의 판단 등을 생각하여 고려해야한다.
마지막으로 Composite Pattern 은 Decorator Pattern 과 UML 구조적으로 유사하다. 자신의 상위 클래스를 attribute 로 가져가며 상속까지 받는 특이한 구조를 가지고 있기 때문이다. 하지만 두 패턴은 의도가 다르다. Decorator Pattern 은 책임을 확장시켜서 Interface 를 강화시키는 것이 목적이였지만, Composite Pattern 은 하나의 Interface 로 Tree 구조 (Hierarchy Data Structure) 을 제공하는 것이 목적이다.
정리하며
Composite Pattern 은 처음에 이해하기에 상당히 까다로운 패턴이다. Tree 구조가 도입되고, Root Node 가 있으면서 Root Node 역할을 하는 Composite Class 가 또 자식으로 들어올 수 있는 NoSQL 구조이기 때문이다. 지금까지 살펴본 디자인 패턴 중 이렇게 Hierarchy 를 중심으로 짜여지는 패턴은 없었던 것 같다.
처음에는 다소 어렵지만 Composite Pattern 은 Tree 자료구조와 디자인 패턴이 잘 녹아들어 복잡한 형태의 도메인들을 관리하기에 적합하며, 모든 Node 들을 하나의 가상 객체로 묶어서 편리하게 관리할 수 있다는 장점이 있는 유용한 패턴이다. 적절한 상황을 만나 잘 사용해볼 수 있는 기회가 된다면 좋겠다.
출처
1) 에릭 프리먼, 『Head First Design Patterns』, O'Reilly Media (2004)
2) 중앙대학교 소프트웨어학부 이찬근 교수님 디자인 패턴 수업자료 중
'SW 설계 > Design Pattern' 카테고리의 다른 글
[Design Pattern] Structural - 브릿지 패턴 (Bridge Pattern) (0) | 2023.10.22 |
---|---|
[Design Pattern] Structural - 아답터 패턴 (Adapter Pattern) (0) | 2023.10.22 |
[Design Pattern] Structural - 데코레이터 패턴 (Decorator Pattern) (1) | 2023.10.21 |
[Design Pattern] Creational - 싱글톤 패턴 (Singleton Pattern) (1) | 2023.10.21 |
[Design Pattern] Creational - 빌더 패턴 (Builder Pattern) (0) | 2023.10.21 |