void Example() {
// 1. 메모리 할당
string name = "Hello";
// 2. 새로운 값 할당
name = "World";
// 이때 "Hello"는 더 이상 참조되지 않는 가비지가 됨
// 3. GC가 주기적으로 실행되어 "Hello" 문자열이 차지하던 메모리를 해제
}
오브젝트 풀링이란?
- 자주 생성되고 파괴되는 객체들을 미리 생성해두고 재사용하는 메모리 관리 패턴
- 게임에선 총알, 파티클, 몬스터 등 관리할 때 주로 사용
질문
Q. 풀링이 필요한 이유
A. 성능영향:생성/파괴 시 발생하는 비용, 컴퍼넌트 초기화, 가비지 컬렉션 부화 등 최소화
A. 지속적 생성/파괴로 메모리 단편화 현상 예방, 일관된 메모리 사용 패턴 유지
A. 프레임 드랍 방지 갑작스러운 다수의 오브젝트 생성은 프레임 저하 발생 시킬 수 있음
A. 가비지 컬렉션 실행 빈도 감소
Q. 메모리 단편화가 발생하면 게임 성능에 영향을 미칠 수 있을까?
A. 실제로 충분한 전체 메모리가 있지만, 연속된 큰 공간이 없어 새로운 데이터를 저장하지 못하는 상황 발생
A. 시스템이 이러한 빈 공간들을 관리하느데 추가적인 리소스 소모
A. 특히 모바일 게임에선 심각한 성능 저하의 원인이 될 수 있음
- 이러한 문제들로 인해 로딩 시간 증가, 메모리 사용 호율 저하, 예상치 못한 OOM에러 발생 가능
*메모리 단편화란?
- 마치 퍼즐 조각들 사이에 빈 공간이 생기는 것과 비슷하다고 한다.
[사용][사용][빈공간][사용][빈공간][빈공간][사용][빈공간][사용]
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
오브젝트 풀링의 장점
1. 성능 향상
- GC 호출 감소
- 실시간 메모리 할당/해제 최소화
- 프레임 드롭 방지
2. 메모리 관리 효율
- 예측 가능한 메모리 사용량
- 메모리 단편화 감소
- 안정적인 메모리 패턴
3. 반응성 향상
- 오브젝트 재사용으로 즉각적인 생성
- 지연 없는 오브젝트 스폰
오브젝트 풀링의 단점
1. 초기 메모리 사용
- 풀 생성 시 초기 메모리 필요
- 사용하지 않는 오브젝트도 메모리 점유
2. 풀 크기 관리
- 적절한 풀 크기 예측 필요
- 동적 크기 조정의 복잡성
3. 구현 복잡도
- 추가적인 코드 관리 필요
- 오브젝트 상태 리셋 로직 필요
질문
Q. 다음 상황에서 오브젝트 풀링을 사용하면 어떤 장점이 가장 두드러질까?
void SpawnEnemies() {
for(int i = 0; i < 100; i++) {
Instantiate(enemyPrefab);
}
}
A. 많은 양의 오브젝트를 빠르게 활성화 가능하고, 여러번 사용할때 GC 감소 효과
Q. 오브젝트 풀의 크기를 너무 크게 잡으면 어떤 문제가 발생할까?
A. 필요없는 오브젝트가 생길 수 있고, 많은 양의 메모리를 사용하게 됨
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
기본적인 오브젝트 풀 구현
예시
public class ObjectPool : MonoBehaviour
{
[SerializeField] private GameObject prefab; // 풀링할 프리팹
[SerializeField] private int poolSize = 20; // 초기 풀 크기
private List<GameObject> pool; // 풀 저장소
private void Start()
{
// 풀 초기화
pool = new List<GameObject>();
// 초기 오브젝트 생성
for (int i = 0; i < poolSize; i++)
{
CreateNewObject();
}
}
// 새로운 오브젝트 생성
private GameObject CreateNewObject()
{
GameObject obj = Instantiate(prefab);
obj.SetActive(false);
pool.Add(obj);
return obj;
}
// 풀에서 오브젝트 가져오기
public GameObject GetFromPool()
{
// 비활성화된 오브젝트 찾기
GameObject obj = pool.Find(x => !x.activeInHierarchy);
// 없으면 새로 생성
if (obj == null)
{
obj = CreateNewObject();
}
obj.SetActive(true);
return obj;
}
// 풀에 오브젝트 반환
public void ReturnToPool(GameObject obj)
{
obj.SetActive(false);
}
}
질문
Q. 위 코드에서 CreateNewObject() 메서드가 private인 이유는 무엇일까요?
A. 외부에서 직접 오브젝트 생성을 막기 위한 풀 관리의 일관성 유지
Q. GetFromPool()에서 비활성화된 오브젝트를 찾을 때 사용하는 방식의 시간 복잡도는 얼마일까요? 더 효율적인 방법이 있을까요?
A. 현재 시간 복잡도는 "O(n) - 전체 풀 순회" 이며, Queue를 사용하면 개선 가능
A. Dequeue()와 Enqueue(obj); // O(1) 시간 복잡도
Q. 풀의 크기를 동적으로 조절하고 싶다면 어떤 방식으로 구현하면 좋을까요?
- 풀 크기 한계 설정
- 오브젝트 초기화/리셋 로직
- 여러 종류의 프리팹 지원
최종 결과물 및 예시
public class ObjectPool<T> : MonoBehaviour where T : Component
{
[SerializeField] private T prefab;
private Queue<T> availableObjects;
// 동적 크기 조절을 위한 설정값
private int currentPoolSize;
private int maxPoolSize;
public void Setup(int initialSize, int maxSize)
{
availableObjects = new Queue<T>();
currentPoolSize = initialSize;
maxPoolSize = maxSize;
// 초기 풀 생성
for (int i = 0; i < initialSize; i++)
{
CreateNewObject();
}
}
private T CreateNewObject()
{
if (currentPoolSize >= maxPoolSize)
{
Debug.LogWarning("Pool has reached its maximum size!");
return null;
}
T obj = Instantiate(prefab);
obj.gameObject.SetActive(false);
currentPoolSize++;
return obj;
}
public T Get()
{
T obj = availableObjects.Count > 0
? availableObjects.Dequeue()
: CreateNewObject();
if (obj != null)
{
obj.gameObject.SetActive(true);
InitializeObject(obj); // 상태 초기화
}
return obj;
}
public void Return(T obj)
{
if (obj == null) return;
ResetObject(obj); // 상태 리셋
obj.gameObject.SetActive(false);
availableObjects.Enqueue(obj);
}
// 오브젝트 초기화 (상속받은 클래스에서 구현)
protected virtual void InitializeObject(T obj)
{
// 기본 초기화 로직
}
// 오브젝트 리셋 (상속받은 클래스에서 구현)
protected virtual void ResetObject(T obj)
{
// 기본 리셋 로직
}
}
// 사용 예시: 총알 풀
public class BulletPool : ObjectPool<Bullet>
{
protected override void InitializeObject(Bullet bullet)
{
bullet.transform.position = Vector3.zero;
bullet.transform.rotation = Quaternion.identity;
bullet.Speed = 10f;
bullet.Damage = 1;
}
protected override void ResetObject(Bullet bullet)
{
bullet.StopAllCoroutines();
bullet.TrailRenderer?.Clear();
}
}
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
파티클 풀링
public class ParticlePool : ObjectPool<ParticleSystem>
{
protected override void InitializeObject(ParticleSystem particle)
{
// 파티클 초기 설정
var main = particle.main;
main.stopAction = ParticleSystemStopAction.Callback; // 파티클 종료 시 콜백
// 파티클 완료 이벤트 연결
particle.gameObject.AddComponent<ParticleCallback>()
.Setup(this, particle);
}
protected override void ResetObject(ParticleSystem particle)
{
particle.Clear(); // 모든 파티클 즉시 제거
particle.Stop(); // 파티클 시스템 정지
}
}
// 파티클 완료 감지용 컴포넌트
public class ParticleCallback : MonoBehaviour
{
private ParticlePool pool;
private ParticleSystem particle;
public void Setup(ParticlePool pool, ParticleSystem particle)
{
this.pool = pool;
this.particle = particle;
}
private void OnParticleSystemStopped()
{
// 파티클이 완료되면 자동으로 풀로 반환
pool.Return(particle);
}
}
특징
1. 자동 반환 메커니즘
- 파티클 재생 완료 시 자동 풀로 반환
- 메모리 누수 방지
2. 상태 초기화
- Clear로 기존 파티클 제거
- Stop으로 시스템 정지
3. 최적화 포인트
- 파티클의 수명 관리
- 메모리 사용량 조절
- 동시 재생 파티클 수 제한
질문
Q. 왜 파티클 시스템은 일반 게임오브젝트와 다르게 자동 반환 메커니즘이 필요할까요?
A. 재생 완료 시 자동으로 반환하기 위함과 메모리 누수 방지를 위함
Q. ParticleCallback 컴포넌트를 별도로 만든 이유는 무엇일까요?
A. 코드 관리의 편의성과 관련된 콜백 기능을 해당 클래스에 작성하기 위함
Q. 이 파티클 풀링 시스템에서 추가로 개선할 수 있는 부분이 있을까요?
A. AddComponent로 인한 가비지 발생을 개선할 수 있을 것 같습니다.
AddComponent 개선 방안
public class ParticlePool : ObjectPool<ParticleSystem>
{
protected override void InitializeObject(ParticleSystem particle)
{
var main = particle.main;
main.stopAction = ParticleSystemStopAction.Callback;
// AddComponent 대신 미리 생성된 컴포넌트 활용
var callback = particle.GetComponent<ParticleCallback>();
if (callback == null)
{
callback = particle.gameObject.AddComponent<ParticleCallback>();
}
callback.Setup(this, particle);
}
}
or
// 프리팹 자체에 ParticleCallback 컴포넌트를 미리 추가해두고
protected override void InitializeObject(ParticleSystem particle)
{
var main = particle.main;
main.stopAction = ParticleSystemStopAction.Callback;
// 이미 있는 컴포넌트 초기화만 수행
particle.GetComponent<ParticleCallback>().Setup(this, particle);
}
Q. 추가 고려사항이 있다면?
A. Sub-Emitter 관리
protected override void InitializeObject(ParticleSystem particle)
{
// 모든 하위 파티클 시스템 찾기
var subEmitters = particle.GetComponentsInChildren<ParticleSystem>();
foreach (var subEmitter in subEmitters)
{
var main = subEmitter.main;
main.stopAction = ParticleSystemStopAction.Callback;
// 서브 이미터 초기화 설정
}
}
A. 파티클 버스팅 제어
public class ParticlePool : ObjectPool<ParticleSystem>
{
[SerializeField] private int maxSimultaneousParticles = 1000;
protected override void InitializeObject(ParticleSystem particle)
{
var main = particle.main;
main.maxParticles = maxSimultaneousParticles; // 동시 파티클 수 제한
// 버스트 설정 최적화
var emission = particle.emission;
var bursts = new ParticleSystem.Burst[emission.burstCount];
emission.GetBursts(bursts);
// 버스트 양 조절
foreach(var burst in bursts)
{
burst.count = Mathf.Min(burst.count, maxSimultaneousParticles / 2);
}
emission.SetBursts(bursts);
}
}
A. GPU 인스턴싱 활용
protected override void InitializeObject(ParticleSystem particle)
{
var renderer = particle.GetComponent<ParticleSystemRenderer>();
renderer.enableGPUInstancing = true; // GPU 인스턴싱 활성화
// 배치 최적화를 위한 설정
renderer.minParticleSize = 0.01f;
renderer.maxParticleSize = 0.5f;
}
* 이러한 최적화가 필요한 이유:
1. Sub-Emitter도 메모리를 사용하므로 관리 필요
2. 과도한 파티클 생성은 CPU/GPU 부하 유발
3. 렌더링 배치 최적화로 드로우콜 감소
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
GPU 인스턴싱이란?
- 동일한 메시와 메테리얼을 가진 여러 오브젝트를 한 번의 드로우콜로 랜더링하는 기술
- CPU -> GPU로 데이터 전송을 최소화하여 성능 향상
작동 방식
- 비슷한 파티클들을 하나의 드로우콜로 처리
- 여러 파티클을 한번에 GPU로 전성
즉, 드로우콜 최적화
성능 향상 원리
[일반 렌더링]
파티클1 → GPU 호출 → 렌더링
파티클2 → GPU 호출 → 렌더링
파티클3 → GPU 호출 → 렌더링
[GPU 인스턴싱]
파티클1
파티클2 → 단일 GPU 호출 → 일괄 렌더링
파티클3
성능 이점
- 드로우콜 감소
- CPU-GPU간 통신 감소
- 배치 처리로 인한 랜더링 성능 향상
Sub-Emitter(서브 이미터)란?
- 메인 파티클 시스템에서 추가로 발생 시키는 하위 파티클 시스템을 가르킴
- 파티클의 생명 주기에 따른 추가 효과를 생성
종류
1.Birth Sub-Emitter
메인 파티클 생성 → 서브 파티클 발생
예: 불꽃 생성 시 연기 발생
2. Death Sub-Emitter
메인 파티클 소멸 → 서브 파티클 발생
예: 총알 충돌 시 스파크 효과
3. Collision Sub-Emitter
메인 파티클 충돌 → 서브 파티클 발생
예: 빗방울이 땅에 닿을 때 물방울 튀김
예시코드
void SetupSubEmitters(ParticleSystem mainSystem)
{
var subEmitter = mainSystem.subEmitters;
// 서브 이미터 추가
var deathEffect = Instantiate(deathParticlePrefab, mainSystem.transform);
var subEmitterType = new ParticleSystem.SubEmitterData
{
type = ParticleSystemSubEmitterType.Death,
properties = ParticleSystemSubEmitterProperties.InheritNothing
};
subEmitter.AddSubEmitter(deathEffect, subEmitterType);
}
파티클 풀링 최종 개선 코드
using UnityEngine;
using System.Collections.Generic;
// 파티클 콜백 처리를 위한 컴포넌트
public class ParticleCallback : MonoBehaviour
{
private ParticlePool pool;
private ParticleSystem particleSystem;
private ParticleSystem[] subEmitters;
public void Setup(ParticlePool pool, ParticleSystem particleSystem)
{
this.pool = pool;
this.particleSystem = particleSystem;
this.subEmitters = particleSystem.GetComponentsInChildren<ParticleSystem>();
}
private void OnParticleSystemStopped()
{
// 모든 하위 파티클이 정지했는지 확인
foreach (var subEmitter in subEmitters)
{
if (subEmitter != particleSystem && subEmitter.isPlaying)
return;
}
// 모든 파티클이 정지한 경우 풀로 반환
pool.Return(particleSystem);
}
}
// 파티클 시스템 풀
public class ParticlePool : MonoBehaviour
{
[Header("풀 설정")]
[SerializeField] private ParticleSystem particlePrefab;
[SerializeField] private int initialPoolSize = 10;
[SerializeField] private int maxPoolSize = 20;
[Header("파티클 설정")]
[SerializeField] private int maxSimultaneousParticles = 1000;
[SerializeField] private float maxParticleSize = 0.5f;
[SerializeField] private float minParticleSize = 0.01f;
private Queue<ParticleSystem> availableParticles;
private List<ParticleSystem> activeParticles;
private int currentPoolSize;
private void Start()
{
Initialize();
}
private void Initialize()
{
availableParticles = new Queue<ParticleSystem>();
activeParticles = new List<ParticleSystem>();
currentPoolSize = 0;
// 초기 풀 생성
for (int i = 0; i < initialPoolSize; i++)
{
CreateNewParticle();
}
}
private ParticleSystem CreateNewParticle()
{
if (currentPoolSize >= maxPoolSize)
{
Debug.LogWarning("파티클 풀이 최대 크기에 도달했습니다!");
return null;
}
var particle = Instantiate(particlePrefab, transform);
InitializeParticle(particle);
currentPoolSize++;
availableParticles.Enqueue(particle);
return particle;
}
private void InitializeParticle(ParticleSystem particle)
{
// 메인 모듈 설정
var main = particle.main;
main.stopAction = ParticleSystemStopAction.Callback;
main.maxParticles = maxSimultaneousParticles;
// 렌더러 설정
var renderer = particle.GetComponent<ParticleSystemRenderer>();
renderer.enableGPUInstancing = true;
renderer.minParticleSize = minParticleSize;
renderer.maxParticleSize = maxParticleSize;
// 모든 하위 파티클 시스템 설정
var subEmitters = particle.GetComponentsInChildren<ParticleSystem>();
foreach (var subEmitter in subEmitters)
{
var subMain = subEmitter.main;
subMain.stopAction = ParticleSystemStopAction.Callback;
subMain.maxParticles = maxSimultaneousParticles / subEmitters.Length;
}
// 버스트 설정 최적화
var emission = particle.emission;
if (emission.burstCount > 0)
{
var bursts = new ParticleSystem.Burst[emission.burstCount];
emission.GetBursts(bursts);
foreach (var burst in bursts)
{
burst.count = Mathf.Min(burst.count, maxSimultaneousParticles / 2);
}
emission.SetBursts(bursts);
}
// 콜백 컴포넌트 설정
var callback = particle.GetComponent<ParticleCallback>();
if (callback == null)
{
callback = particle.gameObject.AddComponent<ParticleCallback>();
}
callback.Setup(this, particle);
// 초기 상태 설정
particle.gameObject.SetActive(false);
}
public ParticleSystem Get(Vector3 position, Quaternion rotation)
{
ParticleSystem particle = null;
// 사용 가능한 파티클이 없으면 새로 생성
if (availableParticles.Count == 0)
{
particle = CreateNewParticle();
if (particle == null) return null;
}
else
{
particle = availableParticles.Dequeue();
}
// 파티클 활성화 및 위치 설정
particle.gameObject.SetActive(true);
particle.transform.position = position;
particle.transform.rotation = rotation;
// 모든 파티클 시스템 초기화 및 재생
var systems = particle.GetComponentsInChildren<ParticleSystem>();
foreach (var system in systems)
{
system.Clear();
system.Play();
}
activeParticles.Add(particle);
return particle;
}
public void Return(ParticleSystem particle)
{
if (particle == null) return;
// 모든 파티클 시스템 정지 및 클리어
var systems = particle.GetComponentsInChildren<ParticleSystem>();
foreach (var system in systems)
{
system.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
}
particle.gameObject.SetActive(false);
activeParticles.Remove(particle);
availableParticles.Enqueue(particle);
}
// 모든 활성 파티클 강제 반환
public void ReturnAll()
{
foreach (var particle in activeParticles.ToArray())
{
Return(particle);
}
activeParticles.Clear();
}
// 풀 크기 조정
public void ResizePool(int newSize)
{
if (newSize < initialPoolSize)
{
Debug.LogWarning("풀 크기는 초기 크기보다 작을 수 없습니다.");
return;
}
maxPoolSize = newSize;
while (currentPoolSize < newSize && CreateNewParticle() != null) { }
}
// 디버그용 통계
public (int total, int active, int available) GetPoolStats()
{
return (currentPoolSize, activeParticles.Count, availableParticles.Count);
}
}
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
동적 풀 크기 조정
예시
public class DynamicObjectPool<T> : MonoBehaviour where T : Component
{
[SerializeField] private T prefab;
[SerializeField] private int initialSize = 10;
[SerializeField] private int maxSize = 100;
[SerializeField] private int growthFactor = 2; // 풀 확장 시 증가 비율
private Queue<T> pool;
private List<T> activeObjects;
private int currentSize;
private void Start()
{
pool = new Queue<T>();
activeObjects = new List<T>();
currentSize = 0;
GrowPool(initialSize);
}
private void GrowPool(int targetSize)
{
int growBy = Mathf.Min(targetSize - currentSize, maxSize - currentSize);
for (int i = 0; i < growBy; i++)
{
T obj = CreateNewObject();
pool.Enqueue(obj);
currentSize++;
}
}
public T Get()
{
if (pool.Count == 0 && currentSize < maxSize)
{
// 풀이 비었을 때 자동 확장
int newSize = Mathf.Min(currentSize * growthFactor, maxSize);
GrowPool(newSize);
}
if (pool.Count > 0)
{
T obj = pool.Dequeue();
activeObjects.Add(obj);
obj.gameObject.SetActive(true);
return obj;
}
Debug.LogWarning("풀이 가득 찼습니다!");
return null;
}
}
질문
Q. growthFactor를 사용하는 이유는 무엇일까요?
A. 점진적으로 풀을 늘리기 위함
// 예시: growthFactor = 2일 때
초기 크기: 10
1차 확장: 20 (10 * 2)
2차 확장: 40 (20 * 2)
3차 확장: 80 (40 * 2)
A. 이렇게 하면 한번에 많이 할당하지 않아도 되고 필요한 만큼만 점진적 증가
Q. maxSize가 필요한 이유는 무엇일까요?
A. 메모리 사용량을 제한하기 위함
Q. activeObjects 리스트를 별도로 관리하는 이유는 무엇일까요?
A. 현재 사용 중인 객체들을 추적하기 위함
예시
// 1. 모든 활성 객체의 위치 재설정
foreach(var obj in activeObjects) {
obj.transform.position = Vector3.zero;
}
// 2. 활성 객체 수 파악
Debug.Log($"현재 사용 중인 객체: {activeObjects.Count}개");
// 3. 모든 객체 회수
void ReturnAll() {
foreach(var obj in activeObjects.ToList()) {
ReturnToPool(obj);
}
}
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
다중 타입 풀링(Multi-Type Pooling)
예시
public class MultiTypeObjectPool : MonoBehaviour
{
// 풀링할 프리팹들의 정보를 담는 구조체
[System.Serializable]
public struct PoolInfo
{
public GameObject prefab;
public string key; // 프리팹 식별자
public int initialSize; // 초기 풀 크기
public int maxSize; // 최대 풀 크기
}
[SerializeField] private PoolInfo[] poolInfos; // Inspector에서 설정
// 키별로 풀을 관리하는 딕셔너리
private Dictionary<string, Queue<GameObject>> pools;
private Dictionary<string, PoolInfo> poolSettings;
private void Start()
{
Initialize();
}
private void Initialize()
{
pools = new Dictionary<string, Queue<GameObject>>();
poolSettings = new Dictionary<string, PoolInfo>();
// 각 타입별 풀 초기화
foreach (var info in poolInfos)
{
poolSettings[info.key] = info;
pools[info.key] = new Queue<GameObject>();
// 초기 오브젝트 생성
for (int i = 0; i < info.initialSize; i++)
{
var obj = CreateNewObject(info.key);
pools[info.key].Enqueue(obj);
}
}
}
// 특정 타입의 오브젝트 가져오기
public GameObject Get(string key)
{
if (!pools.ContainsKey(key))
{
Debug.LogError($"Pool with key {key} doesn't exist!");
return null;
}
Queue<GameObject> pool = pools[key];
GameObject obj;
if (pool.Count > 0)
{
obj = pool.Dequeue();
}
else
{
obj = CreateNewObject(key);
}
obj.SetActive(true);
return obj;
}
private GameObject CreateNewObject(string key)
{
var info = poolSettings[key];
var obj = Instantiate(info.prefab);
obj.SetActive(false);
return obj;
}
}
사용 예시
// 게임 매니저에서 사용
public class GameManager : MonoBehaviour
{
[SerializeField] private MultiTypeObjectPool objectPool;
void SpawnEnemy()
{
// 일반 적
GameObject normalEnemy = objectPool.Get("NormalEnemy");
// 보스 적
GameObject bossEnemy = objectPool.Get("BossEnemy");
// 총알
GameObject bullet = objectPool.Get("Bullet");
}
}
질문
Q. 다중 타입 풀링이 필요한 상황을 예를 들어볼 수 있을까요?
A. 각 타입별 몬스터나 총알 등에서 사용 가능
Q. Dictionary를 사용하는 이유는 무엇일까요?
A. 유연하게 사용하기 위함
Q. 현재 구현에서 개선이 필요해 보이는 부분이 있을까요?
A. 인스펙터로만 추가하도록 되어 있고 동적 풀 크기 조정 등이 사용되지 않음
개선 후 코드
public class AdvancedMultiTypePool : MonoBehaviour
{
[System.Serializable]
public struct PoolInfo
{
public GameObject prefab;
public string key;
public int initialSize;
public int maxSize;
public int growthFactor; // 동적 확장 비율
public bool allowAutoExpand; // 자동 확장 허용 여부
}
[SerializeField] private PoolInfo[] poolInfos;
private Dictionary<string, Queue<GameObject>> pools;
private Dictionary<string, PoolInfo> poolSettings;
private Dictionary<string, int> currentSizes; // 현재 풀 크기 추적
// 런타임에 새로운 풀 추가 가능
public void AddNewPool(PoolInfo newPoolInfo)
{
if (pools.ContainsKey(newPoolInfo.key))
{
Debug.LogError($"Pool with key {newPoolInfo.key} already exists!");
return;
}
poolSettings[newPoolInfo.key] = newPoolInfo;
pools[newPoolInfo.key] = new Queue<GameObject>();
currentSizes[newPoolInfo.key] = 0;
// 초기 크기만큼 생성
GrowPool(newPoolInfo.key, newPoolInfo.initialSize);
}
// 동적 풀 크기 조정
private void GrowPool(string key, int targetSize)
{
PoolInfo info = poolSettings[key];
int currentSize = currentSizes[key];
// 최대 크기 제한 확인
int growBy = Mathf.Min(
targetSize - currentSize,
info.maxSize - currentSize
);
for (int i = 0; i < growBy; i++)
{
var obj = CreateNewObject(key);
pools[key].Enqueue(obj);
currentSizes[key]++;
}
}
public GameObject Get(string key, Vector3 position, Quaternion rotation)
{
if (!pools.ContainsKey(key))
{
Debug.LogError($"Pool with key {key} doesn't exist!");
return null;
}
Queue<GameObject> pool = pools[key];
PoolInfo info = poolSettings[key];
GameObject obj = null;
if (pool.Count == 0)
{
if (info.allowAutoExpand && currentSizes[key] < info.maxSize)
{
// 자동 확장
int newSize = Mathf.Min(
currentSizes[key] * info.growthFactor,
info.maxSize
);
GrowPool(key, newSize);
}
if (pool.Count == 0)
{
Debug.LogWarning($"Pool {key} is full!");
return null;
}
}
obj = pool.Dequeue();
obj.transform.position = position;
obj.transform.rotation = rotation;
obj.SetActive(true);
// IPoolable 인터페이스 구현 확인
if (obj.TryGetComponent<IPoolable>(out var poolable))
{
poolable.OnSpawnFromPool();
}
return obj;
}
// 인터페이스 정의
public interface IPoolable
{
void OnSpawnFromPool();
void OnReturnToPool();
}
}
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
스레드 세이프 풀링
- 스레드 세이프 풀링은 멀티스레드 환경에서 안전하게 풀을 관리하는 방법이며, 특히 서버나 비동기 작업이 많은 게임에서 중요
예시
public class ThreadSafeObjectPool<T> where T : class
{
private readonly ConcurrentQueue<T> pool; // 스레드 세이프한 큐
private readonly Func<T> createFunc; // 객체 생성 함수
private readonly int maxSize;
private int currentSize; // Interlocked로 관리
public ThreadSafeObjectPool(Func<T> createFunc, int initialSize, int maxSize)
{
this.pool = new ConcurrentQueue<T>();
this.createFunc = createFunc;
this.maxSize = maxSize;
this.currentSize = 0;
// 초기 객체 생성
for (int i = 0; i < initialSize; i++)
{
Interlocked.Increment(ref currentSize);
pool.Enqueue(createFunc());
}
}
public T Get()
{
if (pool.TryDequeue(out T item))
{
return item;
}
// 풀이 비었을 때 새로 생성
if (Interlocked.CompareExchange(ref currentSize, currentSize + 1, currentSize) < maxSize)
{
return createFunc();
}
throw new InvalidOperationException("풀이 가득 찼습니다.");
}
public void Return(T item)
{
if (item == null) return;
pool.Enqueue(item);
}
}
사용 예시
public class ThreadSafeGameObjectPool : MonoBehaviour
{
private class PoolableObject
{
public GameObject gameObject;
public Transform transform;
public bool isActive;
}
private ThreadSafeObjectPool<PoolableObject> pool;
private object lockObject = new object();
private void Start()
{
pool = new ThreadSafeObjectPool<PoolableObject>(
createFunc: () => new PoolableObject
{
gameObject = Instantiate(prefab),
transform = gameObject.transform,
isActive = false
},
initialSize: 10,
maxSize: 100
);
}
public GameObject Get(Vector3 position)
{
var poolObject = pool.Get();
// Unity API 호출은 메인 스레드에서만 가능
lock (lockObject)
{
poolObject.gameObject.SetActive(true);
poolObject.transform.position = position;
}
return poolObject.gameObject;
}
}
Unity API 제약
// 잘못된 예
public async Task SpawnAsync()
{
await Task.Run(() => {
gameObject.SetActive(true); // 오류! Unity API는 메인 스레드에서만
});
}
// 올바른 예
public async Task SpawnAsync()
{
var poolObject = await Task.Run(() => pool.Get());
// Unity API 호출은 메인 스레드에서
MainThreadDispatcher.Enqueue(() => {
poolObject.gameObject.SetActive(true);
});
}
ConcurrentQueue와 일반 Queue의 차이점
// 일반 Queue - 스레드 안전하지 않음
Queue<int> normalQueue = new Queue<int>();
// 여러 스레드가 동시에 접근하면 문제 발생!
// ConcurrentQueue - 스레드 안전함
ConcurrentQueue<int> safeQueue = new ConcurrentQueue<int>();
// 여러 스레드가 동시에 접근해도 안전!
작동 방식
- 여러 스레드가 동시에 접근해도 데이터가 깨지지 않도록 보장
- 내부적으로 lock 메커니즘 사용
Interlocked.CompareExchange란?
- 스레드 안전한 값 변경을 위함
// 일반적인 방식 - 스레드 안전하지 않음
currentSize += 1;
// Interlocked 사용 - 스레드 안전함
Interlocked.Increment(ref currentSize);
작동 예시
// 두 스레드가 동시에 값을 변경하려 할 때
// 일반적인 방식
Thread 1: currentSize = 5 읽음
Thread 2: currentSize = 5 읽음
Thread 1: currentSize = 6으로 변경
Thread 2: currentSize = 6으로 변경
결과: currentSize = 6 (잘못된 결과)
// Interlocked 사용
Thread 1: currentSize = 5 읽음
Thread 2: 대기
Thread 1: currentSize = 6으로 변경
Thread 2: currentSize = 6 읽음
Thread 2: currentSize = 7로 변경
결과: currentSize = 7 (정확한 결과)
질문
Q. ConcurrentQueue를 사용하는 이유는 무엇일까요?
A. 여러 스레드가 동시에 접근해도 안전하기 때문
Q. Interlocked.CompareExchange는 왜 사용하나요?
A. 여러 스레드에서 처리하려고 해도 안전하게 값을 가져오기 때문
Q. 왜 Unity API 호출은 메인 스레드에서만 해야 할까요?
A. 랜더링 안정성과 컴포넌트 업데이트 순서 보장, 메모리 관리 안정성 때문
예시
// 잘못된 방법
async Task WrongWay()
{
await Task.Run(() => {
transform.position = Vector3.zero; // 오류 발생!
});
}
// 올바른 방법
async Task CorrectWay()
{
Vector3 newPosition = await Task.Run(() => {
return CalculateNewPosition(); // 계산은 다른 스레드에서
});
transform.position = newPosition; // Unity API는 메인 스레드에서
}
'유니티 > 최적화' 카테고리의 다른 글
[최적화] Assembly Defintion (0) | 2025.01.13 |
---|---|
[최적화] 스크립터블 오브젝트 (0) | 2025.01.12 |
[최적화] 유니티 어드레서블 개념 정리 (0) | 2025.01.09 |