메모리 줄이기

Android 메모리 관리를 이해하고 게임의 메모리 사용량을 측정하는 도구를 설정했다면 다음 단계는 메모리 사용량을 적극적으로 줄이고 최적화하는 것입니다. Android의 엄격한 제한을 준수하면 시스템이 게임을 종료하는 것을 방지하고, 시작 시간이 길어지는 것을 막고, 모든 기기에서 게임이 원활하게 실행되도록 할 수 있습니다.

이 가이드에서는 게임의 메모리 사용 공간을 줄이는 실용적인 기법을 제공하며, 특히 애셋 수준 최적화, 엔진별 구성, 메모리 관리 권장사항에 중점을 둡니다.

Unity에서 메모리 줄이기

Unity의 아키텍처 설계로 인해 네이티브 블록 할당자와 관리되는 힙이 확장되면 엔진은 애셋이 출시된 후에도 운영체제 (OS)에 즉시 반환하는 대신 재사용을 위해 이러한 메모리 페이지를 유지하는 경향이 있습니다. 특히 가상 주소 공간 (예약된 메모리)은 프로세스의 수명 동안 예약된 상태로 유지되며, 여러 가비지 컬렉션 (GC) 및 트리밍 주기가 발생할 때까지 실제 메모리 (RSS)는 즉시 회수되지 않습니다. 따라서 일시적인 메모리 피크로 인해 실제 사용량이 감소한 후에도 상주 메모리가 오랫동안 부풀어 있을 수 있습니다. 이러한 동작은 저사양 기기에서 메모리 부족 (OOM) 비정상 종료의 위험을 높이고 전반적인 런타임 안정성을 저하시킵니다.

따라서 Unity 메모리 최적화는 이러한 엔진 동작에 맞게 조정된 세 가지 핵심 원칙을 통해 접근해야 합니다.

  • 활성 메모리 사용량을 제어하여 메모리 피크 스파이크를 사전에 방지합니다.
  • 텍스처 형식과 셰이더 변형을 관리하여 불필요한 애셋과 네이티브 리소스가 인스턴스화되지 않도록 합니다.
  • 런타임 코드 구조를 리팩터링하여 관리되는 힙에서 불필요한 할당을 없애 GC 빈도와 힙 확장을 최소화합니다.

자세한 내용은 Unity 메모리 최적화를 참고하세요.

Unreal Engine에서 메모리 줄이기

Unreal Engine에서 고품질 렌더링 파이프라인과 복잡한 객체 종속성 그래프는 익명 RSS와 파일 지원 메모리 압력을 모두 크게 높일 수 있습니다. 특히 하드 참조와 딥 블루프린트 상속 계층 구조에 의존하면 사용되지 않는 연결된 애셋이 메모리에 로드됩니다. 또한 과도한 셰이더 순열, 최적화되지 않은 텍스처 스트리밍 풀, 압축되지 않은 ELF 재배치 테이블은 높은 기준 메모리 사용 공간에 기여하여 메모리 부족 종료 (LMK) 위험을 높입니다.

따라서 Unreal Engine 메모리 최적화는 이러한 엔진 동작에 맞게 조정된 세 가지 핵심 원칙을 통해 접근해야 합니다.

  • 데이터와 로직을 분리하고 하드 또는 강력한 참조를 소프트 또는 약한 참조로 바꿉니다.
  • 사용되지 않는 모바일 조명 기능과 순열 옵션을 삭제하여 파이프라인 상태 객체 (PSO)와 중복 렌더링 대상을 최소화하는 동시에 ASTC 압축을 적용하고 기기 프로필로 텍스처 스트리밍 풀을 맞춤설정합니다.
  • RELR 및 APS 재배치 테이블 압축을 사용 설정하여 ELF 바이너리 크기를 줄이고 런타임 실제 메모리 사용 공간을 줄입니다.

자세한 내용은 Unreal 메모리 최적화를 참고하세요.

다중 프로세스 최적화

캐시된 프로세스의 메모리 사용량은 활성 앱에 영향을 미치지 않으므로 메모리 제한 계산에서 제외됩니다. 별도의 격리된 프로세스에서 서비스를 실행하면 기본 프로세스가 최대한 빨리 캐시된 상태로 전환되어 게임의 상태가 개선됩니다.

자세한 내용은 프로세스 상태 및 메모리 추적 방법, Unity로 서비스 프로세스 격리 방법, 및 Unreal로 서비스 프로세스 격리 방법을 참고하세요.

사용자가 인식하는 서비스에서 메모리 사용량 줄이기

게임에서 대규모 다운로드 완료 또는 백그라운드 음성 채팅 시스템과 같은 사용 사례를 위해 사용자가 인식하는 서비스에서 로직을 실행해야 할 수 있습니다. 이러한 전략은 이러한 시나리오에서 메모리 사용량을 관리하고 줄이는 데 도움이 될 수 있습니다.

대규모 다운로드를 위한 전략

이러한 전략은 사용자가 게임을 최소화한 후에도 계속하려는 대규모 다운로드에 적용될 수 있습니다.

1. 다운로드 프로세스 격리

내용: 메모리 할당자가 메모리 풀 페이지를 보유하고 배열을 해제한 후에도 익명 RSS 메모리를 인위적으로 높게 유지할 수 있으므로 별도의 프로세스에서 다운로드를 실행하여 앱에서 사용하지 않는 메모리를 OS가 즉시 회수할 수 있도록 합니다. 서비스와 프로세스를 명시적으로 종료하거나 종료하면 메모리가 OS 풀로 반환되고 기본 프로세스는 영향을 받지 않습니다.

  • Unity에서: 맞춤 매니페스트에서 android:process=":downloader"와 같은 프로세스로 선언된 네이티브 Android Service에 다운로드를 오프로드하고 Unity의 AndroidJavaClass JNI로 호출합니다. 다운로드가 완료되면 프로세스를 종료해야 합니다. 자세한 안내는 Unity로 별도의 프로세스에서 인식 가능한 서비스 실행을 참고하세요.

  • Unreal에서: Unreal 플러그인 언어를 사용하여 android:process=":downloader"와 같은 프로세스로 맞춤 Android Service를 선언하고 C++ JNI로 트리거합니다. 다운로드가 완료되면 프로세스를 종료해야 합니다. 자세한 안내는 Unreal로 별도의 프로세스에서 인식 가능한 서비스 실행을 참고하세요.

  • 네이티브 Android의 경우: AndroidManifest에서 Serviceandroid:process=":downloader"와 같은 프로세스로 선언합니다. 이 격리된 프로세스에서 다운로드를 실행하고 다운로드가 완료되면 Process.killProcess(Process.myPid())를 호출합니다.

이 방법의 장점: 메모리가 보유되는 기간을 줄이고 더 큰 기본 프로세스에서 사용하는 메모리를 해제하면서 다운로드를 계속할 수 있습니다.

2. 다운로드를 디스크에 직접 스트리밍

내용: 네트워크 응답을 큰 배열에 누적한 후 작성하는 대신 작은 고정 크기의 재사용 가능한 버퍼를 사용하여 네트워크 소켓에서 디스크로 데이터를 직접 스트리밍합니다.

  • Unity에서: DownloadHandlerBuffer는 파일 크기 (익명 RSS 메모리)와 동일한 네이티브 메모리 버퍼를 할당하므로 애셋 번들 또는 대용량 파일에 사용하지 마세요. 대신 DownloadHandlerFile을 사용하여 바이트를 네이티브로 디스크에 스트리밍합니다.

  • Unreal에서: (Unreal의 파일 관리자를 사용하는 파일 지원 아카이브)를 사용하여 들어오는 데이터 청크를 FArchive SetResponseBodyReceiveStream()에 직접 파이프하는 대신 IHttpRequest의 페이로드를 TArray<uint8>에 추가합니다.

  • 네이티브 Android의 경우: HTTP 응답에서 .readBytes() 또는 .string()를 호출하는 대신 풀링된 버퍼를 사용하여 InputStreamFileOutputStream에 파이프합니다.

이 방법의 장점: 최대 메모리 사용량 감소

3. 다운로드한 파일 압축 해제 스트리밍

내용: 다운로드한 파일이 압축된 경우 파일을 다운로드하고 RAM에 로드한 후 추출하는 대신 네트워크 입력 스트림을 ZipInputStream과 같은 스트리밍 압축 해제기로 래핑합니다.

이 방법의 장점: 최대 메모리 사용량 감소

4. OS에 위임

내용: 백그라운드 메모리를 완전히 관리하지 않으려면 작업을 Android의 네이티브 API에 위임합니다.

  • WorkManager 는 OS 수준 JobScheduler의 최신 권장 래퍼입니다. Android 14 이상에서는 WorkManager가 사용자 트리거 다운로드를 사용자 시작 데이터 전송 (UIDT) 작업으로 자동 처리합니다. 이는 앱의 프로세스 내에서 실행되므로 메모리 사용량을 최소화하려면 디스크에 직접 스트리밍해야 합니다. UIDT는 시스템 리소스가 제한되는 경우 OS가 다운로드를 정상적으로 일시중지하고 재개할 수 있도록 하여 앱을 메모리 부족 비정상 종료로부터 보호합니다.

  • DownloadManager 는 별도의 시스템 프로세스에서 실행되며 다운로드의 메모리 사용량을 앱에 귀속시키지 않습니다. 파일이 다운로드되어 준비되면 앱에서 브로드캐스트 알림을 수신합니다.

이 방법의 장점: WorkManager는 메모리 부족 시나리오를 처리하는 데 도움이 되고 DownloadManager는 앱의 메모리 사용량을 줄입니다.

보조 서비스를 위한 전략

이러한 전략은 게임이 기본 게임 프로세스와 병렬로 실행하는 보조 서비스(예: 백그라운드 음성 채팅)에 적용될 수 있습니다.

1. 프로세스 격리

내용: 음성 채팅 솔루션과 같은 기능을 기본 게임 엔진에서 분리합니다. 예를 들어 매니페스트에서 선언된 별도의 프로세스에 할당된 Android 포그라운드 서비스 내에서 마이크 캡처 및 네트워크 스트리밍을 실행할 수 있습니다 (예: android:process=":voice").

이 방법의 장점: 앱이 최소화되면 무거운 기본 게임 프로세스는 우선순위가 낮은 캐시된 상태로 전환될 수 있지만 더 가벼운 보조 서비스는 사용자가 인식하는 서비스 상태로 계속됩니다.

2. 사용되지 않는 인프로세스 메모리 트리밍

내용: 보조 서비스가 게임 엔진에 너무 깊이 통합되어 분리할 수 없는 경우 게임이 백그라운드/최소화되는 즉시 프로세스에서 최대한 많은 무게를 줄이세요. 텍스처 캐시 플러시, 필수적이지 않은 장면 언로드, 엔진 틱 및 렌더링 속도를 0으로 낮추고 가비지 컬렉션을 명시적으로 호출하는 것이 좋습니다.

이 방법의 장점: 게임이 포그라운드에 없을 때 필요하지 않은 메모리 사용량을 줄입니다.