책정리/GoF 디자인 패턴

11장 부분-전체 관계 형성 및 관리 문제(Composite 패턴)

GONII 2019. 1. 26. 14:35
    객체 하나는 객체 여러 개로 구성된다고 있다. 왜냐하면 객체를 구성하는 데이터 멤버들을 살펴보면 이들 각각이 객체이기 때문이다. 이처럼 객체들간에는 어느 객체가 다른 객체를 구성하기 위한 일부분이 되는 관계가 존재하는데 이를 구성(Composition) 관계 또는 부분-전체(Part-Whole) 관계라고 한다.
    대부분의 객체에서 이러한 구성 관계 또는 부분-전체 관계는 데이터 멤버를 통해 정적으로(Static) 정의된다. 그러나 때로 우리는 객체 하나를 구성하는 요소들을 동적으로 정의할 필요가 있다. 예를 들어 실행 시간에 객체 여러 개를 그룹핑해서 객체 하나로 정의하고자 경우가 여기에 속할 것이다.
    이처럼 객체 하나를 구성하는 여러 객체들을 동적으로 정의해서 사용하고자 어떤 방식으로 클래스를 설계하는 것이 바람직한지를 살펴보도록 하자.
    • 문제 사례 설명
    Visio 그래픽 편집기에서 팔레트에 등록된 기본 도형들은 프로그램 초기에 제공되는 것들이다.
    그런데 만약 어떤 이용자가 기본적으로 제공되는 도형들을 조합해서 새로운 도형을 만들었다고 생각해보자. 도형을 여러 문서에 반복해서 사용하고자 어떻게 해야할까?
    간단한 방법 하나는 원하는 도형을 한쪽 문서에서 복사해서 다른 문서로 붙여넣는 형태일 것이다. 그러나 방식은 복사하고자 하는 도형이 복잡할 경우 도형을 이루고 있는 기본 도형들을 일일이 선택해서 복사하기가 쉽지 않다는 문제가 있다. 또한 문서가 많아졌을 경우 내가 원하는 도형이 어느 문서에 있는지 찾으려면 많은 시간을 소비해야 것이다.
    마이크로소프트 Visio 프로그램에서는 이런 문제를 해결해주기 위해 이용자가 기본 도형을 조합해서 정의한 도형을 팔레트에 등록할 있게 해준다. 이를 통해 우리는 여러 개의 기본 도형으로 만들어진 새로운 도형을 원래부터 프로그램에서 제공하던 기본 도형과 마찬가지 형태로 사용 있게 되는 것이다. 또한 이때 팔레트는 문서와 별도로 관리되기 때문에 문서의 양과는 상관없이 원하는 도형을 팔레트에서 곧바로 찾아 사용할 있는 장점도 있다.
    그렇다면 마이크로소프트 Visio 같은 해결책은 어떻게 가능하겠는가?
    • 다양한 접근 방법 및 COMPOSITE 패턴
    문제에서 해결하기를 원하는 것은 기본 도형을 조합해서 새로운 도형을 정의하고 이를 팔레트에 등록해서 사용할 있게 그래픽 편집기를 설계하는 것이다.
    이때 새로운 도형은 수시로 만들어질 있기 때문에 새로운 도형을 만들 때마다 프로그램을 수정해서는 안되며, 프로그램의 수정 없이 동적으로 모든 것들이 가능해야 한다. 또한 새로 등록된 도형과 처음부터 프로그램에 의해 제공되는 기본 도형은 사용되는 방식이 서로 달라서는 안되며 이용자가 아무런 차이 없이 사용할 있어야 한다.
    본격적인 문제 해결에 들어가기 전에 먼저 용어부터 정리해보자. 우리가 다루는 도형들은 모두 객체로 표현될 것이다. 왜냐하면 우리는 객체지향 개념에 입각해서 모든 문제를 접근하고 있기 때문이다. 그런데 여기서 우리는 프로그램에 의해 처음부터 제공되는 도형과 이용자가 정의한 팔레트에 등록해서 사용하는 도형을 구분해서 지칭하고 있다. 이를 위해 우리는 기본 객체(Primitive Object) 구성 객체(Composed Object)라는 용어를 각각 사용할 것이다. 이때 기본 객체는 프로그램에 의해 처음부터 제공되는 도형에 대한 객체를 가리키고, 구성 객체는 이용자가 도형 여러 개를 조합해서 구성한 도형에 대한 객체를 가리킬 것이다.
    이러한 용어 정의를 바탕으로 문제를 다시 요약하면, 기본 객체들을 임의로 조합해서 만든 구성 객체를 어떻게 생성, 관리할 것인가와 기본 객체와 구성 객체가 어떻게 하면 동일한 형태로 사용될 있는가로 정리할 있을 것이다.
    • 기본적인 방법: 서로 다른 자료형을 활용 방식
    객체를 생성하고 관리하려면 먼저 객체의 자료형인 클래스가 정의되어야 한다. 왜냐하면 클래스는 객체를 생성시켜주기 위한 기본틀이기 때문이다.
    그렇다면 주어진 문제에서 클래스는 어떻게 정의할 있을까?
    기본적으로 생각해볼 있는 방법은 기본 객체들을 위한 클래스와 구성 객체를 위한 클래스를 별도로 정의하는 것이다. 대신 구성 객체들은 자신을 구성하는 기본 객체들에 대한 참조 값들을 관리하기 위해 내부적으로 별도의 데이터 멤버들을 가져야 것이다. 이때 구성 객체가 여러 종류의 기본 객체를 동일한 형태로 참조할 있게 만들어주기 위해서는 모든 기본 객체에 대한 클래스 상위에 부모 클래스를 정의해주어야 것이다. 왜냐하면 구성 객체 입장에서는 이런 부모 클래스가 있어야 어떤 종류의 기본 객체에 대해서도 동일한 자료형으로 참조할 있도록 데이터 멤버를 정의할 있기 때문이다.
    이런 기본적인 생각을 바탕으로 클래스 구조를 그려보면 [그림 11-1] 같은 형태가 것이다.


    우선 [그림 11-1] 클래스 구조를 이용해서 팔레트에 새로운 도형을 등록한다고 해보자. 경우 팔레트를 구현하는 입장에서는 기본 도형들에 대한 객체와 ComposedGraphic 클래스에 대한 객체를 따로 관리해야 한다는 불편함이 생긴다. 왜냐하면 [그림 11-1] 클래스 구조에서는 기본 객체와 구성 객체의 자료형이 서로 완전히 다르기 때문이다.
    [그림 11-1] 클래스 구조가 가지는 논리적인 모델 상의 문제는 ComposedGrahpic 클래스의 객체를 구성하는 것은 모두 기본 객체여야 한다는 점이다. 왜냐하면 ComposedGraphic 클래스가 참조하고 있는 Graphic 클래스의 하위에는 모두 기본 객체에 대한 클래스만 존재하기 때문이다. 다시 말해 ComposedGraphic 객체를 다른 ComposedGraphic 객체의 구성원으로 포함시킬 없다는 것을 의미한다. 이것은 사용자가 이미 정의해놓은 도형들을 조합해서 다른 도형을 정의하는 것이 불가능하고, 새로운 도형을 정의하려면 항상 기본 도형을 가지고 처음부터 다시 만들어야 함을 의미하므로, 사용자들에게 상당한 불편을 것이다.
    • 패턴 활용 방법: COMPOSITE 패턴
    [그림 11-1] 클래스 구조에서 나타난 문제는 크게 가지였다. 하나는 객체가 다른 구성 객체의 일부가 없다는 것이었고, 다른 하나는 클래스를 이용하는 입장에서 기본 객체와 구성 객체의 자료형이 달라 이를 구분해서 사용해야 한다는 것이었다.
    문제의 원인은 기본 객체에 대한 클래스와 구성 객체에 대한 클래스가 완전히 별개로 정의되어 있기 때문이다. 따라서 문제를 해결하기 위해서는 기본 객체와 구성 객체에 대한 클래스들이 동일한 자료형으로 다루어질 있게 만들어주어야 한다. 이를 위한 가장 좋은 방법은 바로 기본 객체에 대한 클래스들과 구성 객체에 대한 클래스가 동일한 상속 구조 하에 놓여지도록 하면 것이다.
    한편 두번째 문제는 기본 객체나 구성 객체 모두 구성 객체의 구성원이 있게 만들어주어야 하는데, 문제를 해결하기 위해서는 기본 객체에 대한 클래스와 구성 객체에 대한 클래스를 공통으로 대표할 있는 클래스를 구성 객체가 참조하게 클래스를 설계를 변경하면 것이다.
    가지 해결책을 감안해서 [그림 11-1] 클래스 구조를 수정하면 [그림 11-2] 같은 클래스 구조를 얻을 있을 것이다.


    한편 [그림 11-2] 클래스 구조를 이용할 경우 구성 객체는 트리 형태의 구조로 만들어질 것이다. 예를 들어 [그림 11-3]에서 aComposedGraphic 구성 객체는 개의 다른 구성 객체와 개의 기본 객체로 구성된 것이며, 이중 개의 구성 객체는 다시 aTriangle aRectangle 객체로 구성된 것이다. 이처럼 구성 객체는 트리 형태의 구조로 만들어지는데, 이때 구성 객체의 요소들을 일일이 찾아다닐 있기 때문에 [그림 11-2]에서 보는 바와 같이 GetChild()라는 인터페이스가 추가로 필요할 것이다.


    이처럼 어떤 객체의 구성원들이 동적으로 결정될 객체를 생성, 관리 있으면서도 기본 객체와 구성 객체를 구분없이 사용할 있고, 기본 객체 뿐만 아니라 구성 객체도 다른 구성 객체의 구성원이 있게 만들어진 [그림 11-2] 같은 클래스 구조를 Composite 패턴이라고 한다.
    • 샘플 코드
    [소스 11-1] 그래픽 편집기에 대한 Composite 패턴 적용 샘플 코드
    #include <iostream>
    #include <list>
    #include <iterator>
    #include <map>
    using namespace std;
     
    class Graphic
    {
    public:
    virtual void Draw() = 0;
    virtual void Add(Graphic* pObj);
    virtual void Remove(Graphic* pObj){}
    virtual Graphic* GetChild(int nth) { return 0; }
    };
     
    class Line : public Graphic
    {
    public:
    void Draw()
    {
    // 라인 그리기
    }
    };
     
    class Triangle : public Graphic
    {
    public:
    void Draw()
    {
    // 삼각형 그리기
    }
    };
     
    class Rectangle : public Graphic
    {
    public:
    void Draw()
    {
    // 사각형 그리기
    }
    };
     
    class ComposedGraphic : public Graphic
    {
    public:
    void Draw()
    {
    list<Graphic*>::iterator iter;
    for (iter = components_.begin(); iter != components_.end(); iter++)
    {
    (*iter)->Draw();
    }
    }
     
    void Add(Graphic* pObj)
    {
    components_.push_front(pObj);
    }
     
    void Remove(Graphic* pObj)
    {
    list<Graphic*>::iterator iter;
    for (iter = components_.begin(); iter != components_.end(); iter++)
    {
    if ((*iter) == pObj)
    {
    components_.erase(iter);
    }
    }
    }
     
    Graphic* GetChild(int nth)
    {
    int i;
    list<Graphic*>::iterator iter;
    for (i = 0, iter = components_.begin(); iter != components_.end(); iter++, i++)
    {
    if (i == nth)
    return *iter;
    }
     
    return 0;
    }
     
    private:
    list<Graphic*> components_;
    };
     
    class Palette
    {
    public:
    Palette()
    {
    Graphic* pGraphic = new Triangle;
    item_[1] = pGraphic;
     
    pGraphic = new Rectangle;
    item_[2] = pGraphic;
     
    // 필요한 만큼 기본 도형 등록
    }
     
    void RegisterNewGraphic(Graphic* pGraphic)
    {
    item_[item_.size() + 1] = pGraphic;
    }
    private:
    map<int, Graphic*> item_;
    };
     
    void main()
    {
    Triangle aTriangle;
    Rectangle aRectangle;
    ComposedGraphic aComposedGraphic2;
     
    aComposedGraphic2.Add(&aTriangle);
    aComposedGraphic2.Add(&aRectangle);
     
    Line aLine;
    Rectangle aRectangle2;
    ComposedGraphic aComposedGraphic;
     
    aComposedGraphic.Add(&aComposedGraphic2);
    aComposedGraphic.Add(&aLine);
    aComposedGraphic.Add(&aRectangle2);
    }
    [소스 11-1]에서 ComposedGraphic 클래스는 Graphic 하위 클래스로 정의되었으며, 내부에는 데이터 멤버로 Graphic 클래스 객체에 대한 포인터 변수를 리스트로 저장, 관리할 있게 하고 있다. 또한 ComposedGraphic 클래스는 동적으로 정의되는 구성원 객체를 추가, 삭제할 있도록 하기 위해 Add(), Remove() 멤버 함수도 제공하고 있다. 밖에 ComposedGraphic 클래스는 외부에서 원할 경우 객체를 구성하는 세부 객체들을 찾아볼 있게 GetChild() 멤버 함수도 제공하고 있다.
    한편 Add(), Remove(), GetChild() 멤버 함ㅅ무는 최상우 클래스인 Graphic 클래스에도 정의되어 있는데, 여기서는 이들 멤버 함수에 대해 아무것도 수행하지 않는 것으로 정의하고 있다. 이것은 ComposedGraphic 클래스에서만 필요로 하는 인터페이스라고 하더라도 Composed Graphic 클래스가 Graphic 하위 클래스로 정의된만큼 외부에서는 ComposedGraphic 클래스 객체인지 아닌지를 구별없이 사용하 ㄹ수 있게 하기 위해 Graphic 클래스에도 이들 인터페이스를 정의해 놓은 것이다. 순가상함수로 정의하지 않은 이유는 그렇게 경우 기본 도형들에 대한 클래스들도 다시 구현해야 하는 불편이 따르기 때문이다.
    • 구현 관련 사항
      1. Composite 패턴을 적용할 경우 구성 객체는 트리 구조를 만들어진다는 것을 [그림11-3]에서 살펴보았다. 그런데 이렇게 트리가 만들어지면 맨처음 떠오르는 문제가 트리 상의 구성요소들을 어떻게 찾아다닐 것인가 하는 것이다. 특히 [그림 11-3]에서처럼 트리를 구성하는 항목들이 자신의 하위에 놓여진 객체에 대한 단방향 포인터만 가지고 있을 경우에는 자신을 참조하고 있는 객체를 찾기가 힘들다는 문제가 존재한다.
    그렇다면 이런 문제를 어떻게 해결할 있을까?
    가장 간단한 방법으로는 트리를 구성하는 객체마다 자신을 참조하는 객체에 대한 역방향 포인터를 가지도록 하는 것이다. , 트리 상의 객체들의 관계가 양방향으로 표현되게 만드는 것이다. 그런데 이렇게 하려면 트리 상의 객체들은 자신을 참조하는 객체에 대한 포인터를 저장하기 위한 데이터 멤버를 별도로 가져야 것이다.
    이런 데이터 멤버는 어떤 클래스에서 정의하는 것이 바람직할까?
    구성 객체를 위한 클래스에서만 데이터 멤버를 정의한다면, 모든 기본 객체들이 자신을 참조하는 객체에 대한 정보를 저장할 없을 것이다. 따라서 데이터 멤버는 Graphic 클래스에 정의하는 것이 바람직할 것이다.
    1. [그림 11-3] 같은 트리상에서 저장 공간을 절약하기 위해 구성 객체 여러 개가 동일한 기본 객체를 공통으로 참조하도록 만들 경우 공유되는 기본 객체의 입장에서는 어떻게 자신을 참조하는 객체에 대한 정보를 관리할 것인가라는 것이다. 물론 역방향 포인터 여러 개를 두고 자신을 참조하는 객체들을 포인터별로 가리키게 수는 있겠지만, 문제는 서로 다른 트리를 형성하는 구성 객체가 동일한 객체를 공유할 경우 역방향 포인터만으로는 자신을 참조하는 객체가 어느 트리에 속하는지를 판별하기 쉽지 않다는 점이다.


    문제를 해결하기 위해서는 객체를 공유하되 공유가 힘든 부분은 공유하지 않도록 분리하는 수밖에 없다. [그림 11-4] 경우 공유가 힘든 부분은 바로 객체가 어떤 트리 구조에 속하는지에 대한 정보다. 따라서 이를 실제 공유 가능한 정보로 분리해서 [그림 11-5] 같이 aRectangleNut 객체와 aRectangleCore객체로 분리한다면 문제를 해결할 있을 것이다. 같은 해결 방법을 Flyweight 패턴이라고 한다.


    1. 기본 객체에 대한 클래스와 구성 객체에 대한 클래스의 공통 부모 클래스로 Graphic 같은 클래스를 정의할 어떤 인터페이스를 포함시켜야 하고, 인터페이스의 구현은 어떻게 것인가다.
    클래스 상속 구조를 설계하는 원칙 하나는 "모든 하위 클래스에게 의미있는 연산만을 상위 클래스의 인터페이스로 정의한다" 것이지만, Composite패턴의 경우에는 구성 객체 클래스에게만 의미 있는 Add(), Remove(), GetChild() 같은 인터페이스도 최상위 클래스인 Graphic 클래스에 포함되고 있다. 이는 기본 객체들을 위한 클래스 입장에서는 불합리하다고 생각될 있는 부분이다.
    그러나 Composite 패턴의 가장 중요한 목적 하나는 Client 입장에서 기본 객체와 구성 객체를 특별한 구분 없이 사용할 있게 만드는 것이다. 따라서 기본 객체들을 위한 클래스 입장에서는 다소 불합리한 인터페이스라도 최상위 클래스의 정의에 포함되어 있다. 물론 이렇게 경우의 단점은 프로그래머가 실수로 기본 객체에 대해 무의미한 인터페이스를 호출해도 컴파일 시에 오류를 찾아내지 못한다는 점이다. 그러나 이러한 단점에도 불구하고 Composite패턴에서는 Client 편의성을 우선하여 고려하는 입장을 견지하고 있다.
    이런 부분이 마음에 든다면 최상위 클래스에는 모든 클래스에게 유용한 인터페이스만 정의하도록 만들 있다. 다만 경우에는 Client입장에서 기본 객체와 구성 객체를 크게 구별하지 않아도 되게 소스코드의 구현에 주의를 기울일 필요가 있다.
    예를 들어 최상위 클래스의 인터페이스로 Add(), Remove(), GetChild() 같은 인터페이스를 정의하지 않았다면, 구성 객체에 대해 이들 인터페이스를 호출해주기 위해서는 Down Casting 수행해야 것이다. 그런데 Down Casting 경우 클래스 상속 구조 상에 새로운 클래스가 추가되면 문제를 일으킬 있다. 예를 들어 [그림 11-2] 클래스 구조에서 ComposedGraphic 클래스 하위에 SpecificComposedGraphic 클래스가 추가되었다고 가정해보자. 그러나 기존의 프로그램은 ComposedGraphic클래스로만 Down Casting하는 것으로 작성되어 있다면 아무리 SpecificComposedGraphic 클래스의 객체가 주어진다하더라도 ComposedGraphic 클래스의 연산만 수행하는 결과를 초래할 것이다.
    Down Casting문제를 해결할 있는 방법을 별도로 강구할 수는 있다. 예를 들어 최상위 클래스와 구성 객체를 위한 클래스에 GetComposite() 같은 인터페이스를 두고, 최상위 클래스에서는 NULL 되돌리고 구성 객체를 위한 클래스에서는 자기 자신을 되돌리게 하면, Client 소스 코드작성 이를 기준으로 현재 사용하고 있는 객체가 기본 객체인지, 구성 객체인지 판별해서 사용이 가능하다. 밖에도 C++ dynamic_cast 사용하면 적절한 DownCasting 수행할 있을 것이다.
    1. 구성 객체가 자신을 구성하는 객체들에 대한 정보를 저장하기 위해 사용하는 자료구조는 여러가지 형태일 있다. 개수가 적으면 리스트 자료구조가 적당할 있고, 개수가 많으면 해쉬테이블 등도 고려해볼만하다.
    또한 객체를 형성하는 객체들간에는 특별한 순서가 있을 있다. 객체들간에 순서가 중요할 경우에는 이들을 접근하고 관리하는 인터페이스를 주의해서 설계해야 한다. 이런 문제를 쉽게 해결할 있는 가지 방법은 iterator패턴을 사용하는 것이다.
    1. 객체 구성 관계 트리에 포함된 객체들 공유되는 객체가 있다면 객체의 소멸 시점도 판단해야 한다. 이에 대한 해결책으로는 Reference Counting 기법을 사용하는 것이 가장 적합할 것이다.
    • COMPOSITE 패턴 정리
    Composite 패턴의 일반적인 클래스 구조를 살펴보면 [그림 11-6] 같다. 여기서 Leaf 클래스와 Composite 클래스는 모두 Component 클래스의 하위 클래스이며, Component 클래스는 추상 클래스지만, Composite 클래스가 가지는 것과 동일한 인터페이스를 정의하고 있음에 유의해야 한다.


    Composite 패턴이 유용하게 활용될 경우
    • 객체들간에 부분-전체(Part-Whole) 관계가 있고 이를 표현하고자 할때
    • 개별 객체와 개별 객체들로 구성된 객체가 공존하는 상황에서 Client 이들을 서로 구분하지 않고 동일한 형태로 모든 객체들을 다루고 싶을
    Composite 패턴의 장단점
    • Client입장에서는 기본 객체와 구성 객체를 특별히 구별하지 않고 소스코드를 작성할 있기 때문에 편리하다. 이는 Composite 패턴의 경우 기본 객체와 구성 객체가 동일한 클래스 상속 구조 내에 정의되어 동일한 자료형으로 표현이 가능하기 때문이다.
    • Composite 패턴은 새로운 클래스의 추가가 용이하다. 왜냐하면 새로운 클래스를 추가하더라도 기존의 Client 소스코드는 변경될 필요가 없기 때문이다. 이는 Composite 패턴에서 최상위 클래스는 하위 클래스들이 필요로 하는 모든 인터페이스를 정의하고 있기 때문에 Client 소스코드가 최상위 클래스 자료형만으로도 작성될 있으므로 얻어지는 장점이다.
    • Composite패턴은 전반적으로 설계를 일반화시킨다. 이런 점은 특정 Component 객체로만 Composite객체를 구성하고 싶을 경우에는 문제가 있다.
    예를 들어 그래픽 편집기에서 새로운 도형을 정의할 삼각형과 사각형만으로 조합할 있고, 직선은 사용하지 못하도록 만들고 싶다면, 실행 시간에 일일이 새로 정의하는 도형에 직선이 포함되지 않은지를 점검하는 수밖에 없다. 왜냐하면 Composite 패턴에서 Component 클래스의 모든 하위 클래스 객체는 동일하게 Composite객체를 구성하기 위한 요소로 사용될 있으며, 특별한 제약을 가하기 힘들기 때문이다.
반응형