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 |