[Structural Pattern] 복합체 패턴 (Composite Pattern)
Design Pattern / Structural Pattern
복합체 패턴의 정의와 해당 디자인 패턴의 예제 코드를 통한 이해 및 설명 정리
Composite Pattern
개념
- 복합 객체( - Composite)와 단일 객체(- Leaf)를 동일한 컴포넌트로 취급하여, 클라이언트에게 이 둘을 구분하지 않고 동일한 인터페이스를 사용하도록 하는 구조 패턴- 정리하자면, 복합체 패턴은 그릇과 내용물을 동일시해서 재귀적인 구조를 만들기 위한 디자인 패턴이라고 말할 수 있음
 
- 복합체 패턴은 전체-부분의 관계를 갖는 객체들 사이의 관계를 트리 계층 구조로 정의해야 할 때 유용함 
패턴 구조
- Component- Leaf와- Composite를 묶는 공통적인 상위 인터페이스
 
- Composite- 복합 객체로서, - Leaf역할이나- Composite역할을 넣어 관리하는 역할
- Component구현체들을 내부 리스트로 관리함
- add와- remove메소드는 내부 리스트에 단일 / 복합 객체를 저장
- Component인터페이스의 구현 메서드인- operation은 복합 객체에서 호출되면 재귀하여, 추가 단일 객체를 저장한 하위 복합 객체를 순회하게 됨
 
- Leaf- 단일 객체로서 단순하게 내용물을 표시하는 역할 
- Component인터페이스의 구현 메서드인- operation은 단일 객체에서 호출되면 적절한 값만 반환
 
- Client- 클라이언트는 Component를 참조하여 단일 / 복합 객체를 하나의 객체로서 다룸
 
- 클라이언트는 
예제 코드
폴더 구조 예제
1
2
3
4
5
6
7
8
9
10
11
/** Component 인터페이스 */
interface Node {
  // 계층 트리 출력
  // print(): void;
  print(str: string): void;
  // 파일/폴더 용량 얻기
  getSize(): number;
}
export { Node };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import { Node } from "./component";
/** Composite 객체 */
class Folder implements Node {
  private name: string; // 폴더 이름
  private list: Node[];
  constructor(name: string) {
    this.name = name;
    this.list = [];
  }
  // 리스트에 폴더, 파일 추가
  public add(node: Node): void {
    this.list.push(node);
  }
  public print(str: string): void {
    const size = this.getSize(); // 폴더가 담고 있는 모든 파일에 대한 용량 합산
    console.log(`${str} 📂 ${this.name} (${size}KB)`);
    for (const node of this.list) {
      // Folder 일 경우 재귀 동작
      node.print(str + "   "); // 인자로 공백문자를 할당하여 indent 처리
    }
  }
  // 각 파일의 용량(KB) 구하기
  public getSize(): number {
    let sum = 0;
    for (const node of this.list) {
      sum += node.getSize(); // print 로직과 똑같이 재귀 동작
    }
    return sum;
  }
}
export { Folder };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Node } from "./component";
/** Leaf 객체 */
class File implements Node {
  private name: string; // 파일 이름
  private size: number; // 파일 사이즈
  constructor(name: string, size: number) {
    this.name = name;
    this.size = size;
  }
  public print(str: string): void {
    console.log(`${str} 📄 ${this.name} (${this.size}KB)`);
  }
  public getSize(): number {
    return this.size;
  }
}
export { File };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { Folder } from "./composite";
import { File } from "./leaf";
class Client {
  public static main(_args?: []): void {
    const root: Folder = new Folder("root");
    const file_1: File = new File("file_1", 10);
    const sub_1: Folder = new Folder("sub_1");
    const sub_2: Folder = new Folder("sub_2");
    root.add(sub_1);
    root.add(file_1);
    root.add(sub_2);
    const file_1_1: File = new File("file_1_1", 10);
    const file_1_2: File = new File("file_1_2", 10);
    sub_1.add(file_1_1);
    sub_1.add(file_1_2);
    const file_2_1: File = new File("file_2_1", 10);
    sub_2.add(file_2_1);
    // 전체 dir 출력
    root.print("");
  }
}
Client.main();
// 📂 root (40KB)
//    📂 sub_1 (20KB)
//       📄 file_1_1 (10KB)
//       📄 file_1_2 (10KB)
//    📄 file_1 (10KB)
//    📂 sub_2 (10KB)
//       📄 file_2_1 (10KB)
아이템 가방 예제
1
2
3
4
5
6
7
/** Component 인터페이스 */
interface IItemComponent {
  getPrice(): number;
  getName(): string;
}
export { IItemComponent };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { IItemComponent } from "./component";
/** Composite 객체 */
class Bag implements IItemComponent {
  // 아이템들과 서브 가방 모두를 저장하기 위해 인터페이스 타입 리스트로 관리
  components: IItemComponent[] = new Array<IItemComponent>();
  name: string; // 가방 이름
  constructor(name: string) {
    this.name = name;
  }
  // 리스트에 아이템 & 가방 추가
  public add(item: IItemComponent): void {
    this.components.push(item);
  }
  // 현재 가방의 내용물을 반환
  public getComponents(): IItemComponent[] {
    return this.components;
  }
  public getPrice(): number {
    let sum: number = 0;
    for (const component of this.components) {
      // 만일 리스트에서 가져온 요소가 Item이면 정수값을 받고, Bag이면 "재귀 함수" 동작 ⭐️
      sum += component.getPrice(); // 자기 자신 호출 (재귀)
    }
    return sum;
  }
  public getName(): string {
    return this.name;
  }
}
export { Bag };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { IItemComponent } from "./component";
/** Leaf 객체 */
class Item implements IItemComponent {
  name: string; // 아이템 이름
  price: number; // 아이템 가격
  constructor(name: string, price: number) {
    this.name = name;
    this.price = price;
  }
  public getPrice(): number {
    return this.price;
  }
  public getName(): string {
    return this.name;
  }
}
export { Item };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import { Bag } from "./composite";
import { Item } from "./leaf";
class Client {
  public static main(_args?: string[]): void {
    // 1. 장비 가방 인스턴스 생성
    const bag_equipment: Bag = new Bag("장비 가방");
    // 2. 아이템 인스턴스 생성
    const armor: Item = new Item("갑옷", 250);
    const sword: Item = new Item("장검", 500);
    // 3. 장비 가방에는 모험에 필요한 장비 아이템만을 추가
    bag_equipment.add(armor);
    bag_equipment.add(sword);
    // 4. 소모품 가방 인스턴스 생성
    const bag_food: Bag = new Bag("소모품 가방");
    // 5. 아이템 인스턴스 생성
    const apple: Item = new Item("사과", 400);
    const banana: Item = new Item("바나나", 130);
    // 6. 소모품 가방에는 음식 아이템만을 추가
    bag_food.add(apple);
    bag_food.add(banana);
    // 7. 장비 가방과 소모품 가방을 담을 인벤토리 가방 인스턴스 생성
    const bag_inventory: Bag = new Bag("인벤토리");
    // 8. 장비 가방과 소모품 가방을 인벤토리 가방에 넣음
    bag_inventory.add(bag_equipment);
    bag_inventory.add(bag_food);
    // ----------------------------------------------------------------- //
    const client: Client = new Client();
    // 장비 가방의 아이템의 가격의 총합
    client.printPrice(bag_equipment);
    // 소모품 가방의 아이템의 가격의 총합
    client.printPrice(bag_food);
    // 장비 가방의 가격 총합 + 소모품 가방의 가격 총합
    client.printPrice(bag_inventory);
  }
  public printPrice(bag: Bag): void {
    const result: number = bag.getPrice();
    console.log(`${bag.getName()}의 아이템 총합 : ${result}골드`);
  }
}
Client.main();
// 장비 가방의 아이템 총합 : 750골드
// 소모품 가방의 아이템 총합 : 530골드
// 인벤토리의 아이템 총합 : 1280골드
참고한 출처 사이트
 이 기사는 저작권자의  CC BY 4.0  라이센스를 따릅니다.


