[DesignPattern] Composite Pattern

suhan0304·2024년 10월 30일

Design Pattern

목록 보기
16/16
post-thumbnail

Composite Pattern

복합체 패턴은 복합 객체단일 객체를 동일한 컴포넌트로 취급하여, 클라이언트에게 이 둘을 구분하지 않고 동일한 인터페이스를 사용하도록 하는 구조 패턴이다. 복합체 패턴은 전체-부분의 관계를 갖는 객체들 사이의 관계를 트리 계층 구조로 정의해야 할 때 유용하다. 윈도우나 리눅스의 파일 시스템 구조를 떠올려보면 쉽게 이해할 수 있다.
폴더 안에는 파일이 있을 수 있고 폴더도 들어있을 수 있다. 이를 복합적으로 담을 수 있다 해서 Composite 객체라고 불린다. 반면 파일은 단일 객체이기 때문에 이를 Leaf 객체라고 불린다. 즉, Leaf는 자식이 없다.

복합체 패턴은 바로 이 폴더와 파일을 동일한 타입으로 취급하여 구현을 단순화 시키는 것이 목적이다. 폴더 안에는 파일 뿐만 아니라 서브 폴더가 올 수 있고 또 서브 폴더 안에 서브 폴더가 오고 이런 식으로 계층 구조를 구현하다 보면, 자칫 복잡해 질 수 있는 복합 객체를 재귀 동작을 통해 하위 객체들에게 작업을 위임한다. 그러면 복합 객체와 단일 객체를 대상으로 똑같은 작업을 적용할 수 있어 단일 / 복합 객체를 구분할 필요가 거의 없어진다.

정리하자면, Composite 패턴은 그릇과 내용물을 동일시해서 재귀적인 구조를 만들기 위한 디자인 패턴이라 할 수 있다.


Structrue

  • Component : Leaf와 Compsite 를 묶는 공통적인 상위 인터페이스
  • Composite : 복합 객체로서, Leaf 역할이나 Composite 역할을 넣어 관리하는 역할을 한다.  
    - Component 구현체들을 내부 리스트로 관리한다
    - add 와 remove 메소드는 내부 리스트에 단일 / 복합 객체를 저장
    - Component 인터페이스의 구현 메서드인 operation은 복합 객체에서 호출되면 재귀 하여, 추가 단일 객체를 저장한 하위 복합 객체를 순회하게 된다.
  • Leaf : 단일 객체로서, 단순하게 내용물을 표시하는 역할을 한다.
    - Component 인터페이스의 구현 메서드인 operation은 단일 객체에서 호출되면 적절한 값만 반환한다
  • Client : 클라이언트는 Component를 참조하여 단일 / 복합 객체를 하나의 객체로서 다룬다.

How

interface Component {
    void operation();
}
class Leaf implements Component {

    @Override
    public void operation() {
        System.out.println(this + " 호출");
    }
}
class Composite implements Component {

    // Leaf 와 Composite 객체 모두를 저장하여 관리하는 내부 리스트
    List<Component> components = new ArrayList<>();

    public void add(Component c) {
        components.add(c); // 리스트 추가
    }

    public void remove(Component c) {
        components.remove(c); // 리스트 삭제
    }

    @Override
    public void operation() {
        System.out.println(this + " 호출");
        
        // 내부 리스트를 순회하여, 단일 Leaf이면 값을 출력하고,
        // 또다른 서브 복합 객체이면, 다시 그 내부를 순회하는 재귀 함수 동작이 된다.
        for (Component component : components) {
            component.operation(); // 자기 자신을 호출(재귀)
        }
    }
    
    public List<Component> getChild() {
        return components;
    }
}
class Client {
    public static void main(String[] args) {
        // 1. 최상위 복합체 생성
        Composite composite1 = new Composite();

        // 2. 최상위 복합체에 저장할 Leaf와 또다른 서브 복합체 생성
        Leaf leaf1 = new Leaf();
        Composite composite2 = new Composite();

        // 3. 최상위 복합체에 개체들을 등록
        composite1.add(leaf1);
        composite1.add(composite2);

        // 4. 서브 복합체에 저장할 Leaf 생성
        Leaf leaf2 = new Leaf();
        Leaf leaf3 = new Leaf();
        Leaf leaf4 = new Leaf();

        // 5. 서브 복합체에 개체들을 등록
        composite2.add(leaf2);
        composite2.add(leaf3);
        composite2.add(leaf4);

        // 6. 최상위 복합체의 모든 자식 노드들을 출력
        composite1.operation();
    }
}

operation 메서드를 호출하게 되면, 단일체일 경우 값이 호출 되고, 복합체일 경우 자기 자신을 호출하는 재귀 함수에 의해 저장하고 있는 하위 Leaf 객체들을 순회하여 호출하게 된다.


When

  • 데이터를 다룰때 계층적 트리 표현을 다루어야 할때
  • 복잡하고 난해한 단일 / 복합 객체 관계를 간편히 단순화하여 균일하게 처리하고 싶을때

Then

  • 단일체와 복합체를 동일하게 여기기 때문에 묶어서 연산하거나 관리할 때 편리하다.
  • 다형성 재귀를 통해 복잡한 트리 구조를 보다 편리하게 구성 할 수 있다. 
  • 수평적, 수직적 모든 방향으로 객체를 확장할 수 있다.
  • 새로운 Leaf 클래스를 추가하더라도 클라이언트는 추상화된 인터페이스 만을 바라보기 때문에 개방 폐쇄 원칙(OCP)Visit Website을 준수 한다. (단일 부분의 확장이 용이)

But

  • 재귀 호출 특징 상 트리의 깊이(depth)가 깊어질 수록 디버깅에 어려움이 생긴다.
  • 설계가 지나치게 범용성을 갖기 때문에 새로운 요소를 추가할 때 복합 객체에서 구성 요소에 제약을 갖기 힘들다.
  • 예를들어, 계층형 구조에서 leaf 객체와 composite 객체들을 모두 동일한 인터페이스로 다루어야하는데, 이 공통 인터페이스 설계가 까다로울 수 있다.
    - 복합 객체가 가지는 부분 객체의 종류를 제한할 필요가 있을 때
    - 수평적 방향으로만 확장이 가능하도록 Leaf를 제한하는 Composite를 만들때

Example

복합 객체와 단일 객체를 상자와 아이템으로 비유해보자.

item 클래스와 이를 담든 Bag 클래스가 있다고 하자. 가방 안에 아이템을 담는 형식이니 Item 클래스는 Leaf가 되고 Bag 클래스는 Compostie가 된다.

우리가 구현하고 싶은 것은 Bag 속 리스트에 담긴 Item 객체들의 가격(price) 값을 추출하고 싶다고 한다. 그런데 단순히 가방 안에 아이템들이 들어있을 뿐 아니라 복수의 아이템을 담은 또다른 가방이 여러 개 들어있을 수 있다. 이러한 계층 트리 구조를 컴포지트 패턴으로 클래스를 구성을 하면 아래와 같이 되게 된다.

  • Composite와 Leaf 객체를 공용으로 묶는 ItemComponent 인터페이스를 정의하고, Composite와 Leaf 객체를 동시에 쓰이는 추상 메서드를 정의한다.
  • Composite 객체인 Bag 클래스에서 ItemComponent 타입의 공용 아이템을 담는 내부 리스트를 정의한다.
  • Component 인터페이스의 공통적인 operation인 getPrice() 메서드는 Item일 경우 그대로 반환하고, Bag일 경우 자기 자신을 호출하여 가방에 있어있는 아이템을 순회하는 재귀 동작을 실행한다.
// Component 인터페이스
interface ItemComponent {
    int getPrice();
    string getName();
}
// Composite 객체
class Bag : ItemComponent {
    // 아이템과 가방을 모두 저장하기 위해 인터페이스 타입 리스트로 관리
    List<ItemComponent> components = new List<ItemComponent>();

    string name;

    public Bag(string name) {
        this.name = name;
    }

    // 리스트에 아이템 & 가방 추가
    public void add(ItemComponent item) {
        components.Add(item);
    }

    // 현재 가방 내용물 반환
    public List<ItemComponent> getComponents() {
        return components;
    }

    public int getPrice() {
        int sum = 0;

        foreach (ItemComponent component in components) {
            // 요소가 Bag이면 알아서 '재귀함수' 동작, item이면 값을 반환 받음
            sum += component.getPrice();
        }

        return sum;
    }

    public string getName() {
        return name;
    }
}
class Item_Composite : ItemComponent {
    string name;
    int price;

    public Item_Composite(string name, int price) {
        this.name = name;
        this.price = price;
    }

    public int getPrice() {
        return price;
    }

    public string getName() {
        return name;
    }
}
public class Composite : MonoBehaviour {
    public void Start() {
        // 1. 메인 가방 인스턴스 생성
        Bag bag_main = new Bag("메인 가방");

        // 2. 아이템 인스턴스 생성
        Item_Composite armor = new Item_Composite("갑옷", 250);
        Item_Composite sword = new Item_Composite("장검", 500);

        // 3. 메인 가방에는 모험에 필요한 무구 아이템만을 추가
        bag_main.add(armor);
        bag_main.add(sword);

        // 4. 서브 가방 인스턴스 생성
        Bag bag_food = new Bag("음식 가방");

        // 5. 아이템 인스턴스 생성
        Item_Composite apple = new Item_Composite("사과", 290);
        Item_Composite Banana = new Item_Composite("바나나", 160);

        // 6. 서브 가방에 음식 추가
        bag_food.add(apple);
        bag_food.add(Banana);

        // 7. 메인 가방에 서브 가방 추가
        bag_main.add(bag_food);

        //----------------------------------------------------//

        // 가방 안에 있는 모든 아이템의 값어치 출력 ( 서브 가방에 있는 물건의 값어치 포함 )
        printPrice(bag_main);

        // 가방 안에 있는 모든 아이템의 값어치 출력
        printPrice(bag_food);

    }    
    public void printPrice(ItemComponent bag) {
        int result = bag.getPrice();
        Debug.Log(bag.getName() + "의 아이템 총합 : " + result + " 골드");
    }
}

ItemComponent 만을 사용해서 price를 출력하기 때문에 Item, Bag 구현체 상관 없이 구현 메서드만 호출하면 내부에서 정의된 구현에 따라 원하는 값을 얻을 수 있게 된다.

만일 패턴을 사용하지 않고 계층 트리 구조를 선회하기 위해서는 매우 하드한 코딩을 해야 할지도 모른다.

profile
Be Honest, Be Harder, Be Stronger

0개의 댓글