지난 포스팅에선 컬렉션의 구현 방법을 노출하지 않으면서도 그 칩합체 안에 들어있는 모든 항목에 접근할 수 있게 하는 이터레이터 패턴에 대해 알아보았다. 이번에는 트리 구조를 구성하여 부분과 전체를 나타내는 계층구조로 표현할 수 있는 컴포지트 패턴에 대해 복습한다.

정의

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

트리구조

윈도우의 파일과 폴더를 예시로 들어보자. 바탕화면에 여러 가지 폴더와 파일이 있다. 폴더 안에는 다시 폴더 혹은 파일이 존재한다. 이처럼 하나의 부모에 여러 자식, 다시 그 자식이 부모가 될 수 있는 이러한 계층 구조를 트리라고 부른다. 자식이 있는 원소는 노드(node), 자식이 없는 원소는 **잎(leave)**이라고 부른다.

윈도우의 명령 프롬프트에서 dir 명령어를 입력하면 현재 경로의 파일과 폴더 목록을 출력한다. 이 명령어를 자바에서 구현한다고 생각해보자.

폴더도 단지 자식을 갖고 있는 파일에 불과하다고 생각할 수 있다. 컴포지트 패턴을 이용하면 폴더와 파일을 똑같은 방법으로 다룰 수 있다. 파일과 폴더 항목을 같은 구조에 집어넣어 부분-전체 계층구조를 생성한다. 이러한 구조는 부분(파일과 폴더)들이 모여있지만, 모든 것을 하나로 묶어서 전체로 다룰 수 있다. 폴더는 단지 다른 파일 객체가 들어있는 파일이기 때문에 파일과 같은 개별 객체, 폴더와 같은 복합 객체 모두 결국 복합 객체로 다룰 수 있다. 컴포지트 패턴을 따르는 디자인을 이용하면 간단한 코드로도 파일과 폴더 정보를 출력한다거나 하는 반복 작업을 전체 파일 구조에 대해서 반복해서 적용할 수 있다.

다이어그램

Component 는 복합 객체 내에 들어있는 모든 객체들에 대한 인터페이스를 정의한다. 복합 노드 뿐 아니라 잎 노드에 대한 메소드까지 정의하여 부분-전체를 다룬다

Leaf 는 그 안에 들어있는 원소에 대한 행동을 정의한다. Leaf는 자식이 없는 개별 객체이며 Composite에서 지원하는 기능을 구현해야 한다.

Composite 는 자식이 있는 구성요소의 행동을 정의하고 자식 구성요소를 저장하는 역할을 맡는다. 부분-전체 구조를 다루어야 하므로 복합 객체뿐 아니라 개별 객체인 Leaf와 관련된 기능도 구현해야 한다. 그런 기능들은 쓸모 없지만 예외를 던지거나 하는 방법을 사용한다.

복합 객체인 Composite에는 Component가 들어있다. 구성요소인 Component는 두 종류로 나뉘는데, 하나는 자식 요소를 갖는 복합 객체이고 다른 하나는 자식 요소가 없는 개별 객체이다. 이러한 구조는 재귀적인 성격을 갖는다. 복합 객체에는 일련의 자식들이 있으며, 그 자식들은 다시 다른 자식들을 갖는 복합객체일 수 있다. 데이터의 구조를 부모-자식으로 조직화하다보면 뿌리는 최상위의 복합 객체로부터 시작해서 마지막에는 잎으로 끝나는 트리 구조가 된다. 정확하게는 뿌리(루트)가 있고, 점점 넓어지면서 아래로 내려가는 뒤집힌 나무 모양새가 만들어진다.

예시

객체마을에 있는 어느 식당에 대한 예시이다. 식당에는 메뉴판과 메뉴 항목이 있다. 메뉴판은 팬케이크 메뉴, 객체마을 식당 메뉴, 카페 메뉴와 객체마을 식당 메뉴의 하위 메뉴인 디저트 메뉴가 있다. 메뉴, 메뉴 안에 들어있는 서브메뉴, 메뉴 항목을 모두 표현하려면 트리와 같은 구조가 적합하다. 메뉴 안에 서브 메뉴를 집어넣을 수 있고, 메뉴 항목도 넣을 수 있어야 하기 때문이다. 모든 요소를 하나로 묶어서 생각할 수도 있고 각각 개별로 취급할 수도 있다. 한 메뉴에 대해서만 반복작업을 한다든가 전체 메뉴에 대한 반복작업을 하는 작업을 더 유연하게 적용할 수 있게 된다. 컴포지트 패턴을 이용해서 디자인 해보자.

MenuCoponent를 이용해서 Menu와 MenuItem에 모두 접근할 수 있다.

MenuComponent 는 Menu와 MenuItem 모두에 적용되는 인터페이스를 정의한다. 기본 메소드의 구현을 정의하기 위해서 인터페이스가 아닌 추상 클래스를 사용해도 된다.

MenuItem 은 각 메뉴 항목에서 사용할 메소드만 오버라이드하고 나머지는 기본 구현을 사용한다. 예를 들어 add() 메소드는 Menu에만 사용할 수 있으므로 필요 없다. name, description, vegetarian, price와 같은 속성을 정의하고 get() 메소드에서 이를 반환한다.

Menu 는 메뉴에서 쓰일만한 메소드만 오버라이드하고 나머지는 기본 구현을 사용한다. 예를 들어 isVegetarian() 메소드는 메뉴 항목이 야채 종류인지 알아내기 위한 메소드이므로 적합하지 않는다. name, description와 같은 속성만 정의하고 get() 메소드에서 이를 반환한다.

public void print() {
    System.out.println("\n" + getName());
    System.out.println(", " + getDescription());
    System.out.println("-----------------");
    // 재귀
    Iterator<MenuComponent> iter = menuComponents.iterator();
    while(iter.hasNext()) {
        MenuComponent menuComponent = iter.next();
        menuComponent.print();
    }
}

메뉴는 복합 객체이며 그 안에는 MenuItem과 Menu가 모두 들어있을 수 있다. 따라서 메뉴의 print() 메소드를 호출하면 그 안에 있는 모든 구성요소들의 정보가 출력되어야 한다. print() 메소드에서는 Menu에 대한 정보 뿐 아니라 Menu에 들어있는 다른 메뉴 및 메뉴 항목에 대한 정보까지 출력하도록 구현한다. 메뉴와 메뉴 항목 모두 print() 메소드를 구현하고 있기 때문에 이터레이터 패턴을 사용해서 print() 를 호출하고 나머지는 객체에서 알아서 처리하길 기다리면 된다. 만약 반복작업을 수행하는 중에 다른 메뉴가 나타나면 기존 작업을 잠시 중단하고 그 메뉴의 반복작업을 수행하게 된다.

복합 반복자

복합 반복자란 복합 객체 안에 들어있는 구성 요소에 대해 반복작업을 할 수 있게 해 주는 기능을 제공한다. 이터레이터 패턴에서 외부 반복자란 녀석에 대해 알아봤었다. 위의 식당 예시에서는 반복 작업을 내부 반복자를 이용해서 처리했는데, 구성요소가 MenuItem이 아닌 경우에는 재귀적으로 print()를 호출해서 작업을 처리한다.

하지만, 외부 반복자인 복합 반복자는 밖에 있는 클라이언트에서 next(), hasNext() 와 같은 메소드를 이용해서 반복작업을 처리하기 때문에 반복작업 중의 현재 위치를 관리해야 한다.

의문점

컴포지트 패턴에서는 계층구조를 관리하는 일, 메뉴하고 관련된 작업을 처리하는 일과 같이 두 가지 역할을 맡고있다. 단일 역할 원칙에 위배되는 것이 아닌가?

컴포넌트 패턴에서는 단일 역할 원칙을 깨는 대신에 투명성을 확보하기 위한 패턴이라고 할 수 있다. 여기서 투명성(transparency) 이란 인터페이스에 자식 관리, 잎으로써의 기능을 모두 넣음으로써 클라이언트에서 복합 객체와 잎 노드를 똑같은 방식으로 처리할 수 있게 된 효과를 말한다. 클라이언트에서 어떤 원소가 복합 객체인지 잎 노드인지가 중요하지 않으므로 투명하다고 표현한다.

투명성을 확보하는 대신 안정성은 약간 떨어지게 된다. 클라이언트에서 어떤 원소에 대해 무의미한, 또는 부적절한 작업을 처리하려고 할 수 있기 때문이다. 예를 들어 파일에 파일을 집어넣는다던가 하는 작업을 말이다. 상황에 따라 원칙을 적절하게 사용해야 한다.

안정성을 확보하려고 역할에 따라 인터페이스를 분리하게되면 조건문이나 instanceof 연산자 같은 것을 사용하니 안정성이 떨어지고, 투명성을 확보하려고 두 가지 역할을 넣게 되면 앞서 말한 이유 때문에 안정성이 떨어진다. 가이드라인을 따르는 것이 좋긴 하지만, 항상 그 원칙이 우리가 생각하고 있는 디자인에 어떤 영향을 끼칠지를 생각해야 한다.

자식한테 부모 래퍼런스가 있을 수도 있나?

트리 내에서 돌아다니기 편하게 하기 위해서 자식에게 부모 노드에 대한 포인터를 집어넣을 수도 있다. 자식에 대한 래퍼런스를 지워야 하는 경우에도 반드시 그 부모한테 자식을 지우라고 해야 되는데, 이러한 경우 부모 래퍼런스를 만들어두면 더 수월하게 처리가 가능하다.

어떤 복합 객체에서 자식을 특별한 순서에 맞게 저장해야 한다면 어떻게 해야 하나?

자식을 추가하거나 제거할 때 더 복잡한 관리 방법을 사용해야 하며 계층구조를 돌아다니는 데 있어서도 더 주의를 기울여야 한다.

캐시를 사용하는 경우는?

복합 구조가 너무 복잡하거나, 복합 객체 전체를 한 바퀴 도는 데 너무 많은 자원이 필요한 경우 복합 노드를 캐싱해두면 도움이 된다. 복합 객체에 있는 자식에 대해서 어떤 계산을 하고, 그 모든 자식들에 대해서 반복작업을 수행해야 한다면 계산 결과를 임시로 저장하는 캐시를 만들어서 성능을 향상시킬 수 있다.

마지막으로 컴포지트 패턴의 가장 큰 장점은?

클라이언트를 단순화시킬 수 있다는 점이다. 클라이언트는 복합 객체, 잎 객체 중 어느 것을 사용하고 있는가에 신경 쓰지 않아도 된다. 목적에 맞는 객체의 메소드를 호출하고 있는지 확인하기 위해 여기저기 if문을 작성하지 않아도된다. 메소드 하나만 호출하면 전체 구조에 대해서 반복해서 작업을 처리할 수 있다.

컴포지트 패턴 예제