콘텐츠로 이동

C++ 구조 패턴 — Adapter·Decorator·Composite·Facade·Bridge·Flyweight

구조 패턴(Structural Pattern)은 클래스와 객체를 더 큰 구조로 조합하는 방법을 다룹니다. 기존 코드를 수정하지 않고 기능을 확장하거나 인터페이스를 변환합니다.


호환되지 않는 인터페이스를 연결합니다. 서드파티 라이브러리나 레거시 코드를 현재 시스템에 연결할 때 사용합니다.

// 기존 물리 엔진 인터페이스 (변경 불가)
class PhysicsEngine
{
public:
void ApplyForceXYZ(float fx, float fy, float fz)
{
std::cout << "Force: " << fx << ", " << fy << ", " << fz << "\n";
}
};
// 게임에서 사용하는 벡터 타입
struct Vec3 { float x, y, z; };
// 게임이 원하는 인터페이스
class IPhysicsComponent
{
public:
virtual void ApplyForce(Vec3 force) = 0;
virtual ~IPhysicsComponent() = default;
};
// 어댑터: PhysicsEngine을 IPhysicsComponent로 변환
class PhysicsAdapter : public IPhysicsComponent
{
public:
explicit PhysicsAdapter(PhysicsEngine& engine) : _engine(engine) {}
void ApplyForce(Vec3 force) override
{
_engine.ApplyForceXYZ(force.x, force.y, force.z);
}
private:
PhysicsEngine& _engine;
};
// 사용: 게임 코드는 IPhysicsComponent만 알면 됨
PhysicsEngine engine;
PhysicsAdapter adapter(engine);
IPhysicsComponent& physics = adapter;
physics.ApplyForce({0.0f, 9.8f, 0.0f});

객체에 동적으로 기능을 추가합니다. 상속 없이 조합으로 기능을 확장합니다.

// 기본 컴포넌트 인터페이스
class IWeapon
{
public:
virtual int GetDamage() const = 0;
virtual std::string GetName() const = 0;
virtual ~IWeapon() = default;
};
// 기본 무기
class BasicSword : public IWeapon
{
public:
int GetDamage() const override { return 10; }
std::string GetName() const override { return "Sword"; }
};
// 데코레이터 기반 클래스
class WeaponDecorator : public IWeapon
{
public:
explicit WeaponDecorator(std::unique_ptr<IWeapon> weapon)
: _weapon(std::move(weapon)) {}
protected:
std::unique_ptr<IWeapon> _weapon;
};
// 불꽃 인챈트 데코레이터
class FireEnchant : public WeaponDecorator
{
public:
using WeaponDecorator::WeaponDecorator;
int GetDamage() const override
{
return _weapon->GetDamage() + 15; // 화염 피해 추가
}
std::string GetName() const override
{
return _weapon->GetName() + " of Fire";
}
};
// 독 인챈트 데코레이터
class PoisonEnchant : public WeaponDecorator
{
public:
using WeaponDecorator::WeaponDecorator;
int GetDamage() const override
{
return _weapon->GetDamage() + 8;
}
std::string GetName() const override
{
return _weapon->GetName() + " of Poison";
}
};
// 데코레이터 체인
auto weapon = std::make_unique<BasicSword>();
// Sword(10) → Fire(+15) → Poison(+8) = 33
auto enchanted = std::make_unique<PoisonEnchant>(
std::make_unique<FireEnchant>(std::move(weapon))
);
std::cout << enchanted->GetName() << "\n"; // Sword of Fire of Poison
std::cout << enchanted->GetDamage() << "\n"; // 33

개별 객체와 객체 집합을 동일하게 처리합니다. 트리 구조(씬 그래프, UI 계층)에 적합합니다.

// 컴포넌트 인터페이스
class SceneNode
{
public:
virtual void Render() const = 0;
virtual void AddChild(std::shared_ptr<SceneNode>) {}
virtual ~SceneNode() = default;
std::string name;
explicit SceneNode(std::string n) : name(std::move(n)) {}
};
// Leaf: 자식 없음
class MeshNode : public SceneNode
{
public:
using SceneNode::SceneNode;
void Render() const override
{
std::cout << " Render mesh: " << name << "\n";
}
};
// Composite: 자식 보유
class GroupNode : public SceneNode
{
public:
using SceneNode::SceneNode;
void AddChild(std::shared_ptr<SceneNode> child) override
{
_children.push_back(std::move(child));
}
void Render() const override
{
std::cout << "Group: " << name << "\n";
for (const auto& child : _children)
child->Render();
}
private:
std::vector<std::shared_ptr<SceneNode>> _children;
};
// 씬 그래프 구성
auto root = std::make_shared<GroupNode>("Root");
auto body = std::make_shared<GroupNode>("Character");
auto weapon = std::make_shared<MeshNode>("Sword");
auto terrain = std::make_shared<MeshNode>("Terrain");
body->AddChild(std::make_shared<MeshNode>("Torso"));
body->AddChild(std::make_shared<MeshNode>("Head"));
body->AddChild(weapon);
root->AddChild(body);
root->AddChild(terrain);
// 루트에서 Render 호출 → 전체 트리 순회
root->Render();

복잡한 서브시스템에 단순화된 인터페이스를 제공합니다.

// 복잡한 서브시스템
class AudioSystem
{
public:
void Initialize() { std::cout << "Audio init\n"; }
void LoadBank(const std::string& bank) { std::cout << "Load bank: " << bank << "\n"; }
void PlayEvent(const std::string& event) { std::cout << "Play: " << event << "\n"; }
};
class PhysicsSystem
{
public:
void Initialize(float gravity) { std::cout << "Physics init, gravity=" << gravity << "\n"; }
void SetCollisionGroups(int groups) { }
};
class RenderSystem
{
public:
void Initialize(int width, int height) { std::cout << "Render " << width << "x" << height << "\n"; }
void SetShadowQuality(int quality) { }
};
// 파사드: 엔진 초기화를 단일 메서드로
class GameEngineFacade
{
public:
void Initialize()
{
_audio.Initialize();
_audio.LoadBank("Master");
_physics.Initialize(-9.8f);
_physics.SetCollisionGroups(8);
_render.Initialize(1920, 1080);
_render.SetShadowQuality(2);
}
void PlaySound(const std::string& event) { _audio.PlayEvent(event); }
private:
AudioSystem _audio;
PhysicsSystem _physics;
RenderSystem _render;
};
// 클라이언트: 복잡성을 모름
GameEngineFacade engine;
engine.Initialize();
engine.PlaySound("explosion");

객체에 대한 접근을 제어하는 대리자를 제공합니다. 지연 로딩, 캐싱, 접근 제어에 사용합니다.

class ITexture
{
public:
virtual void Bind() = 0;
virtual ~ITexture() = default;
};
class Texture : public ITexture
{
public:
explicit Texture(const std::string& path)
{
std::cout << "Loading texture: " << path << "\n";
// 실제 로딩 로직
}
void Bind() override { std::cout << "Bind texture\n"; }
};
// 지연 로딩 프록시
class LazyTextureProxy : public ITexture
{
public:
explicit LazyTextureProxy(std::string path) : _path(std::move(path)) {}
void Bind() override
{
// 실제 Bind 호출 시 처음 한 번만 로드
if (!_texture)
_texture = std::make_unique<Texture>(_path);
_texture->Bind();
}
private:
std::string _path;
std::unique_ptr<Texture> _texture; // 필요할 때까지 null
};
// 사용: 생성 시 로딩 없음
auto tex = std::make_unique<LazyTextureProxy>("large_texture.png");
std::cout << "Proxy created (not loaded yet)\n";
// 처음 Bind 호출 시 로딩 발생
tex->Bind(); // "Loading texture: large_texture.png" + "Bind texture"
tex->Bind(); // 두 번째 호출: 로딩 없이 바로 Bind

추상화(Abstraction)와 구현(Implementation)을 별도의 계층으로 분리해 독립적으로 확장할 수 있게 합니다. “상속 대신 구성”의 대표적 예입니다.

// 구현 계층 인터페이스 (플랫폼별 렌더러)
class IRenderer
{
public:
virtual ~IRenderer() = default;
virtual void DrawRect(float x, float y, float w, float h) = 0;
virtual void DrawText(const std::string& text, float x, float y) = 0;
};
// 구체 구현: DirectX
class DirectXRenderer : public IRenderer
{
public:
void DrawRect(float x, float y, float w, float h) override
{ std::cout << "DX DrawRect\n"; }
void DrawText(const std::string& text, float x, float y) override
{ std::cout << "DX DrawText: " << text << "\n"; }
};
// 구체 구현: OpenGL
class OpenGLRenderer : public IRenderer
{
public:
void DrawRect(float x, float y, float w, float h) override
{ std::cout << "GL DrawRect\n"; }
void DrawText(const std::string& text, float x, float y) override
{ std::cout << "GL DrawText: " << text << "\n"; }
};
// 추상화 계층: UI 컴포넌트 (렌더러와 독립적으로 확장 가능)
class UIWidget
{
protected:
IRenderer& _renderer; // Bridge: 구현 계층 참조
public:
explicit UIWidget(IRenderer& renderer) : _renderer(renderer) {}
virtual ~UIWidget() = default;
virtual void Draw() = 0;
};
// 구체 추상화: 버튼 (렌더러 종류와 무관)
class Button : public UIWidget
{
float _x, _y, _w, _h;
std::string _label;
public:
Button(IRenderer& renderer, float x, float y, float w, float h, std::string label)
: UIWidget(renderer), _x(x), _y(y), _w(w), _h(h), _label(std::move(label)) {}
void Draw() override
{
_renderer.DrawRect(_x, _y, _w, _h);
_renderer.DrawText(_label, _x + 5, _y + 5);
}
};
// 장점: UI 클래스와 렌더러 클래스를 각각 독립적으로 추가 가능
// 플랫폼 전환(DX->Vulkan)해도 UI 코드 수정 불필요
DirectXRenderer dxRenderer;
Button btn(dxRenderer, 100, 200, 80, 30, "Play");
btn.Draw();

수천 개의 유사한 객체에서 공유 가능한 불변 데이터를 분리해 메모리를 절약합니다.

// 공유되는 불변 데이터 (Intrinsic State)
struct TreeType
{
std::string meshPath;
std::string texturePath;
float scale;
// 수천 그루의 나무가 이 데이터를 공유
};
// 인스턴스별 가변 데이터 (Extrinsic State)
struct TreeInstance
{
const TreeType* type; // 공유 참조 (포인터만 보관)
glm::vec3 position; // 인스턴스별 고유 위치
float rotation; // 인스턴스별 고유 회전
float health; // 인스턴스별 고유 체력
};
// Flyweight Factory
class TreeTypeFactory
{
std::unordered_map<std::string, TreeType> _types;
public:
const TreeType* GetOrCreate(const std::string& name,
const std::string& mesh,
const std::string& tex,
float scale)
{
auto [it, inserted] = _types.try_emplace(name);
if (inserted)
it->second = { mesh, tex, scale };
return &it->second;
}
};
// 사용: 나무 10,000그루 생성
// TreeType은 3~5개, TreeInstance는 10,000개
TreeTypeFactory factory;
const TreeType* oak = factory.GetOrCreate("oak", "oak.mesh", "oak.png", 1.0f);
const TreeType* pine = factory.GetOrCreate("pine", "pine.mesh", "pine.png", 1.5f);
std::vector<TreeInstance> forest;
for (int i = 0; i < 7000; ++i)
forest.push_back({oak, {rand()%1000.f, 0, rand()%1000.f}, 0.f, 100.f});
for (int i = 0; i < 3000; ++i)
forest.push_back({pine, {rand()%1000.f, 0, rand()%1000.f}, 0.f, 100.f});
// TreeType 3개 × 수십 바이트 + TreeInstance 10000개 × 28바이트
// vs 모든 데이터를 중복 저장: 10000개 × (수십 바이트 + 28바이트)
// 메모리 절감: 수십 MB -> 수백 KB

패턴의도게임 활용
Adapter인터페이스 변환서드파티 물리/오디오 엔진 연결
Decorator동적 기능 추가무기 인챈트, 버프 스택
Composite트리 구조 균일 처리씬 그래프, UI 계층
Facade복잡성 은닉엔진 서브시스템 초기화
Proxy접근 제어지연 로딩, 캐싱
Bridge추상화·구현 분리멀티플랫폼 렌더러, 크로스플랫폼 UI
Flyweight공유 데이터 최적화나무·풀·파티클 등 대량 오브젝트

구조 패턴은 기존 코드를 수정하지 않고 새로운 기능을 조합할 수 있게 합니다. 특히 Flyweight는 오픈 월드 게임의 대량 환경 오브젝트(풀, 나무, 돌)에 필수적이며, Bridge는 멀티플랫폼 엔진에서 추상화 계층을 깔끔하게 유지하게 해줍니다.