컴포지트 패턴 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, “헤드 퍼스트 디자인 패턴”