Skip to content
2글자 이상 입력하세요
structural으로

Composite

12 min 읽기
structural-patternscompositetree-structuredesign-patterns

Composite

Composite는 객체들을 트리 구조로 구성한 다음, 이 구조들을 개별 객체처럼 다룰 수 있게 하는 구조 디자인 패턴입니다.

Composite pattern

Intent

Composite 패턴은 앱의 핵심 모델이 트리로 표현될 수 있을 때만 의미가 있습니다.

예를 들어, ProductsBoxes 두 가지 유형의 객체가 있다고 가정합시다. Box에는 여러 Products와 더 작은 Boxes가 포함될 수 있습니다. 이런 작은 Boxes에도 Products나 더 작은 Boxes가 들어갈 수 있습니다.

Problem

주문 시스템을 만들고 있다고 가정합시다. 주문에는 포장 없는 단순한 제품과 제품으로 채워진 상자, 그리고 다른 상자들이 포함될 수 있습니다.

주문 시스템 문제

이런 주문의 총 가격을 어떻게 결정할까요?

직접적인 접근 방식을 시도할 수 있습니다: 모든 상자를 열고, 모든 제품을 살펴본 다음 총액을 계산합니다. 하지만 이 접근 방식은 프로그램에서 구현하기 어렵습니다. 모든 클래스와 상자의 중첩 수준을 미리 알아야 합니다.

Solution

Composite 패턴은 총 가격을 계산하는 메서드를 선언하는 공통 인터페이스를 통해 ProductsBoxes를 다루도록 제안합니다.

군대 구조 예시

이 메서드는 어떻게 작동할까요?

상자 안에 중첩된 상자가 있다면, 모든 내부 컴포넌트의 가격이 계산될 때까지 재귀적으로 순회합니다.

실제 예시

이 접근 방식의 가장 큰 이점은 트리를 구성하는 객체의 구체적인 클래스에 대해 신경 쓸 필요가 없다는 것입니다. 객체가 단순한 제품인지 복잡한 상자인지 알 필요 없이, 공통 인터페이스를 통해 동일하게 처리할 수 있습니다.

Real-World Analogy

대부분의 국가에서 군대는 계층 구조로 조직됩니다:

명령은 최상위에서 내려져 각 수준을 통해 모든 병사가 무엇을 해야 하는지 알 때까지 전달됩니다.

Structure

Composite 구조

Composite 구조 상세

구성 요소역할
Component트리의 단순 요소와 복잡한 요소 모두에 대한 공통 작업을 설명하는 인터페이스
Leaf하위 요소가 없는 트리의 기본 요소. 대부분의 실제 작업 수행
Container (Composite)하위 요소를 가진 요소. 자식의 구체적인 클래스를 모르고 컴포넌트 인터페이스를 통해서만 작업
Client컴포넌트 인터페이스를 통해 모든 요소와 작업

Pseudocode

그래픽 에디터에서 도형을 쌓는 예시:

그래픽 에디터 예시

// 컴포넌트 인터페이스
interface Graphic {
move(x: number, y: number): void;
draw(): void;
}
// Leaf: 단순 도형
class Dot implements Graphic {
protected x: number;
protected y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
move(x: number, y: number): void {
this.x += x;
this.y += y;
}
draw(): void {
console.log(`Draw dot at (${this.x}, ${this.y})`);
}
}
// Leaf: 원
class Circle extends Dot {
private radius: number;
constructor(x: number, y: number, radius: number) {
super(x, y);
this.radius = radius;
}
draw(): void {
console.log(`Draw circle at (${this.x}, ${this.y}) with radius ${this.radius}`);
}
}
// Composite: 복합 도형
class CompoundGraphic implements Graphic {
private children: Graphic[] = [];
add(child: Graphic): void {
this.children.push(child);
}
remove(child: Graphic): void {
const index = this.children.indexOf(child);
if (index > -1) {
this.children.splice(index, 1);
}
}
move(x: number, y: number): void {
for (const child of this.children) {
child.move(x, y);
}
}
draw(): void {
console.log("=== CompoundGraphic Start ===");
for (const child of this.children) {
child.draw();
}
console.log("=== CompoundGraphic End ===");
}
}
// 클라이언트 코드
class ImageEditor {
private all: CompoundGraphic;
load(): void {
this.all = new CompoundGraphic();
// 개별 도형 추가
this.all.add(new Dot(1, 2));
this.all.add(new Circle(5, 3, 10));
// 복합 도형 추가
const group = new CompoundGraphic();
group.add(new Dot(10, 10));
group.add(new Circle(20, 20, 5));
this.all.add(group);
}
// 선택된 컴포넌트들을 하나의 복합 컴포넌트로 그룹화
groupSelected(components: Graphic[]): void {
const group = new CompoundGraphic();
for (const component of components) {
group.add(component);
this.all.remove(component);
}
this.all.add(group);
this.all.draw();
}
}
// 사용 예시
const editor = new ImageEditor();
editor.load();

주문 시스템 예시

// 컴포넌트 인터페이스
interface OrderItem {
getPrice(): number;
getDescription(): string;
}
// Leaf: 제품
class Product implements OrderItem {
constructor(
private name: string,
private price: number
) {}
getPrice(): number {
return this.price;
}
getDescription(): string {
return this.name;
}
}
// Composite: 상자
class Box implements OrderItem {
private items: OrderItem[] = [];
constructor(private name: string) {}
add(item: OrderItem): void {
this.items.push(item);
}
remove(item: OrderItem): void {
const index = this.items.indexOf(item);
if (index > -1) {
this.items.splice(index, 1);
}
}
getPrice(): number {
let total = 0;
for (const item of this.items) {
total += item.getPrice();
}
return total;
}
getDescription(): string {
const contents = this.items.map(item => item.getDescription()).join(", ");
return `${this.name} [${contents}]`;
}
}
// 사용 예시
const phone = new Product("Phone", 500);
const charger = new Product("Charger", 25);
const earphones = new Product("Earphones", 50);
const smallBox = new Box("Accessories Box");
smallBox.add(charger);
smallBox.add(earphones);
const bigBox = new Box("Main Box");
bigBox.add(phone);
bigBox.add(smallBox);
console.log(`Order: ${bigBox.getDescription()}`);
console.log(`Total Price: $${bigBox.getPrice()}`);
// Output:
// Order: Main Box [Phone, Accessories Box [Charger, Earphones]]
// Total Price: $575

Applicability

다음의 경우에 패턴을 사용합니다:

트리 구조가 필요할 때

트리와 같은 객체 구조를 구현해야 할 때 사용합니다. Composite 패턴은 공통 인터페이스를 공유하는 두 가지 기본 요소 유형(단순 리프와 복잡한 컨테이너)을 제공합니다.

단순/복잡 요소를 동일하게 처리할 때

클라이언트 코드가 단순 요소와 복잡한 요소를 동일하게 처리하도록 하고 싶을 때 사용합니다. Composite 패턴으로 정의된 모든 요소는 공통 인터페이스를 공유하므로, 클라이언트는 작업하는 객체의 구체적인 클래스에 대해 걱정할 필요가 없습니다.

How to Implement

  1. 트리 표현 가능 확인: 앱의 핵심 모델이 트리 구조로 표현될 수 있는지 확인합니다. 단순 요소와 컨테이너로 분해해 봅니다. 컨테이너는 단순 요소와 다른 컨테이너를 모두 포함할 수 있어야 합니다.

  2. 컴포넌트 인터페이스 선언: 단순 요소와 복잡한 요소 모두에 의미 있는 메서드 목록으로 컴포넌트 인터페이스를 선언합니다.

  3. Leaf 클래스 생성: 단순 요소를 나타내는 leaf 클래스를 만듭니다. 프로그램에는 여러 개의 다른 leaf 클래스가 있을 수 있습니다.

  4. Container 클래스 생성: 복잡한 요소를 나타내는 container 클래스를 만듭니다. 하위 요소에 대한 참조를 저장할 배열 필드를 제공합니다. 배열은 리프와 컨테이너를 모두 저장할 수 있어야 하므로 컴포넌트 인터페이스 타입으로 선언합니다.

  5. 자식 관리 메서드 정의: 컨테이너에 자식을 추가하고 제거하는 메서드를 정의합니다.

Pros and Cons

장점

장점설명
편리한 트리 작업다형성과 재귀를 활용하여 복잡한 트리 구조를 더 편리하게 다룰 수 있습니다
Open/Closed Principle기존 코드를 수정하지 않고 새로운 요소 유형을 도입할 수 있습니다

단점

단점설명
공통 인터페이스 설계기능이 너무 다른 클래스들에 공통 인터페이스를 제공하기 어려울 수 있습니다
과도한 일반화특정 시나리오에서 컴포넌트 인터페이스가 과도하게 일반화될 수 있습니다

Relations with Other Patterns

패턴관계
Builder복잡한 Composite 트리를 생성할 때 Builder를 사용하여 생성 단계를 재귀적으로 실행
Chain of ResponsibilityComposite와 함께 사용할 때, leaf가 요청을 받으면 모든 부모 컴포넌트를 통해 객체 트리의 루트까지 전달
IteratorComposite 트리를 순회할 때 Iterator 사용
Visitor전체 Composite 트리에 대해 작업을 실행할 때 Visitor 사용
FlyweightComposite 트리의 공유 leaf 노드를 Flyweight로 구현하여 RAM 절약
DecoratorComposite와 Decorator는 모두 재귀적 합성을 사용하지만 목적이 다름. Decorator는 책임 추가, Composite는 결과 집계
Prototype복잡한 구조를 재구성하는 대신 Prototype으로 복제 가능

출처: refactoring.guru - Composite


이전 글

Command

다음 글

Flyweight