icon
9장 : 성능 최적화

메모리 관리와 최적화 기법


이전 절에서 우리는 언리얼 엔진의 프로파일링 도구를 사용하여 게임의 성능 병목 현상을 식별하는 방법을 알아보았습니다. 이제는 특정 자원, 특히 메모리(Memory) 사용량과 관련된 최적화 기법에 대해 깊이 있게 다뤄보겠습니다. 메모리 관리는 게임의 안정성, 로딩 시간, 그리고 전반적인 성능에 직접적인 영향을 미칩니다. 메모리가 부족하면 게임이 충돌하거나, 프레임 속도가 급격히 저하되거나, 로딩 시간이 길어지는 등의 문제가 발생할 수 있습니다.

이번 절에서는 언리얼 엔진에서 메모리가 어떻게 관리되는지 이해하고, 메모리 사용량을 줄이며 효율성을 높이는 다양한 최적화 기법들을 살펴보겠습니다.


언리얼 엔진의 메모리 관리 기본

언리얼 엔진은 자체적인 메모리 관리 시스템을 가지고 있으며, 이는 주로 UObject 시스템과 가비지 컬렉션(Garbage Collection, GC) 을 중심으로 작동합니다.

가비지 컬렉션 (Garbage Collection)

  • UObject 생명주기 관리: 언리얼 엔진의 대부분의 게임 객체(액터, 컴포넌트, 에셋 등)는 UObject를 상속받습니다. 이 UObject들은 언리얼 엔진의 가비지 컬렉터에 의해 자동으로 메모리에서 해제됩니다.
  • 참조 기반: GC는 UObject에 대한 모든 유효한 참조가 사라졌을 때 해당 객체를 "가비지"로 간주하고 메모리를 회수합니다. UPROPERTY()로 선언된 포인터(UObject*)는 GC에 의해 추적되는 강한 참조(Strong Reference)로 간주됩니다.
  • TWeakObjectPtr: 약한 참조(Weak Reference)를 나타냅니다. GC가 객체를 수집하는 것을 방해하지 않으면서 객체에 접근할 수 있게 합니다. 객체가 사라지면 자동으로 nullptr이 됩니다. 순환 참조를 방지하는 데 유용합니다.
  • GC 실행: GC는 주기적으로 실행되거나, 특정 조건(예: 레벨 로딩 시)에서 강제로 실행될 수 있습니다. GC가 실행되는 동안 게임 스레드가 잠시 멈출 수 있어 "스톨(Stall)" 현상이 발생할 수 있습니다.

C++ 메모리 (Non-UObject Memory)

  • UObject가 아닌 일반 C++ 객체나 데이터(TArray<int>, FString, std::vector 등)는 가비지 컬렉션의 대상이 아닙니다. 이들은 개발자가 직접 new/delete 또는 언리얼 엔진의 FMemory::Malloc/FMemory::Free를 사용하여 명시적으로 관리해야 합니다.
  • 스마트 포인터 (TSharedPtr, TUniquePtr)를 사용하여 Non-UObject 메모리를 자동으로 관리할 수 있습니다.

메모리 최적화 기법

불필요한 에셋 언로드

  • 참조 해제: 더 이상 사용되지 않는 에셋이나 UObject에 대한 모든 참조를 해제해야 가비지 컬렉터가 해당 메모리를 회수할 수 있습니다.
    • nullptr로 설정하거나, TArray에서 제거하거나, 스코프를 벗어나게 합니다.
  • Unload 함수 사용: UGameplayStatics::UnloadStreamLevel() (스트리밍 레벨), UObject::MarkPendingKill() (개별 UObject), GC.CollectGarbage() (수동 GC 트리거) 등을 사용하여 메모리 해제를 촉진할 수 있습니다.
    • 하지만 GC.CollectGarbage()는 스톨을 유발할 수 있으므로, 로딩 화면이나 게임 전환 시점에만 사용하는 것이 좋습니다.
  • 에셋 스트리밍: 모든 에셋을 한 번에 메모리에 로드하는 대신, 필요할 때만 로드하고 사용하지 않을 때 언로드하는 에셋 스트리밍 방식을 사용합니다.
    • 레벨 스트리밍: UWorld::StreamingLevels를 사용하여 큰 맵을 여러 개의 스트리밍 레벨로 분할하고, 플레이어가 해당 지역에 진입할 때만 로드합니다.
    • 비동기 로드 (FStreamableManager, Async Load Asset): UI 이미지, 사운드, 이펙트 등 특정 시점에만 필요한 에셋을 비동기적으로 로드하여 초기 로딩 시간을 줄이고 메모리를 효율적으로 관리합니다.

텍스처 및 메시 최적화

그래픽 에셋은 게임 메모리에서 가장 큰 비중을 차지할 수 있습니다.

  • 텍스처 해상도: 불필요하게 높은 해상도의 텍스처는 줄입니다. Texture Streaming 설정을 활용하여 GPU에 필요한 Mipmap만 로드하도록 합니다. Texture Group 설정을 통해 각 텍스처의 스트리밍 동작을 제어할 수 있습니다.
  • 텍스처 압축: 각 텍스처에 적절한 압축 설정을 사용합니다 (예: BC1 (DXT1), BC3 (DXT5) for RGB/RGBA, BC5 for Normal Maps). 모바일 플랫폼에서는 ETC, PVRTC, ASTC 등을 사용합니다.
  • UV 채널 수: 필요한 UV 채널만 사용하세요. 각 추가 UV 채널은 메시당 추가 메모리를 소비합니다.
  • LOD (Level Of Detail): 카메라 거리에 따라 메시의 폴리곤 수를 자동으로 줄이는 LOD를 사용하여 메모리 및 렌더링 오버헤드를 줄입니다. 스켈레탈 메시의 경우 LOD BiasMin LOD 설정을 활용합니다.
  • 최소화된 정점 색상/탄젠트: 정점 색상이나 탄젠트가 필요 없는 경우 제거하여 메시 데이터를 줄입니다.
  • 스켈레탈 메시 애니메이션 압축: 애니메이션 시퀀스에 적절한 압축 설정을 적용하여 메모리 사용량을 줄입니다.

오디오 최적화

  • 사운드 압축: 오디오 파일에 적절한 압축 형식(예: .ogg (Vorbis), .wav with ADPCM)을 사용하고, 품질과 파일 크기 사이의 균형을 맞춥니다.
  • 스트리밍 오디오: 배경 음악처럼 길이가 길고 메모리를 많이 차지하는 사운드는 메모리에 한 번에 로드하는 대신, 디스크에서 스트리밍하여 재생하도록 설정합니다.
  • 샘플 레이트 감소: 불필요하게 높은 샘플 레이트의 사운드는 낮춥니다.

컨테이너 및 데이터 구조 효율성

  • TArray 초기 용량: TArray를 사용할 때 TArray::Reserve()를 사용하여 예상되는 최대 크기만큼 미리 메모리를 할당하면, 동적 재할당으로 인한 성능 저하와 메모리 파편화를 줄일 수 있습니다.
  • TMap, TSet 해시 충돌: TMap이나 TSet을 사용할 때 키의 해시 충돌이 많아지면 성능이 저하될 수 있습니다. 적절한 해시 함수를 사용하거나, 키를 최적화하는 것을 고려합니다.
  • 캐싱: 자주 접근하는 데이터를 캐싱하여 불필요한 재계산을 피하고 메모리 접근 패턴을 최적화합니다.
  • 오브젝트 풀링 (Object Pooling): 자주 생성되고 파괴되는 액터(예: 총알, 파티클 효과)의 경우, 매번 SpawnActor/DestroyActor를 호출하는 대신 미리 일정 개수를 생성해두고 재활용하는 오브젝트 풀링을 구현하여 메모리 할당/해제 오버헤드를 줄입니다.

메모리 파편화 방지

메모리 파편화는 사용 가능한 총 메모리는 충분하지만, 연속된 큰 블록의 메모리가 부족하여 할당 실패나 성능 저하를 유발하는 현상입니다.

  • 잦은 할당/해제 피하기: 작은 객체들을 빈번하게 생성하고 파괴하는 것을 줄입니다. 오브젝트 풀링이 여기에 도움이 됩니다.
  • 프리젠트-할당(Pre-allocation): 미리 필요한 메모리를 할당해두는 전략을 사용합니다.
  • 메모리 어라인먼트(Alignment): 데이터 구조가 메모리 정렬 규칙을 따르도록 하여 CPU가 효율적으로 접근할 수 있도록 합니다. (언리얼 엔진의 컨테이너는 대부분 자동으로 처리합니다.)

메모리 프로파일링 도구 활용 (복습 및 심화)

  • MemReport: 게임 실행 중 콘솔에 MemReport를 입력하면 Saved/Logs 폴더에 상세한 메모리 사용 보고서가 생성됩니다. 이 보고서에는 각 에셋 타입, 클래스, 시스템별 메모리 사용량에 대한 정보가 포함되어 있어 어떤 부분이 메모리를 가장 많이 소모하는지 파악하는 데 매우 유용합니다.
    • MemReport -full: 더 자세한 할당 스택 정보 포함.
  • Unreal Insights (Memory Insights): Unreal Insights의 Memory Insights 탭은 시간 경과에 따른 메모리 사용량 변화, 할당/해제 이벤트, 메모리 누수를 시각적으로 추적하는 강력한 도구입니다. 특정 시간에 메모리가 급증하는 원인을 분석하는 데 효과적입니다.
  • stat memory: 인게임 오버레이로 현재 총 메모리 사용량 및 주요 시스템별 메모리 사용량을 간략하게 보여줍니다.

개발 환경 설정

  • 개발 빌드 vs 릴리스 빌드: 개발 빌드에서는 디버깅 정보와 프로파일링 코드가 포함되어 메모리 사용량이 더 많을 수 있습니다. 최종 릴리스 빌드에서는 이러한 오버헤드가 제거되어 메모리 사용량이 감소합니다.
  • 에디터에서 테스트 시 주의: 에디터에서 게임을 플레이하면 에디터 자체의 메모리 사용량과 로드된 모든 에셋(심지어 맵에 없는 에셋까지) 때문에 실제 게임보다 훨씬 많은 메모리를 사용합니다. 항상 독립 실행형 게임(Standalone Game) 이나 패키징된 빌드에서 메모리를 측정해야 합니다.
  • 스크립트 언어 최적화: 블루프린트도 메모리를 사용합니다. 불필요하게 큰 블루프린트나 과도한 변수 사용은 피하고, 가능하면 C++로 옮기는 것을 고려합니다.

메모리 관리는 게임 성능 최적화의 핵심적인 부분이며, 안정적이고 효율적인 게임을 만드는 데 필수적입니다. 언리얼 엔진의 가비지 컬렉션 메커니즘을 이해하고, 불필요한 에셋 언로드, 텍스처/메시/오디오 최적화, 효율적인 데이터 구조 사용, 그리고 메모리 파편화 방지 기법을 적용하는 것이 중요합니다. 또한 MemReport와 Unreal Insights 같은 프로파일링 도구를 적극적으로 활용하여 메모리 사용량을 정확히 측정하고 진단함으로써, 메모리 관련 문제를 해결하고 게임의 전반적인 성능을 향상시킬 수 있을 것입니다.