1. 객체 풀 패턴 정의
- 객체를 매번 할당, 해제하지 않고 고정 크기 풀에 들어 있는 객체를 재사용해 메모리 사용 성능을 개선하는 패턴.
2. 객체 풀 패턴이 필요한 경우
- 객체를 빈번하게 생성/삭제해야 할 때
- 객체들의 크기가 비슷할 때
- 객체를 힙에 생성하기가 느리거나 메모리 단편화가 우려될 때
3. 객체 풀 패턴 구현
class Particle
{
private:
int FramesLeft;
double X;
double Y;
double XVel;
double YVel;
public:
Particle() : FramesLeft( 0 ) {}
void Init( double InX, double InY, double InXVel, double InYVel, int InLeftTime )
{
X = InX;
Y = InY;
XVel = InXVel;
YVel = InYVel;
FramesLeft = InLeftTime;
}
void Animate()
{
if ( !InUse() ) return;
--FramesLeft;
X += XVel;
Y += YVel;
}
bool InUse() const { return FramesLeft > 0; }
};
// 객체 풀 클래스
class ParticlePool
{
private:
static const int POOL_SIZE = 100;
Particle Particles[ POOL_SIZE ];
public:
void Create( double InX, double InY, double InXVel, double InYVel, int InLeftTime )
{
// 사용 가능한 파티클을 찾는다.
for ( int index = 0; index < POOL_SIZE; ++index )
{
if ( !Particles[ index ].InUse() )
{
Particles[ index ].Init( InX, InY, InXVel, InYVel, InLeftTime );
return;
}
}
}
void Animate()
{
for ( int index = 0; index < POOL_SIZE; ++index )
Particles[ index ].Animate();
}
};
- 기본 생성자에서 파티클을 '사용 안 함'으로 초기화한다, Init() 함수가 호출되면 파티클이 사용 중 상태로 변경된다.
- 사용 가능한 파티클을 찾을 때까지 풀을 순회한 뒤, 찾으면 초기화를 진행한다.
4. 빈칸 리스트를 사용한 객체 풀 구현
class Particle
{
private:
int FramesLeft;
union
{
// 사용 중일 때의 상태
struct
{
double X, Y;
double XVel, YVel;
} Live;
// 사용 중이 아닐 때의 상태
Particle* Next;
} State;
public:
Particle() : FramesLeft( 0 ) {}
void Init( double InX, double InY, double InXVel, double InYVel, int InLeftTime )
{
State.Live.X = InX;
State.Live.Y = InY;
State.Live.XVel = InXVel;
State.Live.YVel = InYVel;
FramesLeft = InLeftTime;
}
bool Animate()
{
if ( !InUse() ) return false;
--FramesLeft;
State.Live.X += State.Live.XVel;
State.Live.Y += State.Live.YVel;
return FramesLeft == 0;
}
bool InUse() const { return FramesLeft > 0; }
Particle* GetNext() const { return State.Next; }
void SetNext( Particle* InNext ) { State.Next = InNext; }
};
// 객체 풀 클래스
class ParticlePool
{
private:
static const int POOL_SIZE = 100;
Particle Particles[ POOL_SIZE ];
Particle* FirstAvailable;
public:
ParticlePool()
{
// 처음 파티클부터 사용 가능하다.
FirstAvailable = &Particles[ 0 ];
// 모든 파티클은 다음 파티클을 가리킨다.
for ( int index = 0; index < POOL_SIZE - 1; ++index )
Particles[ index ].SetNext( &Particles[ index + 1 ] );
// 마지막 파티클에서 리스트를 종료한다.
Particles[ POOL_SIZE - 1 ].SetNext( nullptr );
}
void Create( double InX, double InY, double InXVel, double InYVel, int InLeftTime )
{
// 풀이 비어있지 않은지를 확인한다.
if ( FirstAvailable == nullptr )
return;
// 얻은 파티클을 빈칸 목록에서 제거한다.
Particle* newParticle = FirstAvailable;
FirstAvailable = newParticle->GetNext();
newParticle->Init( InX, InY, InXVel, InYVel, InLeftTime );
}
void Animate()
{
for ( int index = 0; index < POOL_SIZE; ++index )
{
if ( Particles[ index ].Animate() )
{
// 방금 죽은 파티클을 빈칸 앞에 추가한다.
Particles[ index ].SetNext( FirstAvailable );
FirstAvailable = &Particles[ index ];
}
}
}
};
- FramesLeft를 제외한 모든 멤버 변수를 State 공용체의 Live 구조체 안으로 옮겼다.
- 파티클이 살아 있는 동안에는 Live 구조체를, 사용 중이 아닌 경우에는 공용체의 다른 변수인 Next가 사용된다.
- Next는 이 파티클 다음에 사용 가능한 파티클 객체를 포인터로 가리킨다.
- Next 포인터를 시용하면 풀에서 사용 가능한 파티클이 묶여 있는 연결 리스트를 만들 수 있다.
- 추가 메모리 없이 죽어 있는 객체의 메모리를 재활용해, 자기 자신을 사용 가능한 파티클 리스트에 등록할 수 있다.
5. 객체가 풀과 커플링 된 경우
class Particle
{
friend class ParticlePool;
private:
Particle() : InUse ( false ) {}
bool InUse;
};
class ParticlePool
{
Particle Pool[ 100 ];
//...
};
- 풀에 들어가는 객체에 '사용 중' 플래그나 이런 역할을 하는 함수를 추가하여 사용한다.
- 풀 클래스를 객체 클래스의 friend로 만든 뒤 객체 생성자를 private에 두어 만든다.
6. 객체가 풀과 커플링 되지 않은 경우
template < class TObject >
class GenericPool
{
private:
static const int POOL_SIZE = 100;
TObject Pool[ POOL_SIZE ];
bool InUse[ POOL_SIZE ];
};
- 객체와 풀을 디커플링함으로써, 어떤 객체라도 풀에 넣을 수 있는 재사용 가능한 풀 클래스 구현이 가능하다.
7. 객체를 풀 안에서 초기화 하는 방법
class Particle
{
friend class ParticlePool;
private:
// 다양한 초기화 방법
void Init( double InX, double InY );
void Init( double InX, double InY, double InAngle );
void Init( double InX, double InY, double InXVel, double InYVel );
};
class ParticlePool
{
public:
void Create( double InX, double InY )
{
// Particle 클래스로 포워딩...
// Init() 호출
}
void Create( double InX, double InY, double InAngle )
{
// Particle 클래스로 포워딩...
// Init() 호출
}
void Create( double InX, double InY, double InXVel, double InYVel )
{
// Particle 클래스로 포워딩...
// Init() 호출
}
};
- 풀은 객체를 완전히 캡슐화할 수 있는 장점이 있다.
- 풀 클래스는 객체가 초기화되는 방법과 결합하게 된다.
8. 객체를 풀 밖에서 초기화 하는 방법
class Particle
{
public:
// 다양한 초기화 방법
void Init( double InX, double InY );
void Init( double InX, double InY, double InAngle );
void Init( double InX, double InY, double InXVel, double InYVel );
};
class ParticlePool
{
private:
static const int POOL_SIZE = 100;
Particle Pool[ POOL_SIZE ];
public:
Particle* Create()
{
// 사용 가능한 파티클에 대한 레퍼런스를 반환한다.
}
};
int main()
{
// 외부 코드에서 객체 생성이 실패할 때의 처리를 위한 nullptr 검사 필요
ParticlePool pool;
Particle* particle1 = pool.Create();
if ( particle1 ) particle1->Init( 1, 2 );
Particle* particle2 = pool.Create();
if ( particle2 ) particle2->Init( 1, 2, 3 );
Particle* particle3 = pool.Create();
if ( particle3 ) particle3->Init( 1, 2, 3, 4 );
}
- 풀은 객체 초기화 함수를 전부 제공할 필요가 없어진다.
- 풀이 비어 있다면 NULL을 반환할 수 도 있다. 따라서 객체를 초기화하기 이전에 예외 검사를 진행해야 한다.