리얼 개발

[디자인 패턴] ObjectPool Pattern 본문

디자인 패턴

[디자인 패턴] ObjectPool Pattern

econo-my 2025. 1. 6. 22:56

객체 풀 패턴 (ObjectPool Pattern) 이란

생성 디자인 패턴으로, 필요에 따라 객체를 할당하고 삭제하는

대신 사용할 준비가 된 일련의 초기화된 객체 "풀"을 사용한다.

 

클라이언트가 객체를 사용해야 하는 상황이 되면 미리 객체를 생성해 모아놓은 "풀"에 객체를 요청하고, 클라이언트가 객체 사용을 완료하면 삭제하는 대신 "풀"에 반환한다.

 

이러한 객체 풀 패턴은 게임 개발에서 자주 사용하곤 한다. (다른 곳에서는 어떤지 잘 모르겠음)

 

 

패턴 사용 상황

내가 탄막 게임을 만드는 개발자라고 생각해보자. 화면을 가득 채우고 있는 탄환들은 어떻게 생성해야 할까?

탄환은 계속해서 이동해야하며, 플레이어가 조종하는 캐릭터와 충돌했을 때 충돌 판정이 필요하다.

이런 탄환들은 하나의 객체로 화면에 생성되고 삭제됨을 반복할 것이다.

ex) 플레이어가 공격을 할 때 플레이어 위치에 탄환 생성 및 탄환이 화면에서 벗어났을 때 탄환 삭제

 

오브젝트를 생성하기 위해 메모리에 올리고 삭제하기 위해 메모리에서 내리고,,, CPU는 탄환을 생성하고 삭제하는 것 말고도 할 일이 많은데 시간을 뺏기게 될 것이다. 거기다 빈번하게 메모리에 할당 및 삭제하는 과정은 메모리 단편화의 위험성도 커진다.

 

그래서 객체를 미리 생성해놓고(풀) 빌려쓰고 반환하자! 라는 의미로 객체 풀 패턴을 사용한다.

 

 

장점 / 단점

장점

앞서 말한 문제점을 걱정하지 않아도 된다.

 

단점

미리 생성해놓기에 많은 객체들이 메모리를 차지하고 있는다.

 

 

C++ 예시 코드

 

1. 객체로 사용할 사각형과 삼각형 클래스

    게임 개발을 하다보니 빈번하게 생성, 삭제할 객체들은 보통 상위 클래스로 묶어 다형성으로 관리하는 일이 많았다.

    그래서 예시로도 다형성으로 관리할 수 있도록 상위 클래스인 Diagram을 상속받고 있다.

#pragma once
#ifndef __DIAGRAM_H__
#define __DIAGRAM_H__
#include <iostream>

class Diagram
{
public:
    virtual void Use() = 0;
};

class Rectangle : public Diagram
{
public:
    Rectangle()
    {
        std::cout << "사각형 클래스 생성!" << std::endl;
    }

    ~Rectangle()
    {
        std::cout << "사각형 클래스 제거!" << std::endl;
    }

    virtual void Use() override
    {
        std::cout << "사각형 클래스 사용" << std::endl;
    }
};

class Triangle : public Diagram
{
public:
    Triangle()
    {
        std::cout << "삼각형 클래스 생성!" << std::endl;
    }
    ~Triangle()
    {
        std::cout << "삼각형 클래스 제거!" << std::endl;
    }

    virtual void Use() override
    {
        std::cout << "삼각형 클래스 사용" << std::endl;
    }
};

#endif // __DIAGRAM_H__

 

 

2. 객체 풀 (Object Pool)

    삼각형 50개, 사각형 50개를 만든 뒤 저장한다.

    풀에 접근할 수 있는 GetObject() 함수 정의 -> 템플릿을 사용하여 Diagram 계통의 모든 객체에 접근할 수 있도록 한다.

    ReturnObject를 통해 객체 반납한다.

#pragma once

#include <vector>
#include <typeinfo>
#include <stdexcept>
#include "Diagram.h"

class ObjectPool
{
public:
    ObjectPool()
    {
        Init();
    }

    ~ObjectPool()
    {
        // 풀에 남아 있는 객체 삭제
        for (auto Obj : Pool)
        {
            delete Obj;
        }
        Pool.clear();
    }

    // 객체를 풀에서 가져옴
    template<typename T>
    T* GetObject()
    {
        for (size_t i = 0; i < Pool.size(); ++i)
        {
            T* CastedObj = dynamic_cast<T*>(Pool[i]);
            if (CastedObj)
            {
                // 객체를 반환하고 풀에서 제거
                Pool.erase(Pool.begin() + i);
                return CastedObj;
            }
        }

        // 해당 타입의 객체가 없는 경우 예외 발생
        throw std::runtime_error("현재 풀에서 해당 타입의 객체가 존재하지 않음");
    }


    // 객체를 풀로 반환
    void ReturnObject(Diagram* Obj)
    {
        Pool.push_back(Obj);
    }

private:
    void Init()
    {
        for (int i = 0; i < 100; ++i)
        {
            if (i < 50)
                Pool.push_back(new Rectangle());
            else
                Pool.push_back(new Triangle());
        }
    }

    std::vector<Diagram*> Pool; // Diagram 기반 객체 저장소
};

 

 

3. 메인 실행 함수

#include "ObjectPool.h"

int main()
{
	ObjectPool ObjPool;
	Diagram* Shape;

	try
	{
		Shape = ObjPool.GetObject<Triangle>();
		Shape->Use();
		ObjPool.ReturnObject(Shape);
	}
	catch (std::runtime_error& e)
	{
		std::cout << e.what() << std::endl;
	}

	return 0;
}

 

 

예시 코드가 정확히 맞을지는 모르겠지만, 객체를 빈번하게 생성, 삭제하는 비용이 부담스럽다면 미리 생성해 놓은 뒤 꺼내쓸 수도 있구나란 생각을 가지는 게 포인트인 것 같다.