[디자인 패턴] 플라이웨이트(경량)

나우히즈·2024년 11월 25일
0

Design Pattern

목록 보기
9/22

플라이웨이트는 대단히 많은 수의 매우 비슷한 객체들이 사용되어야 할 때 메모리사용량을 절감하는 방법으로 자주 사용된다.


사용자 이름

만약 대규모 동시 접속 게임에서 사용자의 이름을 저장한다고 하자.
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;
    }
};

중복되는 값이 많다면 이같은 구조를 통해 메모리 절감이 가능할 것이다.


Boost.Flyweight

앞서서는 플라이웨이트를 수작업으로 만들었지만, 이미 좋은 라이브러리에서 플라이웨이트 기능을 구현해놓았다.

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} {}
};

앞서 짠 코드와 동일한 기능을 한다. 중복된 이름을 사용 시, 동일한 메모리 주소를 리턴하는 것을 볼 수 있다.


Flyweight 구조

이처럼 플라이웨이트는 객체마다 변하는 속성(extrinsic)과 객체마다 모두 동일한 속성(intrinsic)을 구분하여 모두 동일한 속성은 공유하여 메모리를 절감하게 하는 것이 포인트다.

  • Flyweight : 객체들을 묶는 인터페이스
  • SharedConcreteFlyweight : 공유 가능한 속성을 보유하는 객체
  • UnsharedConcreteFlyweight : 공유될 수 없는 고유한 값들을 보유한 객체
  • FlyweightFactory : 객체를 만드는 공장 역할이면서 공유되는 값들을 담고있을 클래스.

정리

플라이웨이트 패턴은 프로그래밍의 기본기 같은 느낌이었다. 메모리 사용량을 최적화하고, 동일하게 사용되는건 반복을 줄여 효율적으로 사용하게 하기 위한 패턴이다.



추가학습 : 게임디자인패턴

게임에서 지형이나, 자연환경 구성을 진행하고자 할 때 필수적으로 사용되는 디자인 패턴이다.
Flyweight 즉 경량 패턴은 여러 오브젝트들이 공통적으로 사용하게 되는 부분을 공유 데이터로 만들어두고, 나머지 오브젝트들은 그 데이터를 각기 갖고있는 것이 아닌 공유 데이터를 참조하는 식으로 구성한다.

이를 통해 낭비될 수 있는 메모리 공간을 효율적으로 사용할 수 있게 하는 것이 이 패턴의 목적이다.
혹시 메모리가 충분하다고 하더라도, 게임 프로그래밍에서는 렌더링을 진행하기 위해 데이터를 GPU로 보내는 과정이 필요하다. 수 많은 오브젝트의 동일 데이터를 매 렌더링마다 CPU에서 GPU로 버스를 통해 전달해야한다면 병목현상이 발생해 FPS 값이 내려갈 수 밖에 없다.

게임 프로그래밍에서의 경량 패턴(Flyweight Pattern)

경량 패턴(Flyweight Pattern)은 반복적으로 사용되는 객체들의 메모리 사용량을 최소화하기 위한 설계 패턴이다. 특히 게임 프로그래밍에서는 수많은 객체를 관리해야 하므로, 메모리 최적화가 중요한 상황에서 자주 사용된다.


경량 패턴의 주요 개념

  1. 공유 가능한 상태(내부 상태)와 비공유 상태(외부 상태)를 분리

    • 내부 상태(Intrinsic State): 공유 가능한 데이터로, 변경되지 않는 고정적인 정보. 메모리에서 재사용됨.
    • 외부 상태(Extrinsic State): 개별 객체의 고유한 정보로, 객체 간 공유되지 않음. 실행 시 클라이언트가 전달.
  2. 객체 풀(Object Pool) 사용

    • 경량 패턴은 동일한 특성을 가진 객체를 객체 풀에서 재활용해 메모리 사용량을 줄임.
    • 단일한 경량 패턴만 사용하기 보다는 객체 풀 패턴을 함께 사용한다. 이전에 만들어놓은 경량 패턴 객체를 반환하려면, 이미 생성해둔 객체를 찾을 수 있도록 풀을 관리해야한다.

게임 프로그래밍에서의 활용 예시

1. 스프라이트(Sprite) 렌더링

  • 문제: 게임에는 수천 개의 동일한 스프라이트(예: 나무, 풀, 돌)가 반복적으로 사용됨.
  • 해결: 동일한 스프라이트 데이터를 공유해 메모리 낭비를 방지.
  • 구현:
    • 각 스프라이트의 텍스처, 색상 등 공유 가능한 데이터(내부 상태)를 하나로 통합.
    • 위치, 크기 등 개별적인 속성은 외부 상태로 관리.

2. 문자 렌더링(Font Rendering)

  • 문제: 게임에서 글씨를 표시하기 위해 각 문자를 개별적으로 렌더링하면 메모리와 렌더링 리소스 낭비.
  • 해결: 동일한 폰트 데이터를 재사용하며, 글자 위치와 크기만 별도로 설정.

3. 파티클 시스템(Particle System)

  • 문제: 수천~수백만 개의 파티클 객체를 생성할 경우, 메모리와 성능 문제가 발생.
  • 해결: 동일한 파티클의 텍스처와 기본 속성을 공유하고, 위치, 속도 등만 개별적으로 변경.

4. 타일맵(Tilemap)

  • 문제: 2D 게임에서 배경을 구성하는 수많은 타일이 동일한 텍스처를 사용.
  • 해결: 타일의 이미지 데이터는 하나만 유지하고, 위치 정보를 외부 상태로 관리.

경량 패턴 구조

다음은 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
          

설명:

  1. Flyweight 인터페이스

    • 공통된 메서드(operation)를 정의합니다.
    • 모든 구체적 경량 객체가 구현해야 하는 메서드입니다.
  2. ConcreteFlyweight

    • Flyweight를 구현하며, 공유 가능한 내재 상태(intrinsic state)를 관리합니다. (불변)
  3. UnsharedConcreteFlyweight

    • 공유되지 않는 객체로, 일반적으로 Flyweight 패턴 외부에서 사용됩니다.
    • ConcreteFlyweight 를 상속받거나, 합성하여 하나의 객체로 관리하게 됨.
  4. FlyweightFactory

    • Flyweight 객체를 생성하고 관리하며, 동일한 요청 시 기존 Flyweight 객체를 반환합니다.
    • Flyweight 객체는 내부적으로 map 구조로 관리됩니다(캐싱).
  5. Client

    • Flyweight 객체를 사용하며, 고유한 외재 상태(extrinsic state)를 추가로 관리합니다.
    • 외재 상태는 Flyweight 객체가 공유하지 않고 클라이언트별로 개별적입니다.

이 다이어그램은 Flyweight 패턴의 역할과 각 클래스의 관계를 나타냅니다. 😊


경량 패턴 구현 예제 (C++)

#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)

장점

  1. 메모리 효율성
    • 반복적으로 사용되는 데이터가 적어져 메모리 사용량 감소.
  2. 성능 향상
    • 객체를 생성 및 제거하는 비용이 감소.
  3. 유지보수 용이
    • 데이터 공유를 통해 코드 간결화.

단점

  1. 복잡도 증가
    • 내부 상태와 외부 상태를 명확히 구분해야 해서 코드가 복잡해질 수 있음.
  2. 병목 현상
    • 공유된 리소스에 대한 접근이 많아지면 병목이 발생할 수 있음.

0개의 댓글