본문으로 건너뛰기

안동민 개발노트

본문 시작

메모리 관리와 최적화 기법

UObject GC와 일반 C++ 소유권을 구분하고 에셋 언로드·풀링·자료구조 선택으로 메모리를 절약합니다.

이전 절에서 우리는 언리얼 엔진의 프로파일링 도구를 사용하여 게임의 성능 병목 현상을 식별하는 방법을 알아보았습니다.

이제는 특정 자원, 특히 메모리(Memory) 사용량과 관련된 최적화 기법에 대해 깊이 있게 다뤄보겠습니다.

메모리 관리는 게임의 안정성, 로딩 시간, 그리고 전반적인 성능에 직접적인 영향을 미칩니다.

메모리가 부족하면 게임이 충돌하거나, 프레임 속도가 급격히 저하되거나, 로딩 시간이 길어지는 등의 문제가 발생할 수 있습니다.

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

메모리 관리와 최적화 기법

메모리 최적화는 UObject 수명 주기, asset reference, streaming, GC 비용을 함께 보는 작업입니다. 프로파일링으로 사용량을 확인한 뒤 어떤 객체가 오래 살아남는지, 어떤 에셋이 불필요하게 로드되는지부터 좁힙니다.

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

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

  2. 2
    메모리 최적화 기법

    상태 2 불필요한 에셋 언로드 참조 해제: 더 이상 사용되지 않는 에셋이나 UObject에 대한 모든 참조를 해제해야 가비지 컬렉터가 해당 메모리를 회수할 수 있습니다.

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

    상태 3 MemReport: 게임 실행 중 콘솔에 MemReport를 입력하면 Saved/Logs 폴더에 상세한 메모리 사용 보고서가 생성됩니다.


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

이 다이어그램은 UObject GC, Non-UObject 소유권, 에셋 로딩, 풀링 전략을 메모리 비용 기준으로 나누어 정리합니다.

UObject와 일반 C++ 데이터는 소유권 규칙이 다르다

GC가 따라가는 UObject 참조와 RAII로 관리하는 Non-UObject 데이터를 먼저 구분해야 메모리 회수 판단이 섞이지 않는다.

대상수명 규칙좋은 사용위험 신호
UObjectGC가 참조 그래프를 검사Actor, Component, Asset처럼 엔진 객체로 관리참조가 남아 회수되지 않음
UPROPERTY 참조GC가 강한 참조로 추적소유해야 하는 UObject 포인터 표시누락 시 예기치 않은 회수
TWeakObjectPtr소유하지 않고 유효성만 확인캐시, 관찰자, 임시 접근검사 없이 역참조
Non-UObjectRAII와 스마트 포인터 사용버퍼, 순수 C++ 구조체, 외부 리소스new/delete 수동 관리
에셋·풀로드/반납 정책 필요반복 생성 비용을 풀링으로 제한풀 크기 증가와 스트리밍 지연

언리얼 엔진은 자체적인 메모리 관리 시스템을 가지고 있으며, 이는 주로 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/Non-UObject 메모리 관리 분기

GC 대상 UObject와 C++ 직접 관리 메모리가 서로 다른 수명 규칙을 갖는다는 점을 최적화 경로로 구분한다.

  1. 객체 종류 분류

    UObject인지, 일반 C++ 객체인지, 에셋 리소스인지 먼저 구분한다. classify

  2. 참조 경로 확인

    UObject는 UPROPERTY, Root, Outer 관계가 GC 생존 여부를 좌우한다. reference

  3. 수명 정책 적용

    일반 C++ 객체는 unique/shared pointer, 스택 수명, 명시 해제를 기준으로 관리한다. RAII

  4. 메모리 비용 축소

    텍스처, 메시, 애니메이션은 LOD, streaming, 압축 설정으로 줄인다. asset

불필요한 에셋 언로드

  • 참조 해제: 더 이상 사용되지 않는 에셋이나 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를 호출하는 대신 미리 일정 개수를 생성해두고 재활용하는 오브젝트 풀링을 구현하여 메모리 할당/해제 오버헤드를 줄입니다.

메모리 절감은 GC 대상, 일반 C++ 메모리, 에셋 스트리밍, 데이터 구조를 따로 보되 같은 측정 루프로 다시 확인해야 합니다.

메모리 절감은 소유권부터 확인

GC가 보는 UObject 참조와 직접 관리하는 C++ 메모리를 구분하고, 에셋과 컨테이너 변경은 같은 기준선에서 다시 측정합니다.

  1. UPROPERTY
    UObject 참조

    UPROPERTY 강한 참조를 끊고 필요 없는 배열 항목을 제거해야 GC가 회수할 수 있습니다.

  2. TUniquePtr
    일반 C++ 데이터

    TUniquePtr Non-UObject 메모리는 스마트 포인터와 명시적 소유권으로 누수를 줄입니다.

  3. LOD / Streaming
    그래픽 에셋

    LOD / Streaming 텍스처 그룹, 압축, LOD Bias를 함께 보며 GPU 메모리 압박을 낮춥니다.

  4. Object Pool
    반복 생성 객체

    Object Pool 총알, 이펙트, 임시 액터는 풀링으로 할당과 해제 빈도를 낮춥니다.

메모리 파편화 방지

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

  • 잦은 할당/해제 피하기: 작은 객체들을 빈번하게 생성하고 파괴하는 것을 줄입니다. 오브젝트 풀링이 여기에 도움이 됩니다.
  • 프리젠트-할당(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 같은 프로파일링 도구를 적극적으로 활용하여 메모리 사용량을 정확히 측정하고 진단함으로써, 메모리 관련 문제를 해결하고 게임의 전반적인 성능을 향상시킬 수 있을 것입니다.


메모리 최적화는 의심되는 에셋을 줄이는 작업이 아니라, 측정값을 기준으로 원인을 분류하고 다시 확인하는 루프입니다.

GC, 에셋, C++ 메모리를 분리해 측정한다

`MemReport`, Unreal Insights, 패키징 빌드 측정을 함께 사용하면 에디터 착시와 실제 런타임 메모리 문제를 구분할 수 있습니다.

  1. MemReport 수집

    측정 클래스별, 에셋별, 시스템별 메모리 사용량을 기준선으로 남깁니다.

  2. 소유 영역 구분

    분류 UObject GC, Non-UObject, 텍스처, 사운드, 메시 비용을 따로 봅니다.

  3. 로드 범위 축소

    절감 스트리밍, 압축, LOD, 풀링으로 큰 항목부터 줄입니다.

  4. 스톨 확인

    재측정 GC 스톨과 로딩 지연이 새로 생기지 않았는지 같은 장면에서 비교합니다.

  5. 원인별로 다른 대응

    UPROPERTY 참조가 남아 있으면 GC가 회수하지 못하므로 참조 경로를 추적합니다. 텍스처와 메시, 오디오는 스트리밍과 압축 설정을 먼저 검토합니다. TArray Reserve, 스마트 포인터, 풀링으로 잦은 할당과 파편화를 줄입니다.

  6. UObject

    UPROPERTY 참조가 남아 있으면 GC가 회수하지 못하므로 참조 경로를 추적합니다.

  7. 에셋

    텍스처와 메시, 오디오는 스트리밍과 압축 설정을 먼저 검토합니다.

  8. C++ 데이터

    TArray Reserve, 스마트 포인터, 풀링으로 잦은 할당과 파편화를 줄입니다.

메모리 문제를 줄일 때는 유지해야 할 품질, 로딩 시간, GC 스톨을 함께 비교해야 실제 플레이 경험을 해치지 않습니다.

메모리를 줄이면 로딩과 품질도 함께 변한다

에셋을 늦게 로드하거나 압축률을 높이면 메모리는 줄지만 로딩 지연, 화질 저하, GC 스톨이 새로 생길 수 있습니다.

  1. 현재 사용량 기록

    사용량 기준치 MemReport와 패키징 빌드 기준치를 남겨 변화량을 비교할 수 있게 합니다.

  2. 큰 항목부터 조정

    절감 텍스처, 메시, 사운드, 캐시처럼 규모가 큰 항목을 우선 줄입니다.

  3. 부작용 측정

    영향 스트리밍 지연, 압축 품질, GC 스톨이 새 병목이 되었는지 확인합니다.

  4. 플랫폼별 유지

    확정 PC, 콘솔, 모바일의 메모리 한계와 저장 장치 속도에 맞춰 기준을 나눕니다.

  5. 절감 방법별로 같이 봐야 할 값

    상주 메모리는 줄지만 이동 중 로딩 지연과 팝인이 생기지 않는지 봅니다. 텍스처와 오디오 크기를 줄인 뒤 품질 저하가 허용 범위인지 비교합니다. 할당 비용은 줄지만 풀 크기가 과해 상주 메모리를 늘리지 않는지 확인합니다.

  6. 스트리밍

    상주 메모리는 줄지만 이동 중 로딩 지연과 팝인이 생기지 않는지 봅니다.

  7. 압축

    텍스처와 오디오 크기를 줄인 뒤 품질 저하가 허용 범위인지 비교합니다.

  8. 풀링

    할당 비용은 줄지만 풀 크기가 과해 상주 메모리를 늘리지 않는지 확인합니다.

메모리 관리와 최적화 기법은 할당 주기, 컨테이너 선택, GC 영향, 캐시 비용 기준으로 점검합니다.

메모리 최적화에서는 객체 소유권, 에셋 참조, 로딩 시점, 해제 확인 절차를 함께 점검합니다.