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. 기본 구조
#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 |