[C/C++] 오브젝트 풀 패턴(Object Pool Pattern)

할랑말랑·2026년 3월 20일

C/C++

목록 보기
32/45

오브젝트 풀 패턴(Object Pool Pattern)

객체를 매번 새로 생성하고 파괴하는 대신, 미리 만들어 놓은 객체들을 '풀(Pool)'이라는 저장소에 보관해 두었다가 필요할 때 재사용하는 소프트웨어 생성 디자인 패턴

1. Object Pool의 3단계

  • 준비 (Initialization) : 프로그램 시작 시 또는 필요할 때 일정 수의 객체를 미리 생성하여 풀에 저장
  • 사용 (Runtime) : 새로운 객체가 필요하면 생성(New)하는 대신 풀에서 비활성 상태인 객체를 꺼내 활성화하여 사용
  • 재사용 (Recycle) : 객체 사용이 끝나면 파괴(Destroy)하지 않고 비활성한다.

2. 장점

  • 성능 최적화 : Instantiate(생성) 및 Destroy(파괴) 비용이 없어 성능이 크게 향상
  • 메모리 단편화 방지
  • 캐시 효율 증가 : 객체들이 메모리에 연속 배치
  • 객체 수명 관리 쉬움
  • 초기화 비용 분산 : 초기화 때 한 번만 생성

3. 단점

  • 초기 메모리 사용량 큼 : 시작할 때 많은 메모리 차지
  • 로딩 시간 증가
  • 설계 복잡도 : 객체의 상태를 직접 관리해야 하므로 코드의 복잡성이 올라간다

간단한 몬스터 스폰 예시

  • Enemy : Concrete objec Pool에 적용할 객체
    Spawn : 풀에서 받은 객체를 스폰하기 위한 메서드
    Reset : 객체의 데이터 초기화를 위한 메서드
    Hit : 공격받았을때 처리할 메서드
    Dead : 체력이 0이하가되서 pool에 반납하기위한 메서드
  • EnemyPool : 일정수의 Enemy객체를 관리할 객체
    Return : 객체 사용후 반납 위한 메서드
    GetObject : 사용 가능한 객체를 넘기는 메서드
    PrintStatus : Pool 상태를 출력하기위한 메서드
    CollectDeadEnemies : 사용후 회수가 안된 객체를 재사용하기위해 확인하는 메서드
#include <iostream>
#include <vector>
#include <string>

class EnemyPool;

// 풀에서 관리될 객체 클래스
class Enemy
{
private:
    std::string name;
    int hp;
    bool active;
    EnemyPool* pool; // 자신을 관리하는 풀에 대한 포인터
    bool isdead;

public:
    Enemy(const std::string& _name) : name(_name), pool(nullptr)
    {
        Reset();
    }

    void Reset()
    {
        hp = 100;
        isdead = false;
        active = false;
    }

    bool GetActive() const { return active; }
    void SetActive(const bool _active) { active = _active; }
    std::string GetName() const { return name; }
    void SetPool(EnemyPool* _pool) { pool = _pool; }
    int GetHP() const { return hp; }

    void Spawn()
    {
        std::cout << name << " 스폰" << std::endl;
        active = true;
        isdead = false;
    }

    void Hit(int _damage)
    {
        if (CanBeAttacked())
        {
            std::cout << _damage << " 데미지를 받음." << std::endl;
            hp -= _damage;
            if (hp > 0)
            {
                std::cout << name << "의 hp : " << hp << " 남음" << std::endl;
            }
            else
            {
                Dead();
            }
        }
    }

    bool CanBeAttacked() const
    {
        if (!active) return false;
        if (hp <= 0) return false;
        if (isdead) return false;
        return true;
    }

    void Dead();
};

// 오브젝트 풀 클래스
class EnemyPool
{
private:
    std::vector<Enemy*> pool;

public:
    EnemyPool(int poolsize = 10)
    {
        for (int i = 0; i < poolsize; i++)
        {
            pool.push_back(new Enemy("슬라임" + std::to_string(i)));
        }
    }

    ~EnemyPool()
    {
        for (int i = 0; i < pool.size(); i++)
        {
            delete pool[i];
        }
        pool.clear();
    }

    Enemy* GetObject()
    {
        for (Enemy* enemy : pool)
        {
            if (!enemy->GetActive())
            {
                enemy->SetPool(this);
                enemy->SetActive(true);
                return enemy;
            }
        }
        return nullptr;
    }

    void Return(Enemy* enemy)
    {
        if (enemy == nullptr) return;
        enemy->SetActive(false);
        enemy->Reset();
    }

    // 이 메서드는 이제 Enemy::Dead()에 의해 간접적으로 호출되므로, 외부에서 직접 호출할 필요가 줄어듦
    void CollectDeadEnemies()
    {
        for (Enemy* enemy : pool)
        {
            if (!enemy->GetActive() && enemy->GetHP() <= 0)
            {
                std::cout << enemy->GetName() << " 수거" << std::endl;
                Return(enemy);
            }
        }
    }

    void PrintStatus() const
    {
        int activeCount = 0;
        int deadCount = 0;
        for (Enemy* enemy : pool)
        {
            if (enemy->GetActive())
            {
                if (enemy->GetHP() > 0)
                {
                    activeCount++;
                }
                else
                {
                    deadCount++;
                }
            }
        }
        std::cout << "==[Pool Info]==" << std::endl;
        std::cout << "Size : " << pool.size() << std::endl;
        std::cout << "Active : " << activeCount << std::endl;
        std::cout << "Dead : " << deadCount << std::endl;
        std::cout << "Inactive : " << pool.size() - activeCount - deadCount << std::endl;
    }
};

// Enemy::Dead() 메서드의 정의
void Enemy::Dead()
{
    std::cout << name << " 사망" << std::endl;
    isdead = true;
    if (pool)
    {
        // 자신이 속한 풀에 스스로를 반납
        pool->Return(this);
    }
}

int main()
{
    EnemyPool pool(10);
    Enemy* enemy1 = pool.GetObject();
    enemy1->Spawn();
    pool.PrintStatus();
    enemy1->Hit(50);
    enemy1->Hit(50); // 이 메서드 안에서 Dead()가 호출되고, Dead() 안에서 pool->Return()이 호출됨
    pool.PrintStatus();

    return 0;
}

  • 자기 반납(Self-Returning) 객체 : Enemy 객체는 더 이상 수동적인 데이터 덩어리가 아니라, 자신의 상태에 따라 능동적으로 풀과 상호작용하는 주체
  • 강한 결합(Tight Coupling) : 이 방식은 코드를 단순화하고 객체의 행동을 명확하게 만들 수 있지만, Enemy 클래스가 EnemyPool 클래스에 직접적으로 의존하게 되는 단점이 있다. 즉, Enemy는 자신을 관리해 줄 EnemyPool 없이는 온전한 역할을 수행하기 어렵다.
  • 책임의 이동 객체를 풀에 반납하는 책임이 풀 관리자(EnemyPool)에서 객체 자신(Enemy)에게로 일부 이동했다.

0개의 댓글