플라이웨이트는 대단히 많은 수의 매우 비슷한 객체들이 사용되어야 할 때 메모리사용량을 절감하는 방법으로 자주 사용된다.
만약 대규모 동시 접속 게임에서 사용자의 이름을 저장한다고 하자.
John smith와 같은 이름은 매우 흔해서, 이 내용을 수많은 사람들이 저장하게 하는 것 보다, 이 문자열을 가리키는 포인터를 가지게 하면 메모리를 절약할 수 있을 것이다. 또는 해당 이름들을 자료구조에 저장해두고 이 데이터의 인덱스를 리턴시켜주는 방식도 공간절약에 도움이 될 것이다.
typedef uint32_t key;
struct User
{
User(const string& first_name, const string& last_name)
: first_name{ add(first_name) }, last_name{ add(last_name) } {}
...
protected:
key first_name, last_name;
static bimap<key, string> names;
static key seed;
static key add(cosnt string& s)
{
auto it = names.right.find(s);
if (it == names.right.end())
{
// 새로운 이름은 추가
names.insert({++seed, s});
return seed;
}
return it->second;
}
};
중복되는 값이 많다면 이같은 구조를 통해 메모리 절감이 가능할 것이다.
앞서서는 플라이웨이트를 수작업으로 만들었지만, 이미 좋은 라이브러리에서 플라이웨이트 기능을 구현해놓았다.
struct User2
{
flyweight<string> first_name, last_name;
User2(const string& first_name, const string& last_name)
: first_name{first_name}, last_name{last_name} {}
};
앞서 짠 코드와 동일한 기능을 한다. 중복된 이름을 사용 시, 동일한 메모리 주소를 리턴하는 것을 볼 수 있다.
이처럼 플라이웨이트는 객체마다 변하는 속성(extrinsic)과 객체마다 모두 동일한 속성(intrinsic)을 구분하여 모두 동일한 속성은 공유하여 메모리를 절감하게 하는 것이 포인트다.
플라이웨이트 패턴은 프로그래밍의 기본기 같은 느낌이었다. 메모리 사용량을 최적화하고, 동일하게 사용되는건 반복을 줄여 효율적으로 사용하게 하기 위한 패턴이다.
게임에서 지형이나, 자연환경 구성을 진행하고자 할 때 필수적으로 사용되는 디자인 패턴이다.
Flyweight 즉 경량 패턴은 여러 오브젝트들이 공통적으로 사용하게 되는 부분을 공유 데이터로 만들어두고, 나머지 오브젝트들은 그 데이터를 각기 갖고있는 것이 아닌 공유 데이터를 참조하는 식으로 구성한다.
이를 통해 낭비될 수 있는 메모리 공간을 효율적으로 사용할 수 있게 하는 것이 이 패턴의 목적이다.
혹시 메모리가 충분하다고 하더라도, 게임 프로그래밍에서는 렌더링을 진행하기 위해 데이터를 GPU로 보내는 과정이 필요하다. 수 많은 오브젝트의 동일 데이터를 매 렌더링마다 CPU에서 GPU로 버스를 통해 전달해야한다면 병목현상이 발생해 FPS 값이 내려갈 수 밖에 없다.
경량 패턴(Flyweight Pattern)은 반복적으로 사용되는 객체들의 메모리 사용량을 최소화하기 위한 설계 패턴이다. 특히 게임 프로그래밍에서는 수많은 객체를 관리해야 하므로, 메모리 최적화가 중요한 상황에서 자주 사용된다.
공유 가능한 상태(내부 상태)와 비공유 상태(외부 상태)를 분리
객체 풀(Object Pool) 사용
다음은 Flyweight 패턴의 UML 다이어그램을 텍스트로 표현한 구조입니다:
+------------------+ +-----------------------+
| Flyweight |<--------| FlyweightFactory |
|------------------| |-----------------------|
| + operation(state)| | - flyweights: map |
+------------------+ | + getFlyweight(key): Flyweight |
+-----------------------+
|
+-----------------------------+
|
+-------------------+ implements +--------------------------+
| ConcreteFlyweight |<----------------| UnsharedConcreteFlyweight |
|-------------------| |--------------------------|
| + operation(state): void | | + operation(state): void |
+-------------------+ +--------------------------+
Client
+-------------------+
| - extrinsicState: any |
| + useFlyweight(): void|
+-------------------+
|
+-----------------> Uses Flyweight
Flyweight 인터페이스
operation)를 정의합니다.ConcreteFlyweight
Flyweight를 구현하며, 공유 가능한 내재 상태(intrinsic state)를 관리합니다. (불변)UnsharedConcreteFlyweight
FlyweightFactory
map 구조로 관리됩니다(캐싱).Client
이 다이어그램은 Flyweight 패턴의 역할과 각 클래스의 관계를 나타냅니다. 😊
#include <iostream>
#include <unordered_map>
#include <string>
#include <memory>
// Flyweight 객체: 공유 가능한 상태를 관리
class Sprite {
public:
explicit Sprite(const std::string& texture) : texture_(texture) {}
void render(int x, int y) {
std::cout << "Rendering sprite [" << texture_ << "] at (" << x << ", " << y << ")\n";
}
private:
std::string texture_; // 내부 상태
};
// Flyweight Factory: 객체 재사용 관리
class SpriteFactory {
public:
std::shared_ptr<Sprite> getSprite(const std::string& texture) {
if (sprites_.find(texture) == sprites_.end()) {
sprites_[texture] = std::make_shared<Sprite>(texture);
std::cout << "Creating new sprite for texture: " << texture << "\n";
}
return sprites_[texture];
}
private:
std::unordered_map<std::string, std::shared_ptr<Sprite>> sprites_;
};
// 클라이언트 코드
int main() {
SpriteFactory factory;
auto tree = factory.getSprite("TreeTexture");
auto rock = factory.getSprite("RockTexture");
tree->render(10, 20);
tree->render(15, 25);
rock->render(50, 60);
rock->render(55, 65);
// 재사용 확인
auto anotherTree = factory.getSprite("TreeTexture");
anotherTree->render(100, 200);
return 0;
}
Creating new sprite for texture: TreeTexture
Creating new sprite for texture: RockTexture
Rendering sprite [TreeTexture] at (10, 20)
Rendering sprite [TreeTexture] at (15, 25)
Rendering sprite [RockTexture] at (50, 60)
Rendering sprite [RockTexture] at (55, 65)
Rendering sprite [TreeTexture] at (100, 200)