이론/디자인패턴

객체 풀

tsyang 2022. 3. 4. 02:26

왜써야댐 


객체를 생성하기 위해 힙 메모리를 할당/해제하다보면 메모리 단편화가 생긴다. 메모리 단편화는 특히 메모리가 부족한 모바일 환경에서 치명적이다. 

 

객체 풀은 사용될 객체들을 위한 메모리를 미리 크게 잡아놓음으로써 메모리 단편화를 예방한다. 게다가 일부 관리 언어에서 GC호출 빈도를 줄여주는 효과도 있다.

 

그럼 언제씀


객체 풀 패턴은 다음과 같을 때 사용하면 좋다.

  • 객체를 빈번하게 생성/삭제한다.
  • 객체들의 크기가 비슷하다(같다).
  • 객체를 힙에 생성하는 게 느리거나 메모리 단편화가 우려된다.
  • DB나 네트워크 연결같이 접근 비용이 비싸면서 재사용 가능한 자원을 객체가 캡슐화하고 있다.

 

구현


아래와 같은 파티클 객체가 있다.

class Particle
{
public:
	Particle() : frame_(0){}
	void init(double x, double y, double lifeTime);
	bool isPlaying() const { return frame_ > 0; }
	
private:
	double x_, y_;
	int frame_;
};

 

파티클에 대한 객체 풀은 일단 다음과 같이 표현할 수 있다.

class ParticlePool
{
public :
	Particle * create(double x, double y, int lifeTime)
	{
		for(int i=0; i < POOL_SIZE; ++i)
		{
			//쓰지 않는 파티클을 찾는다.
			if (false == particles_[i].isPlaying())
			{
				particles_[i].init(x, y, lifeTime);
				return &particles_[i];
			}
		}
		return nullptr;
	}
private:
	const static int POOL_SIZE = 100;
	Particle particles_[POOL_SIZE];
};

 

쓰지 않는 파티클을 찾는 부분을 보면 객체 풀을 돌면서 사용중이 아닌 파티클을 찾는다. 당연히 시간이 낭비된다.

 

따라서 별도의 리스트에 사용 가능한 객체들을 모아둘 수 있다. 이렇게 하면 메모리를 희생해서 성능을 향상시킬 수 있다.

 

만약 공용체를 사용할 수 있다면 메모리를 희생하지 않아도 된다!!!

 

class Particle
{
public:
	//원래 있던 코드들
	Particle * getNext() const { return state_.next; }
	void setNext(Particle * next) { state_.next = next; }
	
private:
	union
	{
		struct
		{
			double x_, y_;
			int frame_;
		} live;

		Particle * next;
	} state_;
};

이런식으로 객체가 사용되지 않을때는 다음 사용가능한 객체를 가리키는 포인터를 사용하도록 한다. 이렇게 하면 사용 가능한 풀이 묶여 있는 연결 리스트를 만들 수 있다. 이걸 빈칸 리스트(free list)기법이라고 한다.

 

ParticlePool은 다음과 같이 수정할 수 있다.

class ParticlePool
{
public :
	ParticlePool()
	{
		//빈칸 리스트를 초기화한다.
		availableHead_ = &particles_[0];
		for (int i = 0; i < POOL_SIZE - 1; ++i)
			particles_[i].setNext((&particles_[i + 1]));
		particles_[POOL_SIZE - 1].setNext(nullptr);
	}
	
	Particle * create(double x, double y, int lifeTime)
	{
       	 //새로운 생성 로직
		Particle * newParticle = availableHead_;
		availableHead_ = availableHead_->getNext();
		return newParticle;
	}

private:
	const static int POOL_SIZE = 100;
	Particle particles_[POOL_SIZE];
	Particle * availableHead_;	//빈칸 리스트의 헤드 추가됨
};

 

주의사항


메모리 낭비 가능성

필요한 메모리보다 메모리를 크게 잡아놨다면 당연히 메모리가 낭비된다.

 

사용 가능 객체수 제한

메모리 크기를 미리 잡아놓다보니 사용 가능한 객체수에도 한계가 있다. 다음과 같은 대비책을 고려해보자.

 

애초에 넉넉하게 잡는다 

이런 경우 몇 안되는 최악의 경우때문에 메모리가 낭비된다. 동적으로 풀의 크기를 조절할 수 있다면 더 좋을 것이다.

 

한계를 넘으면 객체를 안 만든다

파티클 시스템같은 경우에 적합하다. 이미 이펙트가 화면을 덮고있다면 생성을 안 해도 티 안 난다.

 

기존 객체를 제거한다

사운드 시스템같은 경우에 적합하다. 새로 재생되는 사운드는 중요하다. 그러나 이미 재생되고 있는 여러 개의 사운드는 한 두개 없어진다고 큰 차이가 없다. 이런 경우에 가장 볼륨이 작은 사운드를 새 사운드로 교체해줄 수 있다.

 

풀의 크기를 늘린다

추가로 풀을 위한 메모리를 할당한다. 이렇게 되면 메모리를 좀 더 유연하게 쓸 수 있다. 다만 추가로 늘린 메모리를 더 이상 쓰지 않을 때 크기를 원래대로 줄여줄지 정해야 한다.

 

객체의 메모리 크기 고정됨

보통 객체풀은 배열로 관리된다. 만약 다른 자료형이나 필드가 추가된 하위 클래스도 같은 풀에 넣고싶다면 풀의 한 칸 크기를 가장 큰 객체의 크기로 맞춰야 한다. 따라서 메모리가 낭비된다. 이러한 경우 객체 크기별로 풀을 나누는게 좋다

 

 

객체 초기화에 주의

객체 풀은 메모리 관리자를 통하지 않기 때문에 메모리가 초기화되었는지의 확인을 직접 해줘야 한다. 따라서 풀에서 새로운 객체를 초기화 한다면 주의해야 한다. 

 

객체도 메모리에 (GC)

객체가 삭제되면 메모리에서 삭제되는게 아니라 풀로 되돌아간다. 이때 삭제된 객체가 다른 객체를 참조한다면 GC가 참조된 객체를 수거해가지 않는다. 이를 위해서 객체를 풀로 회수할 때 참조를 정리하거나 간접참조나 약한참조를 활용해야 한다.

 


 

디자인 결정


풀이 객체와 커플링?

객체 풀을 구현할 때 객체가 자신이 풀에 있는지 알게 할 것인지를 결정해야 한다. 

 

객체가 알게 한다

이렇게 하면 더 간단하게 구현할 수 있다. 단순히 객체에 '나 사용 중' 플래그를 두는 방법이 그렇다. 또 객체 풀을 통해서만 객체를 생성하도록 강제할 수 있다. (friend키워드)

 

객체는 몰라

어떤 객체라도 풀에 넣을 수 있다는 장점이 있다. 대신 '나 사용 중'상태를 객체 외부에서 관리한다. 간단한 방법은 bool 배열을 하나 추가하는 것이다.

 

 

객체 초기화는 어디서?

객체를 재사용하려면 상태를 초기화해야 한다. 이걸 풀에서 해줄까 밖에서 해줄지 결정해야 한다.

 

풀에서 초기화

이렇게하면 풀이 객체를 완전히 캡슐화 할 수 있다. 풀 클래스는 객체가 초기화 되는 방법과 결합된다. 즉, 객체의 다양한 생성자를 지원해야 한다. 캡슐화는 하되 밖에서 객체를 참조할 수 있게 하려면 객체 포인터 말고 핸들 값을 반환한다.

 

밖에서 초기화

이렇게하면 풀의 인터페이스가 단순해진다. 외부 코드에서 객체 생성이 실패했을 때(ex. create()가 nullptr 반환)처리를 해줘야 한다.

 

'이론 > 디자인패턴' 카테고리의 다른 글

데이터 지역성  (0) 2022.02.28
업데이트 메서드  (0) 2022.02.20
타입 객체  (0) 2022.02.10
게임 루프  (1) 2022.01.31
이중 버퍼  (0) 2022.01.26