1. 메모리 단편화(Memory Fragmentation)란 무엇인가?
메모리 단편화는 시스템이 메모리를 할당하고 해제하는 작업을 반복하면서, 전체 메모리 공간이 점차 작고 연속되지 않은 빈 공간(조각, Fragment)들로 분할되는 현상을 말합니다. 마치 넓은 주차장에 차들이 드문드문 주차되어 있어 총 빈 공간은 많지만, 정작 대형 버스가 들어갈 연속된 빈 공간은 찾기 어려운 상황과 유사합니다. 프로그램이 특정 크기의 연속된 메모리 블록을 요청할 때, 시스템에 남아있는 총 여유 메모리 양이 요청 크기보다 훨씬 크더라도, 그 여유 공간들이 작은 조각들로 흩어져 있다면 요청된 크기의 연속된 공간을 찾지 못해 할당에 실패하거나 비효율적인 메모리 관리를 초래할 수 있습니다.
2. 메모리 단편화의 종류: 내부 및 외부 단편화
메모리 단편화는 크게 두 가지 유형으로 나눌 수 있습니다.
- 외부 단편화 (External Fragmentation): 할당된 메모리 블록들 사이사이에 사용되지 않는 작은 메모리 공간들이 흩어져 발생하는 단편화입니다. 즉, 총 가용 메모리는 충분하지만, 연속된 가용 메모리 블록이 없어서 특정 크기 이상의 메모리 할당 요청을 처리하지 못하는 상태입니다. Unity의 관리되는 힙에서 주로 문제가 되는 것은 이 외부 단편화입니다. 가비지 컬렉션 이후에도 살아남은 객체들 사이에 생긴 빈 공간들이 효과적으로 재활용되지 못할 때 심화됩니다.
- 내부 단편화 (Internal Fragmentation): 메모리를 특정 고정 크기 단위(예: 페이징 시스템의 페이지, 메모리 풀의 블록)로 할당할 때, 요청된 크기보다 할당된 메모리 블록이 더 커서 할당된 블록 내부에 사용되지 않는 공간이 발생하는 단편화입니다. 예를 들어, 1KB 단위로 메모리를 할당하는데 0.5KB만 요청하면, 할당된 1KB 블록 내의 나머지 0.5KB는 낭비되는 내부 단편화가 발생합니다. Unity의 관리 힙 단편화 논의에서는 외부 단편화만큼 직접적인 초점은 아니지만, 메모리 풀링 설계 등에서 고려될 수 있습니다.
본 포스팅에서는 주로 Unity 성능에 더 큰 영향을 미치는 외부 단편화에 초점을 맞춥니다.
3. Unity 관리 힙(Managed Heap)에서의 단편화 발생 원인
Unity의 C# 스크립트가 사용하는 관리되는 힙 영역에서 외부 단편화가 발생하는 주요 원인은 다음과 같습니다.
- 객체 생성 및 가비지 컬렉션의 반복:
- 프로그램 실행 중 다양한 크기의 객체들(
new
키워드로 생성된 클래스 인스턴스, 배열 등)이 힙에 할당됩니다. - 이후 가비지 컬렉터(GC)가 작동하여 더 이상 사용되지 않는 객체들의 메모리를 회수합니다.
- 이 과정에서 회수된 메모리 공간은 '구멍(hole)' 또는 빈 조각(free fragment)으로 남게 되는데, 이 구멍들이 힙 전체에 걸쳐 다양한 크기로 흩어지게 됩니다.
- 프로그램 실행 중 다양한 크기의 객체들(
- 비-압축(Non-Compacting) 가비지 컬렉터:
- 특히 Unity에서 전통적으로 사용되어 온 Boehm GC와 같은 일부 GC 알고리즘은 힙 압축(Heap Compaction) 기능을 수행하지 않습니다. 힙 압축은 GC 실행 시 살아남은 객체들을 힙의 한쪽으로 모아 이동시키고, 나머지 공간을 하나의 큰 연속된 빈 공간으로 만드는 작업입니다.
- 힙 압축이 없으면, GC가 가비지를 수거해도 살아남은 객체들 사이의 빈 공간(구멍)은 그대로 유지됩니다. 따라서 시간이 지남에 따라 이러한 구멍들이 누적되어 외부 단편화가 심화될 가능성이 높습니다.
- 다양한 크기의 할당 요청: 프로그램이 매우 다양한 크기의 객체들을 빈번하게 생성하고 해제하면, 힙에 남는 빈 공간의 크기 분포가 매우 불규칙해져 단편화가 가속될 수 있습니다.
- 수명이 다른 객체의 혼재: 수명이 긴 객체(게임 내내 유지되는 매니저 객체 등)와 수명이 짧은 객체(임시 계산 결과, 짧게 사용되는 이펙트 등)가 힙에 뒤섞여 할당되면, 오래 살아남는 객체들이 메모리 특정 영역을 계속 차지하게 되어 주변의 작은 빈 공간들이 합쳐지기 어렵게 만들 수 있습니다.
4. 메모리 단편화가 성능 및 메모리 사용량에 미치는 영향
메모리 단편화가 심화되면 다음과 같은 부정적인 영향을 미칠 수 있습니다.
- 메모리 할당 실패 또는 지연: 애플리케이션이 큰 크기의 연속된 메모리를 요청했을 때, 총 여유 메모리는 충분하더라도 적절한 크기의 빈 블록을 찾지 못해 할당에 실패하거나(
OutOfMemoryException
의 원인 중 하나), 또는 할당 가능한 공간을 찾기 위해 GC가 더 자주 또는 더 오래 실행되어 지연이 발생할 수 있습니다. - GC 실행 빈도 및 시간 증가: 단편화로 인해 사용 가능한 연속 공간이 빠르게 소진되면, 실제 사용 중인 힙 메모리(Used Heap)가 예약된 힙 메모리(Reserved Heap) 한계에 도달하지 않았음에도 불구하고 GC가 더 자주 호출될 수 있습니다. 또한, 단편화된 힙을 관리하는 것은 GC 작업 자체를 더 복잡하게 만들어 GC 멈춤 시간(Pause Time)을 증가시킬 수 있습니다.
- 전체 메모리 사용량 증가: 필요한 메모리를 할당하기 위해 운영체제로부터 더 많은 메모리(힙 영역 확장)를 요청하게 될 수 있습니다. 이로 인해 실제 사용되는 데이터 양에 비해 애플리케이션의 전체 메모리 점유율(Footprint)이 불필요하게 커질 수 있습니다. 이는 특히 메모리가 제한적인 모바일 환경에서 문제가 될 수 있습니다.
- 성능 저하: 위 요인들이 복합적으로 작용하여, 잦은 GC 멈춤, 할당 지연 등으로 인해 전반적인 애플리케이션 성능이 저하되고 사용자 경험(특히 게임의 부드러움)이 나빠집니다.
5. 메모리 단편화 완화 및 관리 전략
메모리 단편화를 완전히 없애기는 어렵지만, 그 영향을 최소화하기 위한 전략들을 적용할 수 있습니다. 이는 대부분 가비지 생성 최소화 전략과 맥을 같이 합니다.
- 가비지 생성 최소화 (핵심 전략): 근본적으로 메모리 할당 및 해제 빈도를 줄이는 것이 단편화 완화의 핵심입니다. 이전 포스팅에서 다룬 GC 최적화 기법들을 철저히 적용합니다.
- 오브젝트 풀링 (Object Pooling) 적극 활용: 동일하거나 유사한 크기의 객체들을 재사용함으로써, 해당 객체들이 차지했던 공간이 빈번하게 생성되고 해제되는 것을 방지하여 단편화를 크게 줄일 수 있습니다. 총알, 적, 이펙트, UI 요소 등 재사용 가능한 모든 것에 풀링을 고려합니다.
- 결과 캐싱 (Caching): 반복적인
GetComponent
, 배열 반환 API 호출 등을 피해 할당을 줄입니다. StringBuilder
사용: 문자열 연산으로 인한 임시 객체 생성을 방지합니다.- 제네릭 컬렉션 사용 및 박싱 방지: 불필요한 힙 할당을 줄입니다.
- 비할당(Non-Allocating) API 사용: Unity가 제공하는 메모리 할당 없는 API를 우선 사용합니다.
- 메모리 선할당 (Pre-allocation):
- 게임 시작 시나 로딩 화면 등 성능에 덜 민감한 시점에 필요한 메모리(특히 큰 버퍼나 풀)를 미리 할당해 둡니다. 이는 초기에 비교적 단편화되지 않은 힙 공간을 확보하는 데 도움이 될 수 있습니다.
List<T>
등 동적 컬렉션 생성 시, 예상되는 크기를new List<T>(initialCapacity)
와 같이 생성자에 전달하여 내부 배열의 재할당(및 이전 배열의 가비지화) 빈도를 줄입니다.
- 구조체(Struct) 활용 고려: 작고 간단한 데이터 구조는 구조체로 정의하여 스택 할당을 유도하거나(지역 변수 등), 클래스 내부에 포함될 때 힙에 별도의 객체 헤더 없이 값만 저장되도록 하여 힙 사용 및 단편화에 미치는 영향을 줄일 수 있습니다. (단, 큰 구조체의 복사 비용은 주의).
- 메모리 집약적 작업 시점 제어: 대규모 에셋 로딩, 복잡한 씬 전환 등 많은 메모리 할당과 GC 압박을 유발하는 작업은 게임 플레이 중이 아닌 로딩 화면, 메뉴 화면 등에서 수행하도록 타이밍을 조절합니다.
6. Unity 프로파일러를 이용한 단편화 간접 확인
메모리 단편화 정도를 직접적인 수치(예: "단편화율 20%")로 측정하는 것은 Unity 프로파일러만으로는 어렵습니다. 하지만 프로파일러를 통해 단편화로 인해 발생할 수 있는 증상들을 간접적으로 확인할 수 있습니다.
- Memory 프로파일러:
Total Reserved Memory
(OS로부터 할당받은 총 힙 크기)와Used Heap
(실제 사용 중인 힙 크기) 사이의 격차를 주시합니다. GC 이후에도 이 격차가 크고 줄어들지 않거나,Used Heap
이 크게 증가하지 않는데도Total Reserved Memory
가 계속 증가하는 경향은 단편화가 심화되고 있을 가능성을 시사합니다.GC Allocated In Frame
수치를 통해 프레임당 발생하는 가비지 양을 지속적으로 모니터링합니다. 높은 할당률은 결국 단편화로 이어질 가능성이 높습니다.
- CPU Usage 프로파일러:
GC.Collect
멈춤 시간이 비정상적으로 길거나, 힙 사용량이 많지 않은데도 GC가 너무 자주 발생하는 경우, 단편화로 인해 GC 효율이 떨어졌을 가능성을 의심해볼 수 있습니다.
가장 중요한 지표는 결국 프레임당 가비지 할당량(GC Allocated In Frame
) 입니다. 이 수치를 꾸준히 낮게 유지하는 것이 단편화와 GC 멈춤 문제를 모두 예방하는 가장 효과적인 방법입니다.
7. 최신 Unity의 GC와 단편화 관리
Unity는 지속적으로 GC 시스템을 개선하고 있습니다.
- 힙 압축(Heap Compaction) 지원 여부: 최신 Unity 버전 및 특정 플랫폼/스크립팅 백엔드(주로 IL2CPP 관련) 설정에서는 힙 압축을 지원하는 GC 옵션(예: SGen GC의 일부 기능 실험적 도입 논의 등)이 고려되거나 제한적으로 사용 가능할 수 있습니다. 힙 압축은 외부 단편화를 직접적으로 해결하는 가장 효과적인 방법입니다. 프로젝트의 Unity 버전과 타겟 플랫폼에서 지원하는 GC 기능 및 설정 옵션을 공식 문서를 통해 확인하는 것이 중요합니다.
- 증분 GC(Incremental GC): 앞서 언급했듯이, 증분 GC는 GC 멈춤 시간 자체를 줄여 체감 성능을 개선하는 데 중점을 둡니다. 힙 압축 기능이 포함되지 않은 증분 GC라면 단편화 문제를 직접 해결하지는 못하지만, GC로 인한 프레임 드랍 현상을 완화시켜 단편화의 부정적 영향을 줄여줄 수는 있습니다.
핵심: Unity의 GC 기술이 발전하더라도, 개발자 입장에서 불필요한 메모리 할당을 최소화하는 노력은 여전히 가장 중요하고 효과적인 메모리 최적화 전략입니다.
결론
메모리 단편화는 Unity의 관리되는 힙 환경에서 발생할 수 있는 잠재적인 성능 저하 요인으로, 가용 메모리가 있음에도 불구하고 연속된 공간 부족으로 인해 메모리 할당에 문제를 일으키거나 GC 효율을 떨어뜨릴 수 있습니다. 특히 힙 압축 기능이 없는 GC 환경에서는 객체의 생성과 소멸이 반복됨에 따라 외부 단편화가 심화될 가능성이 있습니다. 단편화 문제를 완화하는 가장 효과적인 방법은 근본적으로 불필요한 메모리 할당(가비지 생성)을 최소화하는 것입니다. 오브젝트 풀링, 결과 캐싱, StringBuilder
사용, 비할당 API 활용 등 GC 최적화 기법을 적극적으로 적용하고, 프로파일러를 통해 메모리 할당 패턴과 GC 동작을 꾸준히 모니터링해야 합니다. 최신 Unity 버전의 개선된 GC 기능(증분 GC, 잠재적 힙 압축 지원 등)을 이해하는 것도 도움이 되지만, 개발자의 선제적인 메모리 관리 노력이 안정적이고 성능 좋은 Unity 애플리케이션을 만드는 데 핵심적인 역할을 합니다.