• 컴포지트 패턴(Composite Pattern) :: 마이구미
    디자인 패턴 2019. 3. 10. 17:40
    반응형

    이 글은 디자인 패턴 중 컴포지트 패턴(Composite Pattern) 을 다룬다.

    위키피디아의 내용을 기반으로 정리할 예정이다.

    위키 - https://en.wikipedia.org/wiki/Composite_pattern


    글의 주제를 다루기에 앞서, 글들을 참고하면서, 기억에 남는 문장은 다음과 같다.


    It suggests that design patterns aren’t things we invent. They’re things we discover.


    디자인 패턴은 누군가 발명한 것이 아닌, 발견했다고, 생각하면 조금 더 편하게 바라볼 수 있지 않을까 생각한다.




    컴포지트(Composite) 란?


    OOP 에서 컴포지트(Composite) 는 하나 이상의 유사한 객체를 구성으로 설계된 객체로 모두 유사한 기능을 나타낸다.

    이를 통해 객체 그룹을 조작하는 것처럼, 단일 객체를 조작할 수 있다.

    컴포지트 패턴은 무엇인가?


    컴포지트 패턴은 클라이언트가 복합 객체(group of object) 나 단일 객체를 동일하게 취급하는 것을 목적으로 한다.

    여기서 컴포지트의 의도는 트리 구조로 작성하여, 전체-부분(whole-part) 관계를 표현하는 것이다.


    트리 구조를 다룰 때, 프로그래머는 리프 노드와 브랜치를 구별해야한다.

    여기서 코드는 많은 복잡성을 만들어 많은 에러를 초래한다.

    이를 해결하기 위해, 복잡하고 원시적인 객체를 동일하게 취급하기 위한 인터페이스를 작성할 수 있다.


    결과적으로 컴포지트 패턴은 인터페이스와 본연의 컴포지트의 개념을 활용한다.


    컴포지트 패턴은 언제 사용하는가?


    복합 객체와 단일 객체의 처리 방법이 다르지 않을 경우, 전체-부분 관계로 정의할 수 있다.

    전체-부분 관계의 대표적인 예는 Directory-File 이 존재한다.

    이러한 전체-부분 관계를 효율적으로 정의할 때 유용하다.


    • 전체-부분 관계를 트리 구조로 표현하고 싶을 경우.
    • 전체-부분 관계를 클라이언트에서 부분, 관계 객체를 균일하게 처리하고 싶을 경우.



    컴포지트 패턴 uml


    UML 다이어그램이 뜻하는 의미는 다음과 같다.


    "Client" 클래스는 "Leaf" 와 "Composite" 클래스를 직접 참조하지 않고, 공통 인터페이스 "Component" 를 참조하는 것을 볼 수 있다.

    "Leaf" 클래스는 "Component" 인터페이스를 구현한다.

    "Composite" 클래스는 "Component" 객체 자식들을 유지하고, operation() 과 같은 요청을 통해 자식들에게 전달한다.


    각각을 조금 더 코드 관점에서 보면 다음과 같다.


    Component

    모든 component 들을 위한 추상화된 개념으로써, "Leaf" 와 "Composite" 클래스의 인터페이스이다.


    Leaf

    "Component" 인터페이스를 구현하고, 구체 클래스를 나타낸다.


    Composite

    "Component"  인터페이스를 구현하고, 구현되는 자식(Leaf or Composite) 들을 가지고, 이러한 자식들을 관리하기 위한 메소드(addChild, removeChild...)를 구현한다.

    또한, 일반적으로 인터페이스에 작성된 메소드는 자식에게 위임하는 처리를 한다. 

    * Composite.operation() => Leaf.operation(), 자세한 이해 아래 예제를 통해 할 수 있다.


    이를 기반으로, 객체 다이어그램을 보면 이해하기 쉽다.


    컴포지트 패턴 객체 다이어그램


    "Client" 에서 트리 구조에서의 top-level 에 존재하는 "Composite1" 에 요청을 보낸다.

    그러면 "Component" 인터페이스를 구현한 객체들은 트리 구조를 토대로 위에서 아래 방향으로 모든 자식 요소에게 전달하게 된다.

    이것은 실제로 런타임에서 일어나는 행위라고 보면 된다.


    예제 코드를 접목해보자.

    예제 코드는 원, 삼각형, 사각형 등과 같은 형태의 그래픽을 주제로 한다.


    /** "Component" */
    interface Graphic {
    
        //Prints the graphic.
        public void print();
    }


    "Component" 에 해당하는 인터페이스 "Graphic" 를 나타낸다.

    이를 구현하는 클래스들이 동일한 처리를 하는 메소드로 "무엇인가를 출력하는 행위" 를 작성했다.


    /** "Leaf" */
    class Ellipse implements Graphic {
    
        //Prints the graphic.
        public void print() {
            System.out.println("Ellipse");
        }
    }


    "Leaf" 에 해당하는 구체 클래스 중 하나로 표현할 수 있는 "Ellipse" 라는 클래스를 만들었다.

    그리고 인터페이스에 작성된 메소드인 print() 를 오버라이딩했다.


    /** "Composite" */
    class CompositeGraphic implements Graphic {
    
        //Collection of child graphics.
        private List<Graphic> childGraphics = new ArrayList<Graphic>();
    
        //Prints the graphic.
        public void print() {
            for (Graphic graphic : childGraphics) {
                graphic.print();  //Delegation
            }
        }
    
        //Adds the graphic to the composition.
        public void add(Graphic graphic) {
            childGraphics.add(graphic);
        }
    
        //Removes the graphic from the composition.
        public void remove(Graphic graphic) {
            childGraphics.remove(graphic);
        }
    }


    "Composite" 에 해당하는 클래스로, "Component" 에 해당하는 인터페이스 "Graphic" 를 구현하는 요소들을 관리하기 위한 리스트가 존재한다.

    그리고 이를 위한 추가적인 메소드 add, remove 가 존재하고, 인터페이스에서 작성된 메소드인 print() 를 오버라이딩 해주었다.

    여기서 print() 는 위에서 언급한 "Composite" 가 일반적으로 하는 작성되는 형태로써, 자식들에게 요청을 위임하는 처리를 하게 된다.


    /** Client */ public class Program { public static void main(String[] args) { //Initialize four ellipses Ellipse ellipse1 = new Ellipse(); Ellipse ellipse2 = new Ellipse(); Ellipse ellipse3 = new Ellipse(); Ellipse ellipse4 = new Ellipse(); //Initialize three composite graphics CompositeGraphic graphic = new CompositeGraphic(); CompositeGraphic graphic1 = new CompositeGraphic(); CompositeGraphic graphic2 = new CompositeGraphic(); //Composes the graphics graphic1.add(ellipse1); // children - leaf graphic1.add(ellipse2); // children - leaf graphic1.add(ellipse3); // children - leaf graphic2.add(ellipse4); // children - leaf graphic.add(graphic1); // children - composite graphic.add(graphic2); // children - composite //Prints the complete graphic (Four times the string "Ellipse"). graphic.print(); } }


    "Client" 에서는 "Composite" 는 자식을 관리하기 위한 추가적인 메소드인 add() 를 통해 자식으로 여러 개의 "Leaf" 를 가질 수 있다.

    또한, "Composite" 에 해당하는 또 다른 인스턴스를 자식으로 가질 수 있는 모습을 볼 수 있다.

    결과적으로, 트리 구조가 만들어지면서, print() 와 같이 단일 객체와 복합 객체가 같은 방법으로 처리되는 형태가 만들어진다.


    이것이 일반적으로 알려진 컴포지트 패턴에 대한 이야기이다.


    더 나아가, 2가지 형태의 방식으로 나눌 수 있다.

    컴포지트 패턴에서 "Composite" 클래스는 자식들을 관리하기 위해 추가적인 메소드가 필요하다고 언급했다.

    이러한 메소드들이 어떻게 작성되느냐에 따라, 컴포지트 패턴은 다른 목적을 추구할 수 있다.


    컴포지트 패턴


    지금까지 다룬 방식은 타입의 안정성을 추구하는 방식이다.

    이것은 자식을 다루는 add(), remove() 와 같은 메소드들은 오직 "Composite" 만 정의되었다.

    그로 인해, "Client" 는 "Leaf" 와 "Composite" 를 다르게 취급하고 있다.

    하지만 "Client" 에서 "Leaf" 객체가 자식을 다루는 메소드를 호출할 수 없기 때문에, 타입에 대한 안정성을 얻게 된다.


    Ellipse ellipse = new Ellipse();

    CompositeGraphic graphic = new CompositeGraphic();


    다른 방식으로 일관성을 추구하는 방식은, 자식을 다루는 메소드들을 "Composite" 가 아닌 "Component" 에 정의한다.

    그로 인해, "Client" 는 "Leaf" 와 "Composite" 를 일관되게 취급할 수 있다.

    하지만 "Client" 는 "Leaf" 객체가 자식을 다루는 메소드를 호출할 수 있기 때문에, 타입의 안정성을 잃게 된다.


    Graphic ellipse = new Ellipse();

    Graphic graphic = new CompositeGraphic();


    어떤 방식이 더 좋냐를 따지기에는 너무 많은 것이 고려된다.

    위키에서의 이론은 컴포지트 패턴은 타입의 안정성보다는 일관성을 더 강조한다고 한다.

    반응형

    댓글 5

Designed by Tistory.