메모리 관리와 최적화 기법
UObject GC와 일반 C++ 소유권을 구분하고 에셋 언로드·풀링·자료구조 선택으로 메모리를 절약합니다.
이전 절에서 우리는 언리얼 엔진의 프로파일링 도구를 사용하여 게임의 성능 병목 현상을 식별하는 방법을 알아보았습니다.
이제는 특정 자원, 특히 메모리(Memory) 사용량과 관련된 최적화 기법에 대해 깊이 있게 다뤄보겠습니다.
메모리 관리는 게임의 안정성, 로딩 시간, 그리고 전반적인 성능에 직접적인 영향을 미칩니다.
메모리가 부족하면 게임이 충돌하거나, 프레임 속도가 급격히 저하되거나, 로딩 시간이 길어지는 등의 문제가 발생할 수 있습니다.
이번 절에서는 언리얼 엔진에서 메모리가 어떻게 관리되는지 이해하고, 메모리 사용량을 줄이며 효율성을 높이는 다양한 최적화 기법들을 살펴보겠습니다.
언리얼 엔진의 메모리 관리 기본
언리얼 엔진은 자체적인 메모리 관리 시스템을 가지고 있으며, 이는 주로 UObject 시스템과 가비지 컬렉션(Garbage Collection, GC)을 중심으로 작동합니다.
가비지 컬렉션 (Garbage Collection)
UObject생명주기 관리: 언리얼 엔진의 대부분의 게임 객체(액터, 컴포넌트, 에셋 등)는UObject를 상속받습니다. 이UObject들은 언리얼 엔진의 가비지 컬렉터에 의해 자동으로 메모리에서 해제됩니다.- 도달 가능성: GC는 루트에서 추적 가능한 참조 그래프로 도달할 수 있는지를 판단합니다. 살아 있는 객체의
UPROPERTY강한 참조는 대상을 유지하지만, 외부에서 도달할 수 없는 순환 참조만으로 객체가 계속 살아남지는 않습니다. 모든 C++ 포인터가 추적되는 것은 아닙니다. TWeakObjectPtr: 약한 참조(Weak Reference)를 나타냅니다. GC가 객체를 수집하는 것을 방해하지 않으면서 객체에 접근할 수 있게 합니다. 객체가 파괴·수집되면 약한 참조가 유효하지 않게 되므로IsValid()또는Get()결과를 확인합니다. 대상을 소유하지 않는 캐시·관찰에 사용합니다.- GC 실행: GC는 주기적으로 실행되거나, 특정 조건(예: 레벨 로딩 시)에서 강제로 실행될 수 있습니다. GC가 실행되는 동안 게임 스레드가 잠시 멈출 수 있어 스톨(Stall) 현상이 발생할 수 있습니다.
C++ 메모리 (Non-UObject Memory)
TArray,FString,std::vector같은 일반 C++ 값은 소멸자와 RAII로 내부 저장소를 관리합니다. 매번 수동new/delete할 필요는 없습니다. 직접 할당한 버퍼와 외부 자원은 그 API에 맞는 소유권·해제 규칙을 지켜야 합니다.- 스마트 포인터 (
TSharedPtr,TUniquePtr)를 사용하여Non-UObject메모리를 자동으로 관리할 수 있습니다.
메모리 최적화 기법
불필요한 에셋 언로드
- 참조 해제: 더 이상 필요하지 않은 객체를 루트에서 유지하는 강한 참조를 제거합니다. 약한 참조까지 모두 없애야 회수되는 것은 아닙니다.
nullptr로 설정하거나,TArray에서 제거하거나, 스코프를 벗어나게 합니다.
- 해제 경로: 액터는
Destroy(), 스트리밍 레벨은UnloadStreamLevel등 각 시스템의 수명 API를 사용합니다. 일반 에셋을 임의로 파괴 표시하는 방법은 언로드 정책이 아닙니다. 강한 참조와 활성 로드 핸들 등 유지 원인을 해제하고 실제 회수 여부를 측정합니다. - 수동 GC: 엔진의
CollectGarbage같은 명시적 수집도 살아 있는 참조를 무시하여 에셋을 언로드하지는 않습니다. 수집 비용이 프레임에 미치는 영향을 먼저 확인합니다. - 에셋 스트리밍: 모든 에셋을 한 번에 메모리에 로드하는 대신, 필요할 때만 로드하고 사용하지 않을 때 언로드하는 에셋 스트리밍 방식을 사용합니다.
- 레벨 스트리밍: 레벨 스트리밍 또는 World Partition 설정으로 월드를 나누고, 플레이어가 해당 지역에 진입할 때만 로드합니다.
- 비동기 로드 (
FStreamableManager,Async Load Asset): UI 이미지, 사운드, 이펙트 등 특정 시점에만 필요한 에셋을 비동기적으로 로드하여 초기 로딩 시간을 줄이고 메모리를 효율적으로 관리합니다.
텍스처 및 메시 최적화
그래픽 에셋은 게임 메모리에서 가장 큰 비중을 차지할 수 있습니다.
- 텍스처 해상도: 불필요하게 높은 해상도의 텍스처는 줄입니다.
Texture Streaming설정을 활용하여 GPU에 필요한 Mipmap만 로드하도록 합니다.Texture Group설정을 통해 각 텍스처의 스트리밍 동작을 제어할 수 있습니다. - 텍스처 압축: 각 텍스처에 적절한 압축 설정을 사용합니다 (예:
BC1(DXT1),BC3(DXT5) for RGB/RGBA,BC5for Normal Maps). 모바일 플랫폼에서는ETC,PVRTC,ASTC등을 사용합니다. - UV 채널 수: 필요한 UV 채널만 사용하세요. 각 추가 UV 채널은 메시당 추가 메모리를 소비합니다.
- LOD (Level Of Detail): 낮은 디테일 모델로 렌더링 작업을 줄입니다. 여러 LOD가 메모리에 함께 남을 수 있으므로 LOD 전환만으로 상주 메모리가 줄었다고 보지는 않습니다. LOD 스트리밍·쿠킹 포함 범위도 함께 확인합니다.
- 최소화된 정점 색상/탄젠트: 정점 색상이나 탄젠트가 필요 없는 경우 제거하여 메시 데이터를 줄입니다.
- 스켈레탈 메시 애니메이션 압축: 애니메이션 시퀀스에 적절한 압축 설정을 적용하여 메모리 사용량을 줄입니다.
오디오 최적화
- 사운드 압축: 임포트 파일 확장자와 런타임 쿠킹 코덱을 구분하고, 대상 플랫폼이 지원하는 압축·품질 설정을 선택합니다. 파일 크기뿐 아니라 디코딩 비용과 메모리를 함께 측정합니다.
- 스트리밍 오디오: 배경 음악처럼 길이가 길고 메모리를 많이 차지하는 사운드는 메모리에 한 번에 로드하는 대신, 디스크에서 스트리밍하여 재생하도록 설정합니다.
- 샘플 레이트 감소: 불필요하게 높은 샘플 레이트의 사운드는 낮춥니다.
컨테이너 및 데이터 구조 효율성
TArray초기 용량:TArray를 사용할 때TArray::Reserve()를 사용하여 예상되는 최대 크기만큼 미리 메모리를 할당하면, 동적 재할당으로 인한 성능 저하와 메모리 파편화를 줄일 수 있습니다.TMap,TSet해시 충돌:TMap이나TSet을 사용할 때 키의 해시 충돌이 많아지면 성능이 저하될 수 있습니다. 적절한 해시 함수를 사용하거나, 키를 최적화하는 것을 고려합니다.- 캐싱: 자주 접근하는 데이터를 캐싱하여 불필요한 재계산을 피하고 메모리 접근 패턴을 최적화합니다.
- 오브젝트 풀링 (Object Pooling): 자주 생성되고 파괴되는 액터(예: 총알, 파티클 효과)의 경우, 매번
SpawnActor/DestroyActor를 호출하는 대신 미리 일정 개수를 생성해두고 재활용하는 오브젝트 풀링을 구현하여 메모리 할당/해제 오버헤드를 줄입니다.
메모리 파편화 방지
메모리 파편화는 사용 가능한 총 메모리는 충분하지만, 연속된 큰 블록의 메모리가 부족하여 할당 실패나 성능 저하를 유발하는 현상입니다.
- 잦은 할당/해제 피하기: 작은 객체들을 빈번하게 생성하고 파괴하는 것을 줄입니다. 오브젝트 풀링이 여기에 도움이 됩니다.
- 사전 할당(Pre-allocation): 미리 필요한 메모리를 할당해두는 전략을 사용합니다.
- 메모리 어라인먼트(Alignment): 데이터 구조가 메모리 정렬 규칙을 따르도록 하여 CPU가 효율적으로 접근할 수 있도록 합니다. (언리얼 엔진의 컨테이너는 대부분 자동으로 처리합니다.)
메모리 프로파일링 도구 활용 (복습 및 심화)
MemReport: 등록된 통계·객체·에셋 보고를 수집합니다. 일반적인 결과는Saved/Profiling/MemReports의.memreport이며 로그의 실제 저장 경로를 확인합니다.MemReport -full: 추가 보고 항목을 포함합니다. 모든 할당 호출 스택 기록과는 다릅니다.
- Memory Insights:
-trace=default,memory처럼 프로세스 시작부터 메모리 채널을 켜고 할당·해제와 호출 스택을 분석합니다. 패키지의 Development 구성 등 지원 조건을 확인합니다. 증가한 할당이 누수인지 의도한 캐시인지는 수명과 사용 맥락을 대조해야 합니다. stat memory: 인게임 오버레이로 현재 총 메모리 사용량 및 주요 시스템별 메모리 사용량을 간략하게 보여줍니다.
개발 환경 설정
- 개발 빌드 vs 릴리스 빌드: 개발 빌드에서는 디버깅 정보와 프로파일링 코드가 포함되어 메모리 사용량이 더 많을 수 있습니다. 배포 구성에서는 일부 진단 기능이 제외되지만 실제 메모리 차이는 콘텐츠·최적화·로드 조건을 맞춰 측정해야 합니다.
- 에디터에서 테스트 시 주의: 에디터에서 게임을 플레이하면 에디터 자체의 메모리 사용량과 로드된 모든 에셋(심지어 맵에 없는 에셋까지) 때문에 실제 게임보다 훨씬 많은 메모리를 사용합니다. 항상 독립 실행형 게임(Standalone Game)이나 패키징된 빌드에서 메모리를 측정해야 합니다.
- 스크립트 언어 최적화: 블루프린트도 메모리를 사용합니다. 불필요하게 큰 블루프린트나 과도한 변수 사용은 피하고, 가능하면 C++로 옮기는 것을 고려합니다.
풀링·Reserve·캐시는 반복 작업 비용을 줄이는 대신 상주 메모리를 늘릴 수 있습니다. 전후 메모리와 함께 로딩 지연·GC 스톨·품질을 비교해 유지할 범위를 정합니다.