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