[Structural Patterns] - Flyweight

Lee Jeong Min·2022년 1월 15일
0

디자인 패턴

목록 보기
12/23
post-thumbnail

의도

플라이웨이트는 각 객체의 모든 데이터를 유지하는 대신 여러 객체 간에 공통적인 상태 부분을 공유하여 더 많은 객체를 한정된 RAM양 안에서 사용 가능하게 맞출 수 있는 구조 설계 패턴이다.

플라이웨이트는 프로그램이 메모리 소비를 최소한으로 하여 방대한 양의 객체를 지원할 수 있다.

→ 플라이 웨이트를 다른 이름으로 캐시라고 부른다.

문제

오랫동안 일하고 난 후, 재미를 위해 간단한 비디오 게임을 만들기로 결심했다고 가정하자. 이 게임은 플레이어들이 지도 주변을 움직이면서 서로 슈팅하는 게임이다. 게임의 독특한 기능으로 현실적인 입자 시스템을 구현하기로 결정하여 엄청난 양의 총알, 미사일, 폭발로 인한 파편이 지도 곳곳을 날아다니게 만들어 플레이어에게 짜릿한 경험을 줄 수 있게 하였다.

마지막 커밋을 하여 완료 후에 친구에게 게임을 보내 테스트를 시켜보았다. 내 컴퓨터에서는 이 게임이 완벽하게 실행되었지만 친구는 오랫동안 게임을 할 수 없었다. 그 이유로 친구의 컴퓨터가 나의 컴퓨터보다 RAM의 양이 부족하여 게임이 중단되었다는 것을 알아냈다.

실제 문제는 입자 시스템과 관련이 있었다. 총알, 미사일 또는 파편 조각과 같은 각 입자는 많은 데이터를 포함하는 별도의 물체로 표현되었다. 어느 순간 플레이어의 스크린에서 대학살이 절정에 달했을 때 새로 생성된 입자들이 남은 RAM에 더 이상 맞지 않아 프로그램이 충돌했다.

Particle 부분에 데이터를 많이 포함하는 문제

해결책

Particle 클래스를 자세히 살펴보면 색상과 스프라이트 필드가 다른 필드보다 메모리를 훨씬 많이 소모하는 것을 알 수 있다. 더 안좋았던 것은 두 필드가 모든 입자에 걸쳐 거의 동일한 데이터를 저장한다는 것이다.(모든 총알은 같은 색깔과 스프라이트를 가지고 있음)

좌표, 움직임 벡터, 속도와 같은 입자의 상태의 다른 부분들은 각 입자에 대해 고유하다. 이러한 필드의 값은 색상과 스프라이트와 달리 시간이 지남에 따라 변한다.

객체의 이러한 상수 데이터를 보통 본질 상태라고 한다. 다른 객체들은 읽을 수 있을 뿐 바꿀 수 없다. 물체의 나머지 상태는 다른 물체에 의해 '외부로부터' 변경되며, 외적 상태라고 불린다.

플라이웨이트 패턴을 보면 객체 내부의 외부 상태 저장을 방지할 수 있다. 대신 이 상태를 의존하는 특정 메서드로 전달해야 한다. 따라서 이러한 객체는 외적 객체보다 변형이 훨씬 적은 내적 상태만 다르기 때문에 더 적은 수의 객체를 필요로 한다.(색상과 스프라이트를 내부가 아닌 다른곳에 만든 후 특정 방법으로 전달)

게임으로 돌아와서 입자 클래스에서 외부 상태를 추출했다고 가정하면, 총알, 미사일, 파편 조각 이 세 개의 다른 물체만이 게임의 모든 입자를 나타내기에 충분할 것이다. 이제 충분히 짐작 가능하듯이 본질적인 상태만 저장하는 물체를 플라이웨이트라고 부른다.

외부 상태 스토리지

외적인 상태는 어디로 이동할까? 어느 Class는 여전히 이것을 보관해야한다. 대부분의 경우 패턴을 적용하기 전에 객체를 집계하는 컨테이너 객체로 이동한다.

이 케이스의 경우 이것이 입자 필드에 있는 모든 입자를 저장하는 주요 게임 객체이다. 외부 상태를 이 클래스로 이동하려면 각 개별 입자의 좌표, 벡터 및 속도를 저장하기 위한 배열 필드를 여러개 작성해야한다. 하지만 이게 다가 아니다. 입자를 나타내는 특정 플라이 웨이트에 대한 참조를 저장하기 위해 다른 배열이 필요하다. 동일한 인덱스를 사용하여 입자의 모든 데이터에 액세스할 수 있도록 이러한 배열이 동기화 되어 있어야 한다.

더 세련된 방법은 플라이웨이트 객체에 대한 참조와 함께 외부 상태를 저장하는 별도의 문맥(Context) 클래스를 만드는 것이다. 이 방법은 컨테이너 클래스에 단일 배열만 있으면 된다.

엄밀히 말하면 처음처럼 많은 문맥적 대상들이 필요하다. 그러나 메모리를 가장 많이 사용하는 필드는 몇개의 플라이웨이트 객체로 이동되어 수천 개의 작은 컨텍스트 객체가 수천 개의 데이터 복사본을 저장하는 대신 플라이웨이트 객체 하나를 재사용할 수 있다.

플라이웨이트 및 불변성

동일한 플라이웨이트 객체가 다른 컨텍스트에서 사용될 수 있으므로 상태를 수정할 수 없도록 해야한다. 플라이웨이트는 생성자 매개변수를 통해 상태를 한번만 초기화해야 한다. setter나 public 필드가 다른 객체에 노출되어서는 안된다.

플라이웨이트 공장
다양한 플라이웨이트에 보다 편리하게 액세스하기 위해 기존 플라이웨이트 객체의 풀을 관리하는 공장 메서드를 만들 수 있다. 이 메서드는 클라이언트에서 원하는 플라이웨이트의 고유 상태를 수락하고 이 상태와 일치하는 기존 플라이웨이트 객체를 찾아 찾은 경우 반환한다. 그렇지 않으면 새 플라이 가중치를 만들어 풀에 추가한다.

이 방법을 배치할 수 있는 몇 가지 옵션이 있다. 가장 명백한 장소는 플라이웨이트 컨테이너이다. 또는 새 공장 클래스를 만들 수 있다. 아니면 공장 메서드를 정적으로 만들어 실제 플라이웨이트에 넣을 수도 있다.

구조

  1. 플라이웨이트 패턴은 최적화를 시키는 것이다. 적용하기 전에 프로그램 메모리에 비슷한 객체의 수가 많은 것과 관련된 RAM 소모 문제가 있는지 확인해라.

  2. 플라이웨이트 클래스에는 원래 객체 상태의 여러 객체 간에 공유할 수 있는 부분이 포함된다. 동일한 플라이웨이트 객체는 여러 다른 맥락(Contexts)에서 사용될 수 있다. 플라이 웨이트 안에 저장된 상태를 고유상태라고 하고 플라이웨이트 메서드로 전달되는 상태를 외적(extrinsic)이라고 한다.

  3. Context 클래스는 모든 Original 객체에서 고유한 외부 상태를 포함한다. 컨텍스트가 플라이웨이트 객체 중 하나와 쌍을 이루면 원래 객체의 전체 상태를 나타낸다.

  4. 일반적으로 원래 객체의 동작은 플라이웨이트에 남아있는데, 이 경우 플라이웨이트 메서드를 호출하는 사람은 누구든지 적절한 수의 외적 상태를 메서드의 매개변수로 전달해야 한다.

  5. 클라이언트는 플라이 웨이트의 외부 상태를 계산하거나 저장한다. 클라이언트의 관점에서 플라이웨이트는 일부 상황별 데이터를 메서드의 매개변수로 전달하여 런타임에 구성할 수 있는 템플릿 객체이다.

  6. 플라이웨이트 공장은 기존 플라이웨이트 풀을 관리한다. 공장에서 고객은 플라이웨이트를 직접 만들지 않고 공장을 플라이웨이트의 본질적인 상태의 일부분을 전달하면서 부른다. 공장에서는 이전에 만든 플라이 웨이트를 검토하고 검색 기준과 일치하는 기존 플라이 웨이트를 반환하거나 아무것도 발견되지 않으면 새 플라이 웨이트를 만든다.

적용가능성

  • 플라이웨이트 패턴은 프로그램이 사용 가능한 RAM이 부족할 정도로 수많은 객체를 지원해야 할 때만 사용하라.

장단점

장점

  • 프로그램에 유사한 객체가 엄청 많이 있다면 RAM을 절약할 수 있다.

단점

  • 누군가 플라이웨이트 메서드를 호출할 때마다 일부 컨텍스트 데이터를 다시 계산해야하는 경우 RAM 대신 CPU를 트레이딩하는 결과가 된다.(CPU와 트레이드 오프 관계를 말하는 것 같다.)

  • 코드가 훨씬 복잡해진다. 새로운 팀원들을 왜 한 객체의 상태가 그런식으로 분리되었는지 궁금해 할 것이다.

Flyweight in TypeScript

TypeScript의 패턴 사용

복잡도: ★★★

인기: ☆☆☆

사용 예: 플라이웨이트 패턴은 메모리 소비를 최소화하는 단일 목적을 가지고 있다. 프로그램이 RAM 부족으로 어려움을 겪지 않는다면 이 패턴을 사용하지 않아도 된다.

식별: 플라이웨이트는 새로 생성하는 대신 캐시된 객체를 반환하는 생성 방법으로 인식될 수 있다.

index.ts

// 플라이웨이트는 여러 실제 비즈니스 엔티티에 속하는 상태의 공통 부분(내재 상태)을 저장한다.
// 플라이웨이트는 메서드 매개 변수를 통해 나머지 상태(각 엔티티에 대해 고유한 외적 상태)를 받아들인다.
class Flyweight {
  private sharedState: any;

  constructor(sharedState: any) {
    this.sharedState = sharedState;
  }

  public operation(uniqueState): void {
    const s = JSON.stringify(this.sharedState);
    const u = JSON.stringify(uniqueState);
    console.log(`Flyweight: Displaying shared (${s}) and unique (${u}) state.`);
  }
}

// 플라이웨이트 공장은 플라이웨이트 객체를 생성하고 관리하여 플라이웨이트가 올바르게 공유되도록 한다.
// 클라이언트가 플라이 웨이트를 요청할 때 팩토리는 기존 인스턴스를 반환하거나 아직 존재하지 않는 경우 새 인스턴스를 만든다.
class FlyweightFactory {
  private flyweights: { [key: string]: Flyweight } = <any>{};

  constructor(initialFlyweights: string[][]) {
    for (const state of initialFlyweights) {
      this.flyweights[this.getKey(state)] = new Flyweight(state);
    }
    console.log(this.flyweights);
  }

  // 지정된 상태에 대한 플라이웨이트의 문자열 해시를 반환한다.
  private getKey(state: string[]): string {
    return state.join('_');
  }

  // 지정된 상태를 가진 기존 플라이 웨이트를 반환하거나 새 플라이 웨이트를 만든다.
  public getFlyweight(sharedState: string[]): Flyweight {
    const key = this.getKey(sharedState);

    if (!(key in this.flyweights)) {
      console.log("FlyweightFactory: Can't find a flyweight, creating new one.");
      this.flyweights[key] = new Flyweight(sharedState);
    } else {
      console.log('FlyweightFactory: Resuing existing flyweight.');
    }

    return this.flyweights[key];
  }

  public listFlyweights(): void {
    const count = Object.keys(this.flyweights).length;
    console.log(`\nFlyweightFactory: I have ${count} flyweights:`);
    for (const key in this.flyweights) {
      console.log(key);
    }
  }
}

const factory = new FlyweightFactory([
  ['Chevrolet', 'Camaro2018', 'pink'],
  ['Mercedes Benz', 'C300', 'black'],
  ['Mercedes Benz', 'C500', 'red'],
  ['BMW', 'M5', 'red'],
  ['BMW', 'X6', 'white'],
]);
factory.listFlyweights();

function addCarToPoliceDatabase(
  ff: FlyweightFactory,
  plates: string,
  owner: string,
  brand: string,
  model: string,
  color: string
) {
  console.log('\nClient: Adding a car to database.');
  const flyweight = ff.getFlyweight([brand, model, color]);

  flyweight.operation([plates, owner]);
}

addCarToPoliceDatabase(factory, 'CL234IR', 'James Doe', 'BMW', 'M5', 'red');

addCarToPoliceDatabase(factory, 'CL234IR', 'James Doe', 'BMW', 'X1', 'red');

factory.listFlyweights();

결과

요약

플라이 웨이트는 유사한 객체가 수없이 많은 때, 공통적인 부분을 공유하여 한정된 RAM양 안에서 사용 가능하게 맞추어 주는 구조 설계 패턴이다.(캐시)

본질 상태와 외부 상태를 분리하고 이 외부상태를 의존하는 특정 메서드로 전달하여 사용한다. 또한 공유된 상태에 플라이웨이트가 있다면 반환하고 없다면 생성하는 방식으로 작동한다.

플라이웨이트: 본질적인 상태만 저장하는 물체!

참고 사이트

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글