디자인 패턴

Flyweight 패턴

Keisa 2024. 7. 25. 10:14

의도

플라이웨이트는 각 객체에 모든 데이터를 유지하는 대신 여러 객체들 간에 상태의 공통 부분들을 공유하여 사용할 수 있는 RAM에 더 많은 객체들을 포함할 수 있도록 하는 구조 디자인 패턴입니다.

 문제

당신은 재미 삼아 플레이어들이 지도를 돌아다니며 서로에게 총을 쏘는 간단한 비디오 게임을 만들기로 했습니다. 당신은 폭발들로 인한 방대한 양의 총알들, 미사일들 및 파편들이 지도 전체를 날아다니는 전율 넘치는 경험을 플레이어들에게 선사하기로 했으며, 이를 선사하기 위해 사실적인 입자 시스템을 구현하기로 했습니다.

당신은 게임을 완성한 후 친구에게 게임을 보냈습니다. 당신의 컴퓨터에서는 게임이 완벽하게 실행되었지만, 당신의 친구는 오랫동안 게임을 즐길 수 없었습니다. 왜냐하면 친구의 컴퓨터에서는 시작 후 고작 몇 분 후에 게임이 계속 충돌했기 때문입니다. 당신이 디버그 로그를 자세히 살펴본 결과, 친구의 컴퓨터의 RAM이 당신의 컴퓨터처럼 충분하지 않아 게임이 충돌했음이 분명해졌습니다.

문제의 원인은 당신의 입자 시스템과 관련이 있었습니다. 각 총알, 미사일 또는 파편과 같은 입자는 많은 데이터를 포함하는 별도의 객체로 표시되었습니다. 플레이어 화면의 대학살이 절정에 이르렀을 때 새로 생성된 입자들을 더 이상 나머지 RAM이 감당하지 못해서 프로그램이 충돌했습니다.

 해결책

Particle​(입자) 클래스를 자세히 살펴보면 color​(색상) 및 sprite(스프라이트) 필드들이 다른 필드들보다 훨씬 더 많은 메모리를 사용한다는 것을 알 수 있습니다. 더 나쁜 것은 이 두 필드가 모든 입자에 걸쳐 거의 같은 데이터를 저장한다는 것입니다. 예를 들어, 모든 총알은 같은 색상과 스프라이트를 갖습니다.

좌표, 이동 벡터 및 속도와 같은 입자 상태의 다른 부분들은 각 입자마다 고유하며, 이러한 필드들의 값은 시간이 지남에 따라 변경됩니다. 이 데이터는 입자의 계속 변화하는 콘텍스트를 나타내나, 반면 색상과 스프라이트는 각 입자마다 일정하게 유지됩니다.

객체의 이러한 상수 데이터를 일반적으로  라고 합니다. 이 데이터는 객체 안에서 삽니다. 다른 객체들은 이 데이터를 읽을 수만 있고 변경할 수는 없습니다. 종종 다른 객체들에 의해 '외부에서' 변경되는 객체의 나머지 상태를  라고 합니다.

플라이웨이트 패턴은 객체 내부에 공유한 상태의 저장을 중단하고, 대신 이 상태를 이 상태에 의존하는 특정 메서드들에 전달할 것을 제안합니다. 고유한 상태만 객체 내에 유지되므로 해당 고유한 상태는 콘텍스트가 다른 곳에서 재사용할 수 있습니다. 이러한 객체들은 공유한 상태보다 변형이 훨씬 적은 고유한 상태에서만 달라지므로 훨씬 더 적은 수의 객체만 있으면 됩니다.

이제 당신의 게임을 다시 살펴봅시다. 입자 클래스에서 공유한 상태를 추출했다고 가정하면 총알, 미사일, 파편의 세 가지 다른 객체만으로도 게임의 모든 입자를 충분히 나타낼 수 있습니다. 지금쯤 짐작하셨겠지만 고유한 상태만 저장하는 객체를 플라이웨이트라고 합니다.

공유한 상태 스토리지

공유한 상태는 어디로 이동할까요? 일부 클래스가 이 상태를 여전히 저장하고 있는 거겠죠? 대부분의 경우 공유한 상태는 패턴을 적용하기 전에 객체들을 집합시키는 컨테이너 객체로 이동됩니다.

당신의 게임에서 이것은 particle 필드에 모든 입자를 저장하는 주요 Game 객체입니다. 공유한 상태를 이 클래스로 이동하려면 개별 입자의 좌표, 벡터 및 속도를 저장하기 위한 여러 배열 필드들을 생성해야 합니다. 거기서 끝이 아닙니다. 입자를 나타내는 특정 플라이웨이트에 대한 참조를 저장하려면 다른 배열이 필요합니다. 이러한 배열들은 같은 인덱스를 사용하여 입자의 모든 데이터에 액세스할 수 있도록 동기화되어야 합니다.

이보다 더 훌륭한 해결책은 플라이웨이트 객체에 대한 참조와 함께 공유된 상태를 저장할 별도의 콘텍스트 클래스를 만드는 것입니다. 이 접근 방식을 사용하려면 컨테이너 클래스에 단일 배열만 있으면 됩니다.

잠시만요! 처음에 그랬던 것처럼 이런 콘텍스트 객체들이 많이 있어야 하지 않나요? 맞습니다. 그러나 이제는 이러한 객체들이 이전보다 훨씬 작습니다. 가장 메모리를 많이 사용하는 필드들이 고작 몇 개의 플라이웨이트 객체들로 이동되었습니다. 이제 하나의 커다란 플라이웨이트 객체를 몇천 개의 작은 콘텍스트 객체들이 재사용할 수 있으며, 더 이상 커다란 플라이웨이트 객체의 데이터의 천 개의 복사본을 저장할 필요가 없습니다.

플라이웨이트와 불변성

같은 플라이웨이트 객체가 다른 콘텍스트들에서 사용될 수 있으므로 해당 플라이웨이트 객체의 상태를 수정할 수 없는지 확인해야 합니다. 플라이웨이트는 생성자 매개변수들을 통해 상태를 한 번만 초기화해야 합니다. 또 setter 또는 public 필드들을 다른 객체들에 노출해서는 안 됩니다.

플라이웨이트 팩토리

다양한 플라이웨이트들에 보다 편리하게 액세스하기 위해 기존 플라이웨이트 객체들의 풀을 관리하는 팩토리 메서드를 생성할 수 있습니다. 이 메서드는 클라이언트에서 원하는 플라이웨이트의 고유한 상태를 받아들이고 이 상태와 일치하는 기존 플라이웨이트 객체를 찾고 발견되면 반환합니다. 그렇지 않으면 새 플라이웨이트를 생성하여 풀에 추가합니다.

이 메서드를 배치할 수 있는 몇 가지 옵션이 있습니다. 그중 가장 확실한 장소는 플라이웨이트 컨테이너입니다. 대안으로 당신은 새 팩토리 클래스를 생성할 수 있고, 또 팩토리 메서드를 정적으로 만들고 실제 플라이웨이트 클래스에 넣을 수 있습니다.

 

/**
 * Flyweight Design Pattern
 *
 * Intent: Lets you fit more objects into the available amount of RAM by sharing
 * common parts of state between multiple objects, instead of keeping all of the
 * data in each object.
 */

struct SharedState
{
    std::string brand_;
    std::string model_;
    std::string color_;

    SharedState(const std::string &brand, const std::string &model, const std::string &color)
        : brand_(brand), model_(model), color_(color)
    {
    }

    friend std::ostream &operator<<(std::ostream &os, const SharedState &ss)
    {
        return os << "[ " << ss.brand_ << " , " << ss.model_ << " , " << ss.color_ << " ]";
    }
};

struct UniqueState
{
    std::string owner_;
    std::string plates_;

    UniqueState(const std::string &owner, const std::string &plates)
        : owner_(owner), plates_(plates)
    {
    }

    friend std::ostream &operator<<(std::ostream &os, const UniqueState &us)
    {
        return os << "[ " << us.owner_ << " , " << us.plates_ << " ]";
    }
};

/**
 * The Flyweight stores a common portion of the state (also called intrinsic
 * state) that belongs to multiple real business entities. The Flyweight accepts
 * the rest of the state (extrinsic state, unique for each entity) via its
 * method parameters.
 */
class Flyweight
{
private:
    SharedState *shared_state_;

public:
    Flyweight(const SharedState *shared_state) : shared_state_(new SharedState(*shared_state))
    {
    }
    Flyweight(const Flyweight &other) : shared_state_(new SharedState(*other.shared_state_))
    {
    }
    ~Flyweight()
    {
        delete shared_state_;
    }
    SharedState *shared_state() const
    {
        return shared_state_;
    }
    void Operation(const UniqueState &unique_state) const
    {
        std::cout << "Flyweight: Displaying shared (" << *shared_state_ << ") and unique (" << unique_state << ") state.\n";
    }
};
/**
 * The Flyweight Factory creates and manages the Flyweight objects. It ensures
 * that flyweights are shared correctly. When the client requests a flyweight,
 * the factory either returns an existing instance or creates a new one, if it
 * doesn't exist yet.
 */
class FlyweightFactory
{
    /**
     * @var Flyweight[]
     */
private:
    std::unordered_map<std::string, Flyweight> flyweights_;
    /**
     * Returns a Flyweight's string hash for a given state.
     */
    std::string GetKey(const SharedState &ss) const
    {
        return ss.brand_ + "_" + ss.model_ + "_" + ss.color_;
    }

public:
    FlyweightFactory(std::initializer_list<SharedState> share_states)
    {
        for (const SharedState &ss : share_states)
        {
            this->flyweights_.insert(std::make_pair<std::string, Flyweight>(this->GetKey(ss), Flyweight(&ss)));
        }
    }

    /**
     * Returns an existing Flyweight with a given state or creates a new one.
     */
    Flyweight GetFlyweight(const SharedState &shared_state)
    {
        std::string key = this->GetKey(shared_state);
        if (this->flyweights_.find(key) == this->flyweights_.end())
        {
            std::cout << "FlyweightFactory: Can't find a flyweight, creating new one.\n";
            this->flyweights_.insert(std::make_pair(key, Flyweight(&shared_state)));
        }
        else
        {
            std::cout << "FlyweightFactory: Reusing existing flyweight.\n";
        }
        return this->flyweights_.at(key);
    }
    void ListFlyweights() const
    {
        size_t count = this->flyweights_.size();
        std::cout << "\nFlyweightFactory: I have " << count << " flyweights:\n";
        for (std::pair<std::string, Flyweight> pair : this->flyweights_)
        {
            std::cout << pair.first << "\n";
        }
    }
};

// ...
void AddCarToPoliceDatabase(
    FlyweightFactory &ff, const std::string &plates, const std::string &owner,
    const std::string &brand, const std::string &model, const std::string &color)
{
    std::cout << "\nClient: Adding a car to database.\n";
    const Flyweight &flyweight = ff.GetFlyweight({brand, model, color});
    // The client code either stores or calculates extrinsic state and passes it
    // to the flyweight's methods.
    flyweight.Operation({owner, plates});
}

/**
 * The client code usually creates a bunch of pre-populated flyweights in the
 * initialization stage of the application.
 */

int main()
{
    FlyweightFactory *factory = new FlyweightFactory({{"Chevrolet", "Camaro2018", "pink"}, {"Mercedes Benz", "C300", "black"}, {"Mercedes Benz", "C500", "red"}, {"BMW", "M5", "red"}, {"BMW", "X6", "white"}});
    factory->ListFlyweights();

    AddCarToPoliceDatabase(*factory,
                            "CL234IR",
                            "James Doe",
                            "BMW",
                            "M5",
                            "red");

    AddCarToPoliceDatabase(*factory,
                            "CL234IR",
                            "James Doe",
                            "BMW",
                            "X1",
                            "red");
    factory->ListFlyweights();
    delete factory;

    return 0;
}

'디자인 패턴' 카테고리의 다른 글

Chain of Responsibility 패턴  (0) 2024.07.25
Proxy 패턴  (0) 2024.07.25
Facade 패턴  (0) 2024.07.25
Decorator 패턴  (0) 2024.07.11
Composite 패턴  (0) 2024.07.11