Typescript로 다시 쓰는 GoF - Composite

아홉번째태양·2023년 9월 2일
0

Composite이란?

Composite이란 사전적으로 혼합물이라는 뜻이다. 언뜻 다른 패턴들과는 다르게 이름만으로 직관적으로 그 역할을 유추하기가 다소 쉽지는 않지만, 이름 그대로 서로 다른 무엇인가를 혼합하여 하나인것처럼 취급하는 패턴이다. 보통 내용물과 그릇, 혹은 단수와 복수를 하나로 동일시한다고 이야기하는데 예시를 살펴보면 이해가 보다 빠르다.


언제쓸까?

Composite 패턴은 어떤 위계질서를 트리 형태로 나타낼 수 있는 상황에서 쓰이며 일상적으로 굉장히 자주 등장하는 패턴이다.

예를들어,

  • 파일 시스템: 파일과 디렉토리는 서로 다른 객체지만, 둘 다 디렉토리라는 그릇 안에 집어넣을 수 있다.
  • 조직 구조도: 개개인이 팀을 이루고, 이 팀은 다시 다른 팀의 한 구성원으로 포함될 수 있다.
  • 메뉴 시스템: 각각의 재료들이 단품을 구성하고 단품은 다시 메뉴를 구성한다. 그리고 이들은 모두 가격 열량 등 공통된 속성이나 인터페이스를 가질 수 있다.


Composite 구현

Composite 패턴을 이용해 파일 시스템을 간단하게 구현해보자.

Composite 패턴에는 다음의 객체들이 필요하다.

  1. Leaf 잎
    내용물, 혹은 단수의 객체를 나타낸다. 이 안에는 다른 객체를 넣을 수 없다.
  2. Composite 복합체
    그릇, 혹은 복수의 집합체를 나타낸다. 이 안에는 다른 Leaf와 Composite이 들어갈 수 있다.
  3. Component 컴포넌트
    Leaf와 Composite을 동일한 역할로 묶기 위한 공통 인터페이스를 담은 상위 클래스다.
  4. Client 의뢰자
    Composite 패턴의 사용자이며, Leaf와 Composite을 조합해 복잡한 구조를 만들어 낸다.

Component

먼저 파일과 디렉토리의 공통 인터페이스를 나타내는 객체를 만든다.

abstract class Entry {
  abstract getName(): string;
  abstract getSize(): number;

  printList(): void;
  printList(prefix: string): void;
  printList(prefix?: string) {
    if (prefix === undefined) {
      this.printListImpl('');
    } else {
      this.printListImpl(prefix);
    }
  }

  protected abstract printListImpl(prefix: string): void;

  toString(): string {
    return `${this.getName()} (${this.getSize()})`;
  }
}

Entry 클래스는 이름과 크기를 표시하는 getNamegetSize라는 인터페이스의 형태와, 해당 객체의 하위 내용물들을 모두 출력하는 printList라는 메소드를 가진다.


Leaf

Leaf는 내용물이 되는 단수 객체로 파일 시스템에서는 개별 파일이 이에 해당한다.

class File extends Entry {
  constructor(
    private readonly name: string,
    private readonly size: number,
  ) {
    super();
  }

  getName(): string {
    return this.name;
  }

  getSize(): number {
    return this.size;
  }

  printListImpl(prefix: string): void {
    console.log(`${prefix}/${this.toString()}`);
  }
}

File은 그 안에 다른 내용물이나 그릇을 담을 수 없기 때문에 printListImpl에서 오로지 자기자신만을 출력한다.


Composite

마지막으로 File의 집합체가 될 Directory를 만든다.

class Directory extends Entry {
  private readonly directory: Entry[] = [];

  constructor(
    private readonly name: string,
  ) {
    super();
  }

  getName(): string {
    return this.name;
  }

  getSize(): number {
    return this.directory.reduce((acc, entry) => acc + entry.getSize(), 0);
  }

  add(entry: Entry): Entry {
    this.directory.push(entry);
    return this;
  }

  printListImpl(prefix: string): void {
    console.log(`${prefix}/${this.toString()}`);
    this.directory.forEach((entry) => {
      entry.printList(`${prefix}/${this.name}`);
    });
  }
}

내용물들을 담을 수 있는 그릇이기 때문에 내용물들을 추가하거나 관리하기 위한 메소드들이 정의되어야한다. 여기서는 add라는 메소드가 추가되었다. 그리고 그릇이기 때문에 그릇 안에 포함된 하위 객체들을 출력할 수 있도록 printlistImpl을 구현한다.


Client - 구현 확인

이제 구현한 코드의 동작을 확인해본다.

실제로 파일이나 디렉토리를 생성하지는 않겠지만, 디렉토리를 만들고 그 아래 다른 파일이나 디렉토리를 추가하고 출력을 해보자.

console.log('Making root entries...');
const rootdir = new Directory('root');
const bindir = new Directory('bin');
const tmpdir = new Directory('tmp');
const usrdir = new Directory('usr');

rootdir.add(bindir);
rootdir.add(tmpdir);
rootdir.add(usrdir);

bindir.add(new File('vi', 10000));
bindir.add(new File('latex', 20000));

rootdir.printList();
Making root entries...
/root (30000)
/root/bin (30000)
/root/bin/vi (10000)
/root/bin/latex (20000)
/root/tmp (0)
/root/usr (0)



참고자료

Java언어로 배우는 디자인 패턴 입문 - 쉽게 배우는 Gof의 23가지 디자인패턴 (영진닷컴)

0개의 댓글