컴포지트 패턴 Composite Pattern
컴포지트 패턴
- 객체를 트리구조로 구성하여 부분-전체 계층구조를 구현.
- 모든 노드는 요소(Component)로서의 동일한 특성을 공유한다.
- 개별 객체로서의 리프와 복합 객체로서의 중간 노드(Composite)는 동일한 메서드를 구현하고 동일한 동작을 해야 한다.
- 다만 복합 객체는 개별 객체 전체의 행동을 담보하고 통합적인 동작을 수행한다.
- 결과적으로 클라이언트는 컴포지트 패턴을 활용하면 그것이 개별 객체인지 복합 객체인지 관계 없이 동일한 동작의 수행을 보장받는다.
예제 - 할 일(ToDo)
- 컴포지트 패턴을 사용하여 Todo를 구현했다.
- 인터페이스는 Notification 이며 noti 메서드를 가진다.
- 구현체는 Todo와 TodoGroup이 있다. 전자는 하나의 할 일을 가지고 후자는 여러 개의 할 일을 가진다.
public interface Notification {
String noti();
}
@Getter
public class ToDo implements Notification {
private final LocalDate day;
private final String description;
public ToDo(LocalDate day, String description) {
this.day = day;
this.description = description;
}
@Override
public String noti() {
return "[" + day + "]" + description + "\n";
}
}
- 컴포지트를 구현 할 때 리프는 자신에 대한 구현만을 필요로 한다.
- 자식을 가진 노드는 자식을 포함한 자손을 접근하고 이를 통합적으로 구현할 의무를 가진다.
- 그러므로 컴포지트 패턴의 경우 두 가지 형태로 구현한다. 앞서 ToDo는 리프로서 구현하고 노드로서 ToDoGroup은 리프에 대한 통합적인 제어를 수행한다.
- ToDoGroup은 자식 노드를 수집하고, noti 메서드에서 자식의 noti를 합성한 값을 생성한다.
public class TodoGroup implements Notification {
private final List<Notification> notifications = new ArrayList<>();
private final String description;
public TodoGroup(String description) {
this.description = description;
}
public void add(Notification notification) {
notifications.add(notification);
}
@Override
public String noti() {
String category = "["+description+"]";
StringBuilder sb = new StringBuilder();
for (Notification n : notifications) {
String[] split = n.noti().split("\n");
for (String s : split) {
sb.append(category).append(s).append("\n");
}
}
return sb.toString();
}
}
- 클라이언트는 ToDo 혹은 ToDoGroup이 아닌 Notification 객체로서 noti 메서드를 통해 접근한다.
- ToDoGroup은 무한한 계층을 가질 수 있다. 아래 예제를 보면 외부활동(out) - 운동(workout) - 리프(자전거타기)의 3 단계의 계층이 있다.
TodoGroup out = new TodoGroup("외부 활동");
out.add(new ToDo(LocalDate.of(2023, 4, 1), "만우절 체험"));
out.add(new ToDo(LocalDate.of(2023, 4, 6), "진달래 구경"));
out.add(new ToDo(LocalDate.of(2023, 4, 2), "친구들과 여행가기"));
TodoGroup workOut = new TodoGroup("운동");
out.add(workOut);
workOut.add(new ToDo(LocalDate.of(2023, 3, 9), "한강 자전거 타기"));
ToDo family = new ToDo(LocalDate.of(2023, 5, 9), "가족 저녁 식사");
TodoGroup myTodoList = new TodoGroup("나의 일정");
myTodoList.add(out);
myTodoList.add(family);
System.out.println(myTodoList.noti());
컴포지트의 장점과 단점
- 공통 특성이 많은 경우 유용하다. 특히 UI 혹은 이벤트 프레임워크는 자식, 자손 노드에 대한 통합적인 관리를 필요로 하며, 컴포지트 패턴을 활용한다.
- HTML을 예를 들면,
- div 태그를 hidden으로 숨긴다면 div의 자식 태그 역시 hidden으로 하는 것이 맞다.
- 일관적인 프로그래밍을 간단하게 처리하도록 도와준다.
- 새로운 요소의 추가가 간편하다. ToDoGroup에 새로운 ToDo를 쉽게 삽입 가능하며, ToDoGroup은 해당 변화를 즉각적으로 반영한다.
- 다만, 공통된 인터페이스의 정의 과정에서 지나치게 일반화 해야 하는 경우도 생길 수 있다. 일반화를 실패하여 노드의 타입이 무엇인지 체크하고 분기할 수도 있다. 이러한 상황이 발생하면 컴포지트 패턴의 포기를 고려해야 한다.
참조
- 백기선, 디자인 패턴 강의(https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4)
- O’Reilly, “헤드 퍼스트 디자인 패턴”