반응형

의도

  • 부분과 전체의 계층을 표현하기 위해 객체들을 모아 트리 구조로 구성한다. 사용자로 하여금 개별 객체와 복합 객체를 모두 동일하게 다룰 수 있도록 하는 패턴이다.

 

활용성

  • 부분-전체의 객체 계통을 표현하고 싶을 때
  • 사용자가 객체의 합성으로 생긴 복합 객체와 개개의 객체 사이의 차이를 알지 않고도 자기 일을 할 수 있도록 만들고 싶을 때
    • 사용자는 복합 구조(composite structure)의 모든 객체를 똑같이 취급하게 된다.

 

결과

  • 기본 객체와 복합 객체로 구성된 하나의 일관된 클래스 계통을 정의한다.
    • 일반화된 상위 개념의 객체를 조작하는 방식으로 프로그래밍하면, 런타임 기본 객체와 복합 객체를 구분하지 않고 일관되게 프로그래밍 할 수 있다.
  • 사용자의 코드가 단순해진다.
    • 사용자 코드는 복합 구조이나 단일 객체와 동일하게 다루는 코드로 작성되기 때문이다. 즉, 사용자는 객체의 특성이 복합 구조인지 단일 구조인지 신경안쓰고 개발할 수 있다. 구분이 필요없어지므로 불필요한 함수를 사용하지 않아도된다.
  • 새로운 종류의 구성요소를 쉽게 추가할 수 있다.
    • 복합체의 Composite나 Leaf의 서브클래스들은 기존에 존재하는 구조들과 독립적으로 동작이 가능하다. 새로운 요소가 추가되었다고 해서 사용자의 프로그램이 변경될 필요가 없다.
  • 설계가 지나치게 범용성을 많이 가진다.
    • 새로운 요소를 쉽게 추가할 때의 문제는 복합체의 구성요소에 제약을 가하기 힘들다는 점이다.
    • 복합체가 오직 한 개의 구성요소만 가졌으면 하는 경우에 Composite 클래스 만으로 타입 시스템을 통해 제약을 가할 수 없다. 런타임에 검사해야 한다.

협력 방법

  • 사용자는 복합 구조 내 객체 간의 상호작용을 위해 Component 클래스 인터페이스를 사용한다.
  • 요청받은 대상이 Leaf 인스턴스이면 자신이 정의한 행동을 직접 수행하고, 대상이 Composite이면 자식 객체들에게 요청을 위임한다. 위임하기 전,후에 다른 처리를 수행할 수도 있다.

 

구현 고려사항

  • 포함 객체에 대한 명확한 참조자
    • 자식 구성요소에서 부모를 가리키는 참조자를 관리하면 복합체 구조의 관리를 단순화할 수 있다.
      • 부모에 대한 참조자는 구조를 거슬러 올라가거나 요소를 하나 삭제하는 과정을 단순화시킨다.
      • 예) 복합 구조가 중첩될 때, 복합 구조의 모든 자식들이 또 다시 부모가 되는데 복합 구조에서 추가나 삭제가 일어날 때 구성요소의 부모가 변경된다. 이 경우 부모를 가리키는 참조자를 이용해서 보다 용이하게 관리할 수 있다.
    • 부모 객체에 대한 참조자는 주로 Component 클래스에 둔다. Leaf 클래스와 Composite 클래스는 Component를 상속 받으므로 실제로 두 클래스 모두 이 부모 객체에 대한 참조자를 관리하는 셈이다.
  • 구성요소 공유
    • 메모리 저장 공간의 필요량을 줄일 수 있다. 그러나 구성요소가 하나 이상의 부모를 갖는다면 구성요소를 공유하기 어렵다.
  • Component 인터페이스를 최대화
    • 복합체 패턴의 주요 목표 중 하나는 사용자가 어떤 Leaf나 Composite 클래스가 존재하는지 모르도록 하는 것이다. 이런 목표를 달성하려면, Component 클래스는 Composite와 Leaf에 정의된 모든 공통의 영산을 다 정의하고 있어야 한다.
      • Component 클래스는 이들 연산에 대한 기본 구현을 제공하고 Leaf와 Composite 클래스가 이를 재정의한다.
    • 문제는 Component 클래스에서는 서브클래스인 Leaf  클래스가 정의하지 않는 연산도 정의할 수 있다.
      • Composite 클래스에만 의미있는 연산도 Component 클래스에 정의해야 한다.
      • 자식들에 접근하는 인터페이스는 Composite에 필수적이지만, Leaf 클래스에는 의미가 없다.
        • 이는 Leaf는 Conponent 클래스의 서브클래스이나 자식을 갖지 않는다고 볼 수 있다. 이러한 점을 이용하여 Conponent 클래스의 자식을 처리하는 연산의 기본 구현 사항으로 아무것도 반환하지 않도록 기본 구현을 만들고 Leaf 클래스는 이 구현을 그대로 사용하고 Composite 클래스는 자식을 반환하도록 재정의하여 해결 할 수도있다.
  • 자식을 관리하는 연산 선언
    • 안전성과 투명성 사이의 양자택일 문제가 발생한다.
    • 자식을 관리하는 인터페이스를 클래스 계통의 최상위 클래스에 정의
      • 서브클래스 모두에게 동일한 인터페이스가 유지되어 이를 사용하는 사용자에게 인터페이스의 투명성을 부여할 수 있다.
      • 그러나 사용자가 Leaf 클래스의 인스턴스에게 Add()나 Remove() 연산을 호출하는 의미 없는 행동을 하지 않도록 안전성 유지를 위한 비용을 지불해야 한다.
        • Leaf 클래스에서 자식을 관리하는 연산을 호출할 때는 오류 처리되어야 한다.
    • 자식을 관리하는 인터페이스를 Composite 클래스에만 자식을 관리하는 연산을 정의
      • Leaf 클래스의 인스턴스에 Add()나 Remove()와 같은 연산을 요청하지 않을 것이므로 안전성은 보장할 수 있다.
      • 그러나 Leaf 클래스와 Composite 클래스가 서로 다른 인터페이스를 갖게 되므로 사용자는 이를 동일한 대상으로 간주하고 사용할 수 없다. 즉 투명성을 부여할 수 없다.
      • 안전성을 선택한 경우, 처리하는 대상 객체가 어떤 클래스에서 만들어진 것인지에 대한 타입 정보를 잃어버릴 수도 있으며 Component를 Composite로 변환해야 할 수도 있다.
        • 타입 안전성 없는 캐스트를 쓰지 않는 방법
          • Component 클래스에 자기 자신을 반환하는 연산 추가한다.
            • 기본 구현으로 null 을 반환하고 Composite 클래스에서는 자기 자신을 반환하도록 재정의한다.
            • 해당 연산이 Composite 클래스를 반환하면 자식을 관리하는 연산(Add, Remove)를 수행할 수 있도록 한다.
          • 안전성을 확보하여도 연산을 처리할 수 있는 대상인지를 일일이 다 검사해야 하는 문제가 있다. 이는 확장성이라는 측면에서 바람직하지 않다.
  • Component에 Component의 자식을 인스턴스 변수로 관리하는것은 바람직하지 않다.
    • Component 클래스에는 자식에 접근하고 관리하는 연산이 정의되어 있다.
    • 그러나 인스턴스 변수로 관리하면 자식이 없는 Leaf 클래스에도 자식을 관리하기 위해 메모리를 정의해야하는 문제가 발생한다.
  • 자식 사이의 순서 정하기
    • 자식 간의 순서가 의미 있고 문제가 될 때는 자식에게 접근, 관리하는 인터페이스를 설계할 때 자식들의 순서를 관리할 수 있도록 주의를 기울여야한다.
      • 반복자 패턴이 도움을 줄 수 있다.
  • 성능 개선을 위한 캐싱(caching)
    • 복합 구조 내부를 수시로 순회하고 탐색해야 한다면, Composite 클래스는 자식을 순회하는 정보를 미리 담고 있을 수도 있다.
      • Composite 클래스가 탐색이나 최단 경로 순회의 실제 결과를 임시로 저장
    • 구성요소가 변경되면 부모가 캐싱하는 정보는 의미가 없어진다. 따라서 구성요소가 자신의 부모가 누구인지 아는 상황에서만 의미가 있다.
    • 캐싱을 이용하려면, 현재 저장된 캐시의 내용이 유효한지 아닌지를 확인하는 연산을 정의해야 한다.
  • 구성요소 삭제 책임자
    • 가비지 컬렉션(garbage collection)의 기능을 제공하지 않는 언어에서는 자식이 없어질 때 Composite 클래스가 보통 삭제의 책임을 진다. 그러나 Leaf 객체가 변경될 수 없는 객체이거나 공유될 수 있는 객체라면 예외적으로 삭제할 수 없다.
  • 구성요소 저장 시 적합한 데이터 구조
    • 연결리스트, 배열, 트리, 해시 테이블 등 모두가 구현대상이다.
    • 어느 것이 더 효율적인지에 따라서 선택하여 구현한다.
    • 일반적인 데이터 구조를 사용하지 않고 각 자식마다 모두 각각의 변수를 정의하여 사용할 수도 있다.
      • Composite를 상속하는 서브클래스 각각에 자신이 정의한 자식 관리 방법에 따라 필요한 구현을 별도로 정의해야 한다.
        • 해석자 패턴이 도움을 줄 수 있다.

 

구조

  • 전형적인 Composite 객체 구조

실제 구현 구조 - 자식을 관리하는 인터페이스를 클래스 계통의 최상위 클래스에 정의

소스코드 - 자식을 관리하는 인터페이스를 클래스 계통의 최상위 클래스에 정의

//Component
public abstract class Graphic {
    public abstract void draw();
    public Graphic getChild(int i) {
        return null;
    };
    public abstract void add(Graphic graphic);
    public abstract void remove(Graphic graphic);
}
//Leaf
public class Line extends Graphic {
    @Override
    public void draw() {
        System.out.println("draw Line");
    }

    @Override
    public void add(Graphic graphic) {
        System.out.println("can't add");
    }

    @Override
    public void remove(Graphic graphic) {
        System.out.println("can't remove");
    }
}
//Leaf
public class Text extends Graphic {
    @Override
    public void draw() {
        System.out.println("draw Text");
    }

    @Override
    public void add(Graphic graphic) {
        System.out.println("can't add");
    }

    @Override
    public void remove(Graphic graphic) {
        System.out.println("can't remove");
    }
}
//Leaf
public class Rectangle extends Graphic {
    @Override
    public void draw() {
        System.out.println("draw Rectangle");
    }

    @Override
    public void add(Graphic graphic) {
        System.out.println("can't add");
    }

    @Override
    public void remove(Graphic graphic) {
        System.out.println("can't remove");
    }
}
//Composite
public class Picture extends Graphic {
    //복합 구성요소 관리
    private final List<Graphic> graphicList;

    public Picture() {
        graphicList = new ArrayList<>();
    }

    @Override
    public void draw() {
        System.out.println("composite draw start");
        for (Graphic graphic : this.graphicList) {
            graphic.draw();
        }
        System.out.println("composite draw end");
    }

    @Override
    public Graphic getChild(int i) {
        if(this.graphicList.size() == 0){
            throw new IndexOutOfBoundsException("Composite에서 자식을 삭제하면 자식은 Composite의 부모를 가리키는 참조자에서 삭제된다.");
        }
        return this.graphicList.get(i);
    }

    @Override
    public void add(Graphic graphic) {
        this.graphicList.add(graphic);
    }

    @Override
    public void remove(Graphic graphic) {
        System.out.println("remove " + this.graphicList.remove(graphic));
    }
}
public class Main {
    public static void main(String[] args) {
        Graphic line = new Line();
        Graphic text = new Text();
        Graphic rectangle = new Rectangle();
        Graphic parentPicture = new Picture();
        Graphic childPicture = new Picture();

        /* Leaf 클래스는 자식과 관련된 연산 미동작 */
        line.draw();
        line.add(text);
        System.out.println(line.getChild(0));
        line.remove(text);
        System.out.println("-----------------------");
        text.draw();
        text.add(line);
        System.out.println(text.getChild(0));
        text.remove(line);
        System.out.println("-----------------------");
        rectangle.draw();
        rectangle.add(text);
        System.out.println(rectangle.getChild(0));
        rectangle.remove(text);
        System.out.println("-----------------------");
        /* //Leaf 클래스는 자식과 관련된 연산 미동작 */

        /* 복합 구성요소는 자식과 관련된 연산 동작 */
        childPicture.add(line);
        childPicture.add(text);
        childPicture.add(rectangle);
        childPicture.draw();
        childPicture.remove(line);
        System.out.println(childPicture.getChild(0));
        childPicture.draw();
        System.out.println("-----------------------");
        parentPicture.add(new Text());
        parentPicture.add(new Text());
        parentPicture.add(new Text());
        parentPicture.add(childPicture);
        parentPicture.add(new Rectangle());
        parentPicture.draw();
        System.out.println("-----------------------");
        System.out.println(parentPicture.getChild(0));
        System.out.println(parentPicture.getChild(1));
        System.out.println(parentPicture.getChild(2));
        parentPicture.remove(parentPicture.getChild(0));
        System.out.println("-----------------------");
        parentPicture.draw();
        System.out.println("-----------------------");
        System.out.println(parentPicture.getChild(0));
        System.out.println(parentPicture.getChild(1));
        System.out.println(parentPicture.getChild(2));
        /* //복합 구성요소는 자식과 관련된 연산 동작 */
    }
}

결과

draw Line
can't add
null
can't remove
-----------------------
draw Text
can't add
null
can't remove
-----------------------
draw Rectangle
can't add
null
can't remove
-----------------------
composite draw start
draw Line
draw Text
draw Rectangle
composite draw end
remove true
composite.Text@3d24753a
composite draw start
draw Text
draw Rectangle
composite draw end
-----------------------
composite draw start
draw Text
draw Text
draw Text
composite draw start
draw Text
draw Rectangle
composite draw end
draw Rectangle
composite draw end
-----------------------
composite.Text@59a6e353
composite.Text@7a0ac6e3
composite.Text@71be98f5
remove true
-----------------------
composite draw start
draw Text
draw Text
composite draw start
draw Text
draw Rectangle
composite draw end
draw Rectangle
composite draw end
-----------------------
composite.Text@7a0ac6e3
composite.Text@71be98f5
composite.Picture@6fadae5d

관련 패턴

  • 구성요소-부모 간의 연결은 책임 연쇄 패턴에서 많이 사용되는 예이다.
  • 장식자 패턴은 자주 복합체 패턴과 함께 사용된다.
    • 두 패턴이 함께 사용될 때는 둘 다 동일한 하나의 부모 클래스를 상속받는다.
    • 장식자는 Component의 인터페이스를 지원해야 한다.
  • 플라이급 패턴으로 구성요소의 공유 방법을 얻을 수 있다.
    • 그러나 공유되는 구성요소의 부모는 참조할 수 없다.
  • 반복자 패턴을 이용하면, 구성요소를 순회하는 방법을 얻을 수 있다.
  • 방문자 패턴을 이용하면, 이 패턴을 사용하지 않을 때 Composite와 Leaf 클래스에 걸쳐 분산될 수 있는 행동을 국소화시킬 수 있다.

 


[참고자료]

리처드 헬름, 랄프 존슨, 존 블리시디스, 『GoF의 디자인 패턴 : 재사용성을 지닌 객체지향 소프트웨어의 핵심요소』, 김정아 번역, 프로텍미디어(2015)

http://www.cs.unc.edu/~stotts/GOF/hires/pat4cfso.htm

반응형

+ Recent posts