Ayden's journal

플라이웨이트 패턴

플라이웨이트는 권투 등의 격투기에서 '경량급'을 지칭하는 표현이다. 이처럼 플라이웨이트 패턴(Flyweight Pattern)은 동일한 데이터를 여러 객체 간에 공유하는 방식으로 객체를 경량화하는, 메모리 효율성을 극대화하는 디자인 패턴이다. 이 패턴의 핵심은 공유 가능한 데이터(intrinsic)를 하나의 객체로 관리하고, 객체마다 달라야 하는 데이터(extrinsic)는 별도로 처리하는 것이다.

플라이웨이트 패턴에서는 FlyweightFactory 클래스를 사용해 동일한 내부 상태를 공유하는 객체들을 재사용하여 메모리 사용을 최적화한다. 내부 상태는 여러 객체에서 공유되는 불변의 데이터로, FlyweightFactory가 관리하는 객체들에 의해 공통적으로 사용된다. 객체를 요청하면, FlyweightFactory는 이미 생성된 객체를 반환하거나, 필요시 새 객체를 생성하여 반환한다. 이 방식으로 동일한 상태를 여러 객체가 공유하게 되어 메모리를 절약할 수 있는 것이다.

이렇게 생성된 Flyweight 객체는 공유 가능한 내부 상태만 저장하며, 개별 객체마다 다른 속성은 FlyweightContext에서 따로 관리한다. 이를 통해 메모리 사용량을 최적화하면서도 각 객체의 개별성을 유지할 수 있다. 예를 들어, 게임에서 "Warrior"와 같은 캐릭터 유형(공격력, 방어력 등)은 Flyweight 객체로 공유되지만, 개별 캐릭터의 위치나 플레이어 이름은 FlyweightContext가 관리하여 독립적인 동작이 가능하도록 한다.

type TCharacterType = "Warrior" | "Archer" | "Mage"

// Flyweight: 공유되는 내부 상태 관리
class CharacterFlyweight {
  constructor(
    public type: TCharacterType,
    public attack: number,
    public defense: number
  ) {}
}

// FlyweightFactory: 공유 객체를 생성 및 관리
class CharacterFlyweightFactory {
  private characterMap: Map<TCharacterType, CharacterFlyweight> = new Map()
  private stats = deepFreeze({ // 여러 객체에서 공유되는 "불변"의 데이터
    Warrior: { attack: 50, defense: 30 },
    Archer: { attack: 40, defense: 20 },
    Mage: { attack: 60, defense: 10 },
  })

  getFlyweight(type: TCharacterType) {
    if (!this.characterMap.get(type)) {
      this.characterMap.set(type, new CharacterFlyweight(type, this.stats[type].attack, this.stats[type].defense))
    }
    return this.characterMap.get(type) as CharacterFlyweight
  }
}

// FlyweightContext: 외부 상태를 관리
class CharacterContext {
  constructor(
    private name: string,
    private x: number,
    private y: number,
    private flyweight: CharacterFlyweight,
  ) {}

  // 현재 상태 출력
  display() {
    console.log(
      `[${this.name}] ${this.flyweight.type} - 공격력: ${this.flyweight.attack}, 방어력: ${this.flyweight.defense}, 위치: X: ${this.x}, Y: ${this.y}`
    );
  }

  // 위치 이동 메서드
  move(dx: number, dy: number) {
    this.x += dx;
    this.y += dy;
    console.log(`[${this.name}] 이동 -> 새로운 위치: X: ${this.x}, Y: ${this.y}`);
  }
}

const factory = new CharacterFlyweightFactory()
const warrior1 = new CharacterContext("ayden", 0, 0, factory.getFlyweight("Warrior"))
const warrior2 = new CharacterContext("blair", 0, 0, factory.getFlyweight("Warrior"))

 

 

CharactorContext에 제공해주어야 할 파라미터가 많다보니 빌더 패턴을 적용해도 괜찮을 것 같다.

class CharacterBuilder {
  private name: string | undefined;
  private x: number | undefined;
  private y: number | undefined;
  private flyweight: CharacterFlyweight | undefined;
  private static flyweightFactory = new CharacterFlyweightFactory();

  setName(name: string) {
    this.name = name;
    return this;
  }

  setPosition({x, y}: {x: number, y: number}) {
    this.x = x;
    this.y = y;
    return this;
  }

  setFlyweightType(type: TCharacterType) {
    this.flyweight = CharacterBuilder.flyweightFactory.getFlyweight(type);
    return this;
  }

  build() {
    if (!this.name) throw new Error("Name is required")
    if (!this.x || !this.y) throw new Error("Position is required")
    if (!this.flyweight) throw new Error("Flyweight is required")

    return new CharacterContext(this.name, this.x, this.y, this.flyweight)
  }
}

const warrior1 = new CharacterBuilder()
  .setName("ayden")
  .setPosition({ x: 0, y: 0 })
  .setFlyweightType("Warrior")
  .build()
const warrior2 = new CharacterBuilder()
  .setName("blair")
  .setFlyweightType("Warrior")
  .build() // Error!! Position is required

 

블로그의 정보

Ayden's journal

Beard Weard Ayden

활동하기