C++ Flyweight 패턴 완벽 가이드 | 공유로 메모리 절약하기

C++ Flyweight 패턴 완벽 가이드 | 공유로 메모리 절약하기

이 글의 핵심

C++ Flyweight 패턴 완벽 가이드. 공통 상태(intrinsic)를 공유하고 개별 상태(extrinsic)만 따로 두어 객체 수가 많을 때 메모리를 줄이는 구조 패턴, 실전 예제, 텍스트 렌더링, 게임 타일까지.

Flyweight 패턴이란? 왜 필요한가

공유·캐시와 맞닿은 구조 패턴은 구조 패턴 시리즈에서 Composite·Proxy와 함께 정리합니다.

문제 시나리오: 메모리 낭비

문제: 10,000개의 나무를 그릴 때 각 나무마다 텍스처를 복사하면 메모리가 폭발합니다.

// 나쁜 설계: 메모리 낭비
class Tree {
    Texture texture_;  // 10MB
    int x_, y_;        // 위치만 다름
};

std::vector<Tree> forest(10000);  // 10MB × 10000 = 100GB!

해결: Flyweight 패턴공통 상태(텍스처)를 공유하고 개별 상태(위치)만 따로 둡니다.

// 좋은 설계: Flyweight
class TreeType {  // 공유되는 intrinsic 상태
    Texture texture_;  // 10MB (1개만)
};

class Tree {  // extrinsic 상태
    TreeType* type_;  // 포인터만 (8 bytes)
    int x_, y_;       // 위치
};

std::vector<Tree> forest(10000);  // 10MB + (16 bytes × 10000) = 10.16MB
flowchart LR
    client["Client"]
    factory["FlyweightFactory"]
    flyweight["Flyweightbr/(TreeType)"]
    context["Contextbr/(Tree)"]
    
    client --> factory
    factory --> flyweight
    context --> flyweight
    client --> context

목차

  1. 기본 구조
  2. 텍스트 렌더링 예제
  3. 게임 타일 시스템
  4. 자주 발생하는 문제와 해결법
  5. 프로덕션 패턴
  6. 완전한 예제: 파티클 시스템

1. 기본 구조

#include <unordered_map>
#include <string>
#include <memory>
#include <iostream>

// 공유되는 내부 상태 (intrinsic)
struct Glyph {
    char ch;
    int width, height;
    std::string bitmap;  // 실제로는 큰 데이터
    
    Glyph(char c, int w, int h) : ch(c), width(w), height(h) {
        bitmap = std::string(w * h, '#');  // 가상 비트맵
        std::cout << "Creating Glyph '" << ch << "'\n";
    }
};

class GlyphFactory {
    std::unordered_map<char, std::shared_ptr<Glyph>> cache_;
public:
    std::shared_ptr<Glyph> get(char c) {
        auto it = cache_.find(c);
        if (it != cache_.end()) {
            std::cout << "Reusing Glyph '" << c << "'\n";
            return it->second;
        }
        auto g = std::make_shared<Glyph>(c, 8, 16);
        cache_[c] = g;
        return g;
    }
    
    size_t getCacheSize() const { return cache_.size(); }
};

// 외부 상태(extrinsic): 위치 등 — 호출 시 전달
void draw(std::shared_ptr<Glyph> g, int x, int y) {
    std::cout << "Draw '" << g->ch << "' at (" << x << "," << y << ")\n";
}

int main() {
    GlyphFactory factory;
    
    std::string text = "HELLO WORLD";
    int x = 0;
    for (char c : text) {
        if (c == ' ') { x += 8; continue; }
        auto glyph = factory.get(c);
        draw(glyph, x, 0);
        x += 8;
    }
    
    std::cout << "\nTotal unique glyphs: " << factory.getCacheSize() << '\n';
    // 출력:
    // Creating Glyph 'H'
    // Draw 'H' at (0,0)
    // Creating Glyph 'E'
    // Draw 'E' at (8,0)
    // Reusing Glyph 'L'
    // Draw 'L' at (16,0)
    // Reusing Glyph 'L'
    // Draw 'L' at (24,0)
    // ...
    // Total unique glyphs: 7
    
    return 0;
}

2. 텍스트 렌더링 예제

폰트 Flyweight

#include <unordered_map>
#include <memory>
#include <iostream>
#include <string>

// Flyweight: 공유되는 폰트 데이터
class Font {
    std::string name_;
    int size_;
    std::string fontData_;  // 실제로는 수 MB
public:
    Font(std::string name, int size) 
        : name_(std::move(name)), size_(size) {
        fontData_ = std::string(1000000, 'F');  // 1MB 가상 데이터
        std::cout << "Loading font: " << name_ << " " << size_ << "pt\n";
    }
    
    void render(char c, int x, int y) const {
        std::cout << "Render '" << c << "' with " << name_ 
                  << " at (" << x << "," << y << ")\n";
    }
};

class FontFactory {
    std::unordered_map<std::string, std::shared_ptr<Font>> fonts_;
    
    std::string makeKey(const std::string& name, int size) {
        return name + "_" + std::to_string(size);
    }
public:
    std::shared_ptr<Font> getFont(const std::string& name, int size) {
        std::string key = makeKey(name, size);
        auto it = fonts_.find(key);
        if (it != fonts_.end()) return it->second;
        
        auto font = std::make_shared<Font>(name, size);
        fonts_[key] = font;
        return font;
    }
};

// Context: extrinsic 상태를 가진 문자
class Character {
    char ch_;
    int x_, y_;
    std::shared_ptr<Font> font_;  // Flyweight 참조
public:
    Character(char ch, int x, int y, std::shared_ptr<Font> font)
        : ch_(ch), x_(x), y_(y), font_(std::move(font)) {}
    
    void draw() const {
        font_->render(ch_, x_, y_);
    }
};

int main() {
    FontFactory factory;
    
    auto arial12 = factory.getFont("Arial", 12);
    auto arial12_2 = factory.getFont("Arial", 12);  // 재사용
    auto times14 = factory.getFont("Times", 14);
    
    std::vector<Character> text;
    text.emplace_back('H', 0, 0, arial12);
    text.emplace_back('i', 10, 0, arial12_2);  // 같은 폰트
    text.emplace_back('!', 20, 0, times14);
    
    for (const auto& ch : text)
        ch.draw();
    
    // 출력:
    // Loading font: Arial 12pt
    // Loading font: Times 14pt
    // Render 'H' with Arial at (0,0)
    // Render 'i' with Arial at (10,0)
    // Render '!' with Times at (20,0)
    
    return 0;
}

핵심: 같은 폰트는 한 번만 로드되고, 각 문자는 위치만 다르게 가집니다.


3. 게임 타일 시스템

타일맵 Flyweight

#include <unordered_map>
#include <memory>
#include <iostream>
#include <vector>

// Flyweight: 공유되는 타일 타입
class TileType {
    std::string name_;
    std::string texture_;  // 실제로는 큰 텍스처
    bool walkable_;
public:
    TileType(std::string name, std::string texture, bool walkable)
        : name_(std::move(name)), texture_(std::move(texture)), walkable_(walkable) {
        std::cout << "Loading tile type: " << name_ << '\n';
    }
    
    void render(int x, int y) const {
        std::cout << "[" << name_[0] << "]";
    }
    
    bool isWalkable() const { return walkable_; }
};

class TileFactory {
    std::unordered_map<std::string, std::shared_ptr<TileType>> types_;
public:
    std::shared_ptr<TileType> getTileType(const std::string& name) {
        auto it = types_.find(name);
        if (it != types_.end()) return it->second;
        
        // 타일 타입별 속성 정의
        bool walkable = (name != "wall");
        auto type = std::make_shared<TileType>(name, name + ".png", walkable);
        types_[name] = type;
        return type;
    }
};

// Context: extrinsic 상태를 가진 타일
class Tile {
    int x_, y_;
    std::shared_ptr<TileType> type_;  // Flyweight 참조
public:
    Tile(int x, int y, std::shared_ptr<TileType> type)
        : x_(x), y_(y), type_(std::move(type)) {}
    
    void render() const {
        type_->render(x_, y_);
    }
    
    bool isWalkable() const {
        return type_->isWalkable();
    }
};

class TileMap {
    std::vector<std::vector<Tile>> tiles_;
    TileFactory factory_;
public:
    TileMap(int width, int height) {
        // 간단한 맵 생성
        for (int y = 0; y < height; ++y) {
            std::vector<Tile> row;
            for (int x = 0; x < width; ++x) {
                std::string type = (x == 0 || x == width-1 || y == 0 || y == height-1) 
                    ? "wall" : "grass";
                row.emplace_back(x, y, factory_.getTileType(type));
            }
            tiles_.push_back(std::move(row));
        }
    }
    
    void render() const {
        for (const auto& row : tiles_) {
            for (const auto& tile : row)
                tile.render();
            std::cout << '\n';
        }
    }
};

int main() {
    TileMap map(10, 5);
    map.render();
    
    // 출력:
    // Loading tile type: wall
    // Loading tile type: grass
    // [W][W][W][W][W][W][W][W][W][W]
    // [W][G][G][G][G][G][G][G][G][W]
    // [W][G][G][G][G][G][G][G][G][W]
    // [W][G][G][G][G][G][G][G][G][W]
    // [W][W][W][W][W][W][W][W][W][W]
    
    return 0;
}

핵심: 50개 타일이 있어도 타일 타입은 2개(wall, grass)만 로드됩니다.


4. 자주 발생하는 문제와 해결법

문제 1: Flyweight 수정

// ❌ 나쁜 예: 공유 객체 수정
auto glyph = factory.get('A');
glyph->width = 20;  // 모든 'A'가 영향받음!

해결: Flyweight는 불변(immutable)으로 만드세요.

// ✅ 좋은 예: const 메서드만
class Glyph {
    const int width_, height_;
public:
    int getWidth() const { return width_; }  // const만
};

문제 2: 과도한 extrinsic 상태

// ❌ 나쁜 예: extrinsic이 너무 많음
void draw(Glyph* g, int x, int y, int r, int g, int b, float rotation, float scale) {
    // 매번 8개 인자 전달
}

해결: extrinsic 상태를 구조체로 묶으세요.

// ✅ 좋은 예
struct RenderContext {
    int x, y;
    Color color;
    float rotation, scale;
};

void draw(Glyph* g, const RenderContext& ctx);

문제 3: 메모리 누수

// ❌ 나쁜 예: Factory가 계속 커짐
class Factory {
    std::unordered_map<std::string, Flyweight*> cache_;  // 영원히 유지
};

해결: LRU 캐시나 weak_ptr을 사용하세요.

// ✅ 좋은 예: weak_ptr로 자동 정리
class Factory {
    std::unordered_map<std::string, std::weak_ptr<Flyweight>> cache_;
public:
    std::shared_ptr<Flyweight> get(const std::string& key) {
        auto it = cache_.find(key);
        if (it != cache_.end()) {
            if (auto sp = it->second.lock())
                return sp;
        }
        auto fw = std::make_shared<Flyweight>(key);
        cache_[key] = fw;
        return fw;
    }
};

5. 프로덕션 패턴

패턴 1: Object Pool과 조합

class FlyweightPool {
    std::vector<std::unique_ptr<Flyweight>> pool_;
    std::unordered_map<std::string, Flyweight*> index_;
public:
    Flyweight* get(const std::string& key) {
        auto it = index_.find(key);
        if (it != index_.end()) return it->second;
        
        pool_.push_back(std::make_unique<Flyweight>(key));
        Flyweight* ptr = pool_.back().get();
        index_[key] = ptr;
        return ptr;
    }
};

패턴 2: Composite과 조합

class CompositeFlyweight : public Flyweight {
    std::vector<Flyweight*> children_;
public:
    void add(Flyweight* fw) { children_.push_back(fw); }
    void render(int x, int y) override {
        for (auto* child : children_)
            child->render(x, y);
    }
};

6. 완전한 예제: 파티클 시스템

#include <memory>
#include <vector>
#include <iostream>
#include <string>
#include <unordered_map>

// Flyweight: 공유되는 파티클 타입
class ParticleType {
    std::string texture_;
    float mass_;
    float friction_;
public:
    ParticleType(std::string texture, float mass, float friction)
        : texture_(std::move(texture)), mass_(mass), friction_(friction) {
        std::cout << "Loading particle type: " << texture_ << '\n';
    }
    
    void render(float x, float y, float vx, float vy) const {
        std::cout << texture_[0] << "(" << x << "," << y << ") ";
    }
    
    float getMass() const { return mass_; }
    float getFriction() const { return friction_; }
};

class ParticleFactory {
    std::unordered_map<std::string, std::shared_ptr<ParticleType>> types_;
public:
    std::shared_ptr<ParticleType> getType(const std::string& name) {
        auto it = types_.find(name);
        if (it != types_.end()) return it->second;
        
        // 파티클 타입별 속성
        float mass = (name == "smoke") ? 0.1f : 1.0f;
        float friction = (name == "fire") ? 0.05f : 0.1f;
        
        auto type = std::make_shared<ParticleType>(name, mass, friction);
        types_[name] = type;
        return type;
    }
};

// Context: extrinsic 상태를 가진 파티클
class Particle {
    float x_, y_;
    float vx_, vy_;
    std::shared_ptr<ParticleType> type_;
public:
    Particle(float x, float y, float vx, float vy, std::shared_ptr<ParticleType> type)
        : x_(x), y_(y), vx_(vx), vy_(vy), type_(std::move(type)) {}
    
    void update(float dt) {
        x_ += vx_ * dt;
        y_ += vy_ * dt;
        vx_ *= (1.0f - type_->getFriction());
        vy_ *= (1.0f - type_->getFriction());
    }
    
    void render() const {
        type_->render(x_, y_, vx_, vy_);
    }
};

class ParticleSystem {
    std::vector<Particle> particles_;
    ParticleFactory factory_;
public:
    void emit(const std::string& type, float x, float y, float vx, float vy) {
        particles_.emplace_back(x, y, vx, vy, factory_.getType(type));
    }
    
    void update(float dt) {
        for (auto& p : particles_)
            p.update(dt);
    }
    
    void render() const {
        for (const auto& p : particles_)
            p.render();
        std::cout << '\n';
    }
};

int main() {
    ParticleSystem system;
    
    // 1000개 파티클 생성 (타입은 3개만)
    for (int i = 0; i < 300; ++i)
        system.emit("fire", i * 0.1f, 0, 1, 2);
    for (int i = 0; i < 400; ++i)
        system.emit("smoke", i * 0.1f, 10, 0.5f, 1);
    for (int i = 0; i < 300; ++i)
        system.emit("spark", i * 0.1f, 20, 2, 3);
    
    std::cout << "\n=== Simulating ===\n";
    system.update(0.016f);
    system.render();
    
    // 출력:
    // Loading particle type: fire
    // Loading particle type: smoke
    // Loading particle type: spark
    //
    // === Simulating ===
    // F(0.016,0.032) F(0.116,0.032) ... (1000개 파티클, 타입은 3개만 로드)
    
    return 0;
}

정리

항목설명
목적공통 상태 공유로 객체 수가 많을 때 메모리 절감
장점메모리 사용 대폭 감소, 캐시 친화적, 객체 생성 비용 절감
단점extrinsic 전달 오버헤드, 코드 복잡도 증가, 스레드 안전성 고려 필요
사용 시기텍스트 렌더링, 게임 타일, 파티클, 아이콘 등 반복 객체 다수

관련 글: 메모리 풀, Object Pool, Adapter 패턴, Composite 패턴.

한 줄 요약: Flyweight 패턴으로 글리프·타일·파티클처럼 반복되는 데이터를 공유해 메모리를 수십~수백 배 줄일 수 있습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ Object Pool | “객체 풀” 가이드
  • C++ Memory Pool | “메모리 풀” 가이드
  • C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
  • C++ Composite 패턴 완벽 가이드 | 트리 구조를 동일 인터페이스로 다루기

이 글에서 다루는 키워드 (관련 검색어)

C++, Flyweight, design pattern, structural, memory, sharing, optimization 등으로 검색하시면 이 글이 도움이 됩니다.

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. C++ Flyweight 패턴 완벽 가이드. 공통 상태(intrinsic)를 공유하고 개별 상태(extrinsic)만 따로 두어 객체 수가 많을 때 메모리를 줄이는 구조 패턴, 실전 예제, 텍스트 렌더링, 게임 타일… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


관련 글

  • C++ Bridge 패턴 완벽 가이드 | 구현과 추상화 분리로 확장성 높이기
  • C++ Cache Optimization |
  • C++ Composite 패턴 완벽 가이드 | 트리 구조를 동일 인터페이스로 다루기
  • C++ Facade 패턴 완벽 가이드 | 복잡한 서브시스템을 하나의 간단한 인터페이스로
  • C++ Memory Pool |