C++ Object Pool | "객체 풀" 가이드

C++ Object Pool | "객체 풀" 가이드

이 글의 핵심

C++ Object Pool에 대해 정리한 개발 블로그 글입니다. struct GameObject { int id; float x, y; void reset() { id = 0; x = y = 0.0f; } };

Object Pool이란?

객체 재사용 패턴

template<typename T>
class ObjectPool {
    std::vector<std::unique_ptr<T>> pool;
    std::vector<T*> available;
    
public:
    ObjectPool(size_t size) {
        for (size_t i = 0; i < size; ++i) {
            auto obj = std::make_unique<T>();
            available.push_back(obj.get());
            pool.push_back(std::move(obj));
        }
    }
    
    T* acquire() {
        if (available.empty()) return nullptr;
        T* obj = available.back();
        available.pop_back();
        return obj;
    }
    
    void release(T* obj) {
        available.push_back(obj);
    }
};

기본 사용

struct GameObject {
    int id;
    float x, y;
    
    void reset() {
        id = 0;
        x = y = 0.0f;
    }
};

int main() {
    ObjectPool<GameObject> pool(100);
    
    // 획득
    GameObject* obj = pool.acquire();
    obj->id = 1;
    obj->x = 10.0f;
    
    // 반환
    obj->reset();
    pool.release(obj);
}

실전 예시

예시 1: 게임 오브젝트

class Bullet {
public:
    float x, y;
    float vx, vy;
    bool active = false;
    
    void reset() {
        x = y = 0.0f;
        vx = vy = 0.0f;
        active = false;
    }
    
    void update(float dt) {
        if (active) {
            x += vx * dt;
            y += vy * dt;
        }
    }
};

class BulletPool {
    ObjectPool<Bullet> pool;
    
public:
    BulletPool(size_t size) : pool(size) {}
    
    Bullet* spawn(float x, float y, float vx, float vy) {
        Bullet* bullet = pool.acquire();
        if (bullet) {
            bullet->x = x;
            bullet->y = y;
            bullet->vx = vx;
            bullet->vy = vy;
            bullet->active = true;
        }
        return bullet;
    }
    
    void despawn(Bullet* bullet) {
        bullet->reset();
        pool.release(bullet);
    }
};

예시 2: RAII 래퍼

template<typename T>
class PooledObject {
    ObjectPool<T>* pool;
    T* obj;
    
public:
    PooledObject(ObjectPool<T>* p) : pool(p), obj(pool->acquire()) {}
    
    ~PooledObject() {
        if (obj) {
            pool->release(obj);
        }
    }
    
    PooledObject(const PooledObject&) = delete;
    PooledObject& operator=(const PooledObject&) = delete;
    
    PooledObject(PooledObject&& other) noexcept
        : pool(other.pool), obj(other.obj) {
        other.obj = nullptr;
    }
    
    T* get() const { return obj; }
    T& operator*() const { return *obj; }
    T* operator->() const { return obj; }
};

int main() {
    ObjectPool<GameObject> pool(100);
    
    {
        PooledObject<GameObject> obj{&pool};
        obj->id = 1;
    }  // 자동 반환
}

예시 3: 동적 확장

template<typename T>
class DynamicPool {
    std::vector<std::unique_ptr<T>> pool;
    std::stack<T*> available;
    size_t batchSize;
    
public:
    DynamicPool(size_t initial, size_t batch) : batchSize(batch) {
        expand(initial);
    }
    
    void expand(size_t count) {
        for (size_t i = 0; i < count; ++i) {
            auto obj = std::make_unique<T>();
            available.push(obj.get());
            pool.push_back(std::move(obj));
        }
    }
    
    T* acquire() {
        if (available.empty()) {
            expand(batchSize);
        }
        T* obj = available.top();
        available.pop();
        return obj;
    }
    
    void release(T* obj) {
        available.push(obj);
    }
};

예시 4: 타입별 풀

class PoolManager {
    std::unordered_map<std::type_index, std::unique_ptr<void, void(*)(void*)>> pools;
    
public:
    template<typename T>
    void createPool(size_t size) {
        auto pool = std::make_unique<ObjectPool<T>>(size);
        pools[typeid(T)] = {pool.release(),  {
            delete static_cast<ObjectPool<T>*>(p);
        }};
    }
    
    template<typename T>
    T* acquire() {
        auto it = pools.find(typeid(T));
        if (it == pools.end()) return nullptr;
        
        auto* pool = static_cast<ObjectPool<T>*>(it->second.get());
        return pool->acquire();
    }
    
    template<typename T>
    void release(T* obj) {
        auto it = pools.find(typeid(T));
        if (it != pools.end()) {
            auto* pool = static_cast<ObjectPool<T>*>(it->second.get());
            pool->release(obj);
        }
    }
};

장점

// 1. 빠른 할당
// - 미리 생성
// - new/delete 회피

// 2. 단편화 방지
// - 고정 크기

// 3. 캐시 친화적
// - 연속 메모리

// 4. 예측 가능
// - 할당 실패 없음

자주 발생하는 문제

문제 1: 초기화

// ❌ 상태 남음
GameObject* obj = pool.acquire();
obj->health = 100;
pool.release(obj);

GameObject* obj2 = pool.acquire();  // 같은 객체
// obj2->health == 100 (이전 상태)

// ✅ reset
obj->reset();
pool.release(obj);

문제 2: 풀 크기

// ❌ 풀 부족
ObjectPool<T> pool(10);

for (int i = 0; i < 100; ++i) {
    T* obj = pool.acquire();  // nullptr
}

// ✅ 충분한 크기 또는 동적 확장

문제 3: 수명

// ❌ 풀 소멸 후 사용
GameObject* obj;
{
    ObjectPool<GameObject> pool(100);
    obj = pool.acquire();
}  // pool 소멸

// obj 댕글링

// ✅ 풀 수명 보장

문제 4: 스레드 안전

// ❌ 스레드 안전 아님
ObjectPool<T> pool(100);

std::thread t1([&]() { pool.acquire(); });
std::thread t2([&]() { pool.acquire(); });  // 레이스

// ✅ 뮤텍스 또는 스레드 로컬

활용 패턴

// 1. 게임 오브젝트
ObjectPool<Bullet> bulletPool(1000);

// 2. 임시 객체
auto obj = pool.acquire();
// 사용
pool.release(obj);

// 3. RAII
PooledObject<T> obj{&pool};

// 4. 동적 확장
DynamicPool<T> pool(100, 50);

FAQ

Q1: Object Pool?

A: 객체 재사용 패턴.

Q2: 장점?

A:

  • 빠른 할당
  • 단편화 방지
  • 예측 가능

Q3: reset?

A: 필수. 상태 초기화.

Q4: 풀 크기?

A: 사용 패턴에 따라 결정.

Q5: 스레드 안전?

A: 뮤텍스 또는 스레드 로컬.

Q6: 학습 리소스는?

A:

  • “Game Programming Patterns”
  • “Effective C++”
  • cppreference.com

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

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

  • C++ Memory Pool | “메모리 풀” 가이드
  • C++ PMR | “다형 메모리 리소스” 가이드
  • C++ Stack Allocator | “스택 할당자” 가이드

관련 글

  • C++ Memory Pool |
  • 배열과 리스트 | 코딩 테스트 필수 자료구조 완벽 정리
  • C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
  • C++ ADL |
  • C++ Aggregate Initialization |