본문 바로가기

OOP, FP/디자인패턴

헤드퍼스트 디자인 패턴: 11. 컴포지트 패턴


지난 포스팅에선  컬렉션의 구현 방법을 노출하지 않으면서도 그 칩합체 안에 들어있는 모든 항목에 접근할 수 있게 하는 이터레이터 패턴에 대해 알아보았다.


이번에는 트리 구조를 구성하여 부분과 전체를 나타내는 계층구조로 표현할 수 있는 컴포지트 패턴에 대해 복습한다.


컴포지트 패턴

객체들을 트리 구조로 구셩하여 부분과 전체를 나타내는 계층구조로 만든다.

컴포지트 패턴을 이용하면 클라이언트에서 개별 객체와 다른 객체들로 구성된 복합 객체(Composite)를 똑같은 방법으로 다룰 수 있다.



트리 구조는 IT 전공자들에게 익숙한 구조이다.

우리가 컴퓨터를 할 때도 쉽게 접할 수 있다.


윈도우에 로그인하면 바탕화면에 여러 가지 폴더와 파일이 있다.

폴더 안에는 다시 폴더 혹은 파일이 존재한다.


이런식으로 하나의 부모에 여러 자식, 다시 그 자식이 부모가 될 수 있는 이러한 계층 구조를 트리라고 부른다.

여기서 자식이 있는 원소(폴더)는 노드(node), 자식이 없는 원소(파일)은 잎(leave)이라고 부른다.




윈도우의 명령 프롬프트에서 dir 명령어를 입력하면 현재 경로의 파일과 폴더 목록을 출력한다.

이 명령어를 자바에서 구현한다고 생각해보자.


폴더도 단지 자식을 갖고 있는 파일에 불과하다고 생각해볼 수 있다.

파일에는 자식 파일의 목록 혹은 부모 파일의 ID, 파일의 TYPE(폴더, 파일), 확장자, 파일명 등등이 있을것이다.

이런 경우 폴더와 파일을 똑같은 방법으로 다룰 수 있으면 편할 것 같은데.. 이럴 때 컴포지트 패턴이 도움을 줄 수 있겠다.


파일과 폴더 항목을 같은 구조에 집어넣어 부분-전체 계층구조를 생성할 수 있다.

이러한 구조는 부분(파일과 폴더)들이 모여있지만, 모든 것을 하나로 묶어서 전체로 다룰 수 있다.


폴더는 단지 다른 파일 객체가 들어있지 않은 파일이기 때문에 파일과 같은 개별 객체, 폴더와 같은 복합 객체 모두 결국 복합 객체로 다룰 수 있다는 이야기다.


컴포지트 패턴을 따르는 디자인을 이용하면 간단한 코드로도 파일과 폴더 정보를 출력한다거나 하는 반복 작업을

전체 파일 구조에 대해서 반복해서 적용할 수 있다.




(1) Component

- Component에서는 복합 객체 내에 들어있는 모든 객체들에 대한 인터페이스를 정의한다.

  복합 노드 뿐 아니라 잎 노드에 대한 메소드까지 정의하여 부분-전체를 다룬다


(2) Leaf

- Leaf에서는 그 안에 들어있는 원소에 대한 행동을 정의한다.

  Leaf는 자식이 없는 개별 객체이며 Composite에서 지원하는 기능을 구현해야 한다.


(3) Composite

- Composite에서는 자식이 있는 구성요소의 행동을 정의하고 자식 구성요소를 저장하는 역할을 맡는다.

  부분-전체 구조를 다루어야 하므로 복합 객체뿐 아니라 개별 객체인 Leaf와 관련된 기능도 구현해야 한다.

  그런 기능들은 쓸모 없지만 예외를 던지거나 하는 방법을 사용해야 한다.


참고사항

* 복합 객체에는 Component가 들어있다.

- 위 구조에서 구성요소인 Component는 두 종류로 나뉜다. 하나는 자식 요소를 갖는 복합 객체이고, 

  다른 하나는 자식 요소가 없는 개별 객체이다.

  이러한 구조는 재귀적인 성격을 갖는다. 복합 객체에는 일련의 자식들이 있으며, 그 자식들은 다시 다른 자식들을 갖는 복합객체일 수 있다.

  데이터의 구조를 이런식으로 조직화하다보면 뿌리는 최상위의 복합 객체로부터 시작해서 마지막에는 잎으로 끝나는 트리 구조가 된다.

  정확하게는 뿌리(루트)가 있고, 거기에서부터 점점 넓어지면서 아래로 내려가는 뒤집한 나무 모양새가 만들어진다.


자 그럼 책에서 등장한 예제를 살펴보자.

어떤 식당에 메뉴판과 메뉴 항목인 음식들에 대한 클래스가 있다.

여기서 메뉴판은 팬케이크 메뉴, 객체마을 식당 메뉴, 카페 메뉴와 객체마을 식당 메뉴의 하위 메뉴인 디저트 메뉴가 있다.




메뉴, 메뉴 안에 들어있는 서브메뉴, 메뉴 항목을 모두 표현하려면 트리와 같은 구조가 적합하다.

메뉴 안에 서브 메뉴를 집어넣을 수 있고, 메뉴 항목도 넣을 수 있어야 하기 때문이다.

모든 요소를 하나로 묶어서 생각할 수도 있고 각각 개별로 취급할 수도 있다.

결국 한 메뉴에 대해서만 반복작업을 한다든가 전체 메뉴에 대한 반복작업을 하는 작업을 더 유연하게 적용할 수 있게 된다.


위의 그림과 같은 식당을 컴포지트 패턴을 이용해서 디자인 해보자.




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


(2) MenuComponent

- MenuComponent에서는 Menu와 MenuItem 모두에 적용되는 인터페이스를 정의한다.

  기본 메소드의 구현을 정의하기 위해서 인터페이스가 아닌 추상 클래스를 사용해도 된다.


(3) MenuItem

- MenuItem에서 쓰일만한 메소드만 오버라이드하고 나머지는 기본 구현을 사용한다.

  예를 들어 add() 메소드는 Menu에만 사용할 수 있으므로 여기선 필요 없다.


(4) Menu

- Menu에서 쓰일만한 메소드만 오버라이드하고 나머지는 기본 구현을 사용한다.

  예를 들어 isVegetarian() 메소드는 메뉴 항목이 야채 종류인지 알아내기 위한 메소드이므로 메뉴엔 적합하지 않다.

- 자식 노드인 Component의 목록을 갖는다.


구현을 살펴보자.


우선 구현쪽은 다이어그램만 봐도 대충 감이 온다.


- MenuItem에서는 name, description, vegetarian, price와 같은 속성을 정의하고 get() 메소드에서 이를 반환하면 되겠다.


- Menu에서는 name, description와 같은 속성만 정의하고 마찬가지로 get() 메소드에서 이를 반환하고

  add(), remove()와 getChild() 메소드는 List<MenuComponent>의 메소드를 사용하면 된다.


단지 Menu의 print()메소드의 구현이 조금 특이하다.


메뉴는 복합 객체이며 그 안에는 MenuItem과 Menu가 모두 들어있을 수 있다(Component).

따라서 메뉴의 print()메소드를 호출하면 그 안에 있는 모든 구성요소들의 정보가 출력되어야 한다.

앞서 재귀적인 성격을 갖는다고 했는데 바로 이러한 특징 때문이다.


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();

}

}


print() 메소드에서는 Menu에 대한 정보 뿐 아니라 Menu에 들어있는 다른 메뉴 및 메뉴 항목에 대한 정보까지 출력하도록 하면 된다.

메뉴와 메뉴 항목 모두 print() 메소드를 구현하고 있기 때문에 이터레이터 패턴을 사용해서

print()를 호출하고 나머지는 객체에서 알아서 처리하길 기다리면 된다.


만약 반복작업을 수행하는 중에 다른 메뉴가 나타나면 기존 작업을 잠시 중단하고 그 메뉴의 반복작업을 실행하게 된다.

서브메뉴가 여러 단계로 중첩되어 있으면 이런 과정은 여러 번 반복된다.


의문점

(1) 컴포지트 패턴에서는 계층구조를 관리하는 일, 메뉴하고 관련된 작업을 처리하는 일과 같이 두 가지 역할을 맡고있다.

    단일 역할 원칙에 위배되는 것이 아닌가?

- 컴포넌트 패턴에서는 단일 역할 워칙을 깨는 대신에 투명성을 확보하기 위한 패턴이라고 할 수 있다.

  여기서 투명성(transparency)란 인터페이스에 자식 관리, 잎으로써의 기능을 모두 넣음으로써 클라이언트에서

  복합 객체와 잎 노드를 똑같은 방식으로 처리할 수 있게 된 효과를 말한다.

  클라이언트에서 어떤 원소가 복합 객체인지 잎 노드인지가 중요하지 않으므로 투명하다고 할 수 있다.


- 투명성을 확보하는 대신 안정성은 약간 떨어지게 된다.

  클라이언트에서 어떤 원소에 대해 무의미한, 또는 부적절한 작업을 처리하려고 할 수 있기 때문이다.

  예를 들어 파일에 파일을 집어넣는다던가 하는 작업을 말이다.


- 상황에 따라 원칙을 적절하게 사용해야 한다는 예시를 보여주는 것이다.

  안정성을 확보하려고 역할에 따라 인터페이스를 분리하게되면 조건문이나 instanceof 연산자 같은 것을 사용하니 안정성이 떨어지고,

  투명성을 확보하려고 두 가지 역할을 넣게 되면 앞서 말한 이유 때문에 안정성이 떨어진다.

  가이드라인을 따르는 것이 좋긴 하지만, 항상 그 원칙이 우리가 생각하고 있는 디자인에 어떤 영향을 끼칠지를 생각해야 한다.


(2) 복합 반복자란 무엇인가?

- 복합 반복자란 복합 객체 안에 들어있는 구성 요소에 대해 반복작업을 할 수 있게 해 주는 기능을 제공한다.


- 앞선 포스팅에서 등장한 이터레이터 패턴에서 외부 반복자란 녀석에 대해 알아봤었다.

  위의 식당 예시에서는 반복 작업을 내부 반복자를 이용해서 처리했는데,

  구성요소가 MenuItem이 아닌 경우에는 재귀적으로 print()를 호출해서 작업을 처리했었다.


- 하지만 외부 반복자인 복합 반복자는 밖에 있는 클라이언트에서 next(), hasNext()와 같은 기능을 사용해서 원하는 반복작업을

  처리하기 위해 반복작업 중의 현재 위치를 관리해야 한다.


* 스택을 사용해서 외부 반복자인 복합 반복자를 구현하는 예제가 있다.

  근데 이 예제의 작성 분량만 한 포스트를 넘어갈 것 같아서 추후에 포스팅하도록 해야겠다.


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

- 트리 내에서 돌아다니기 편하게 하기 위해서 자식에게 부모 노드에 대한 포인터를 집어넣을 수도 있다.

  자식에 대한 래퍼런스를 지워야 하는 경우에도 반드시 그 부모한테 자식을 지우라고 해야 되는데,

  이러한 경우 부모 래퍼런스를 만들어두면 더 수월하게 처리가 가능하다.


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

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


(5) 캐시를 사용하는 경우는?

- 복합 구조가 너무 복잡하거나, 복합 객체 전체를 한 바퀴 도는 데 너무 많은 자원이 필요한 경우 복합 노드를 캐싱해두면 도움이 된다.

  복합 객체에 있는 모든 자식에게 어떤 계산을 하고, 그 모든 자식들에 대해서 반복작업을 수행해야 한다면 계산 결과를

  임시로 저장하는 캐시를 만들어서 성능을 향상시킬 수 있다.


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

- 클라이언트를 단순화시킬 수 있다는 점이다.

  클라이언트는 복합 객체를 사용하고 있는지 잎 객체를 사용하고 있는지에 대해서 신경 쓰지 않아도 되고,

  올바른 객체에 대해서 올바른 메소드를 호추하고 있는지 확인하기 위해 여기저기 if문을 작성하지 않아도 되고,

  메소드 하나만 호출하면 전체 구조에 대해서 반복해서 작업을 처리할 수 있다.



트리 구조를 구성하여 부분과 전체를 나타내는 계층구조로 표현할 수 있는 컴포지트 패턴에 대한 복습을 마치겠다.



1. 스트래티지 패턴

2. 옵저버 패턴

3. 데코레이터 패턴

4-1. 팩토리 메서드 패턴

4-2. 추상 팩토리 패턴

5. 싱글턴 패턴

6. 커맨드 패턴

7. 어댑터 패턴

8. 퍼사드 패턴

9. 템플릿 메소드 패턴

10. 이터레이터 패턴

11. 컴포지트 패턴