복합 패턴(Composite Pattern) ; 추상화

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

클라이언트는 Component 라는 복합객체 인터페이스에 의존하여 복합노드를 포함한 하위 모든 객체에 접근하거나 조작 가능하다.

Tree와 Composite Pattern 의 구성요소 비교

Tree의 구성요소

  • 노드(Node) : 자식이 있는 원소
  • 잎(Leaf) : 자식이 없는 원소

Composite Pattern 의 구성요소

  • Composite : 자식이 있는 객체
  • Leaf : 자식이 없는 객체

책에서 나오는 예제 - Category 가 다른 Menu 구성하기

클라이언트(웨이트리스)가 접근할 MenuComponent 를 구성하는 예제에 대해 설명하고 있다.

최상위 Composite, 클라이언트가 접근할 interface

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface MenuComponent {
default void add(MenuComponent menuComponent) {
throw new UnsupportedOperationException();
}

default void remove(MenuComponent menuComponent) {
throw new UnsupportedOperationException();
}

default void getChild(int i) {
throw new UnsupportedOperationException();
}

default void getDescription() {
throw new UnsupportedOperationException();
}
}

Leaf에 해당하는 객체, MenuComponent 로 부터 add, remove 는 구현하지 않는다.
–> leaf 에서 add, remove 메소드를 호출하면 UnsupportedOperationException 이 발생한다.

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
public class MenuItem implements MenuComponent {
private String name;
private String description;
private int price;

public MenuItem(String name, String description, int price) {
this.name = name;
this.description = description;
this.price = price;
}

public String getName() {
return name;
}

@Override
public String getDescription() {
return description;
}

@Override
public int getPrice() {
return price;
}
}

Composite 객체, 또 다른 Components 를 멤버로 갖고 있다. 또한 leaf의 역할도 수행하기 위해 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
public class Menu implements MenuComponent {
private List<MenuComponent> menuComponents = new ArrayList<>();
private String name;
private 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 menuComponents.get(i);
}

public String getName() {
return name;
}

@Override
public String getDescription() {
return description;
}
}

결론

우리가 객체를 설계할 때 그 구조가 트리구조를 가지고 있을 때, 컴포지트 패턴을 사용하면 이점이 많다.
하지만, 패턴은 추상화를 통해 과도하게 일반화 하고 있는 것은 아닌가 질문을 던져야 할 때가 있다.
컴포지트 패턴의 가장 큰 이점은 ‘클라이언트가 개별 객체와 컬렉션 객체를 동일한 인터페이스로 제어 가능’ 하다는 점을 충분히 인지해야 한다.

생각해보자

컴포지트 패턴에서는 그 활용도가 높을 것이라 기대하진 않았다.
다만, ‘Tree 자료구조를 쓰면 되지 뭐하러 이렇게 귀찮게 쓰지?’ 라는 다소 위험한 생각을 하게 되었다.

Tree vs Composite : 데이터 vs 객체

만약 디렉토리 구조를 시스템으로 구현한다고 가정해보자, 물론 계층구조라는 점과 node, leaf 가 명확히 구분된다는 점에서
‘Tree’ 라는 자료구조가 먼저 떠오르게 된다. (그리고 계층구조를 그리다 보면 필연적으로 Tree 구조로 그리게 되어 있다.)

Tree 는 기본적으로 어떤 데이터를 다루기 위한 자료구조이다. 따라서 아래와 같은 장 단점이 존재할 것이다.

Tree 장점

  • Tree 에 익숙한 개발자라면, 구현 난이도나 활용도가 높다
  • 순회 탐색에 특화된 자료구조이기 때문에 요소 탐색의 방법이 많다(전위, 중위, 후위 …)

Tree 단점

  • Tree 라는 자료구조의 기능으로만 제한된다.
  • 실제 Tree 를 구현해보면 V, T 의 재귀형태로 구현해야 한다.
    익숙하지 않다면, 구현하는데 꽤나 어려움이 있다.

데이터를 다루기 위함인지, 객체를 구성하기 위함인 지 구분해야 한다.
컴포지트 패턴은 객체를 다루기 위한 기술이다. 목적에서도 알 수 있듯이 ‘클라이언트가 동일한 인터페이스로 개별객체와 컬렉션을 제어하기 위함’에 있음을 알아야 한다.
물론 구현하는 기술이나 방법은 Tree와 매우 흡사하지만, 객체의 세계에서는 전혀 다르다는 것을 알아야 할 것 같다.

책에서는 방법론적인 측면에서 Tree와 재귀, 컴포지트 패턴이 매우 유사하다고만 나와 있어서 조금 아쉬웠다.

참고자료