유니티 가비지 컬렉션은 성능 문제를 이해하기 위해 반드시 알아야 하는 핵심 개념입니다.
이전 글에서는 유니티 GC가 프레임 드랍을 만들 수 있는 이유를 정리했습니다.
이번 글에서는 한 단계 더 들어가서 유니티 가비지 컬렉션이 실제로 어떻게 동작하는지를 살펴보겠습니다.
GC를 단순히 “메모리를 정리해주는 기능” 정도로만 이해하면, 왜 성능 문제가 발생하는지 정확히 알기 어렵습니다.
그래서 이번 글에서는 다음 내용을 중심으로 정리합니다.
- Managed Heap이 무엇인지
- GC가 어떤 객체를 정리하는지
- Mark & Sweep 방식이 무엇인지
- 왜 GC가 실행될 때 게임이 잠시 멈출 수 있는지
👉 GC의 동작 원리를 이해하면, 왜 작은 메모리 할당도 반복되면 문제가 되는지 알 수 있습니다.
Managed Heap이란 무엇인가
유니티는 C#을 사용합니다.
C#에서 class로 만든 객체는 일반적으로 Managed Heap이라는 메모리 영역에 할당됩니다.
Managed Heap은 말 그대로 관리되는 힙 메모리입니다.
여기서 “관리된다”는 말은 개발자가 직접 메모리를 해제하지 않아도 된다는 의미입니다.
C++에서는 new로 만든 객체를 직접 delete해야 하는 경우가 많습니다.
하지만 C#에서는 더 이상 사용하지 않는 객체를 GC가 찾아서 자동으로 정리합니다.
이 구조 덕분에 개발자는 메모리 해제 실수를 줄일 수 있습니다.
하지만 편리함에는 비용이 있습니다.
👉 누군가는 사용하지 않는 객체를 찾아야 하고, 그 일을 GC가 수행합니다.
GC는 어떤 객체를 정리하는가
GC는 아무 객체나 무작정 정리하지 않습니다.
기준은 단순합니다.
“아직 접근 가능한 객체인가?”
프로그램에서 더 이상 접근할 수 없는 객체는 사용되지 않는 객체라고 판단할 수 있습니다.
예를 들어 어떤 객체를 생성했지만, 그 객체를 가리키는 참조가 더 이상 없다면 해당 객체는 사용할 방법이 없습니다.
이런 객체는 메모리만 차지하고 있는 상태입니다.
GC는 이런 객체를 찾아서 정리합니다.
반대로 아직 변수, 필드, 리스트, 배열 등에서 참조하고 있는 객체는 살아있는 객체로 판단합니다.
즉, GC는 객체가 필요한지 아닌지를 “의도”로 판단하지 않습니다.
참조가 남아 있는지 여부를 기준으로 판단합니다.
Mark & Sweep 방식
유니티 가비지 컬렉션의 전체 동작 흐름을 그림으로 보면 다음과 같습니다.

👉 이 과정을 이해하면 GC가 왜 성능 문제를 만드는지 바로 연결됩니다.
GC 동작 방식은 여러 가지가 있지만, 기본적인 개념은 Mark & Sweep으로 이해할 수 있습니다.
이름 그대로 두 단계로 나누어 생각하면 됩니다.
- Mark: 아직 사용 중인 객체를 표시합니다.
- Sweep: 표시되지 않은 객체를 정리합니다.
먼저 GC는 프로그램에서 접근 가능한 객체들을 따라가며 “살아있는 객체”를 표시합니다.
이 과정을 Mark 단계라고 합니다.
그 다음 Managed Heap을 확인하면서 표시되지 않은 객체를 제거합니다.
이 과정을 Sweep 단계라고 합니다.
쉽게 비유하면 방 정리와 비슷합니다.
먼저 아직 쓰는 물건에는 표시를 해둡니다.
그리고 표시가 없는 물건은 버립니다.
GC도 비슷한 방식으로 사용 중인 객체와 사용되지 않는 객체를 구분합니다.
왜 GC가 실행되면 멈추는가
문제는 GC가 이 작업을 수행하는 동안 게임의 실행 흐름에 영향을 줄 수 있다는 점입니다.
GC는 살아있는 객체를 찾기 위해 참조 관계를 따라가야 합니다.
그리고 사용되지 않는 객체를 정리해야 합니다.
이 과정은 공짜가 아닙니다.
특히 Managed Heap에 객체가 많거나, 참조 관계가 복잡하거나, 할당이 자주 발생했다면 GC가 해야 할 일도 많아집니다.
이때 메인 스레드가 잠시 멈추면 플레이어는 그것을 끊김으로 느낍니다.
평균 FPS가 높아도 GC가 실행되는 순간 프레임 시간이 튀면 체감 성능은 나빠질 수 있습니다.
👉 GC 문제는 평균 FPS보다 프레임 안정성 문제에 가깝습니다.
작은 할당이 반복되면 왜 위험한가
많은 분들이 처음에는 이렇게 생각합니다.
“작은 객체 하나 만드는 게 그렇게 큰 문제일까?”
한 번만 보면 큰 문제가 아닐 수 있습니다.
하지만 게임은 매 프레임 반복되는 프로그램입니다.
Update에서 작은 객체를 계속 생성하면 그 할당은 빠르게 누적됩니다.
예를 들어 60FPS 게임에서 매 프레임 작은 문자열이나 임시 배열을 생성한다고 생각해봅시다.
1초에 60번, 1분이면 3600번의 할당이 발생합니다.
이런 할당이 쌓이면 Managed Heap에 사용되지 않는 객체가 계속 생기고, 결국 GC가 실행됩니다.
그래서 유니티에서는 “작은 할당”보다 “반복되는 할당”을 더 조심해야 합니다.
👉 특히 Update, LateUpdate, FixedUpdate 안에서 발생하는 할당은 반드시 주의해야 합니다.
GC가 항상 나쁜 것은 아니다
GC는 나쁜 기능이 아닙니다.
오히려 개발자가 직접 메모리를 관리하지 않아도 되게 해주는 매우 중요한 기능입니다.
문제는 GC가 게임 플레이 중 예측하기 어려운 타이밍에 실행될 때입니다.
로딩 화면, 씬 전환, 메뉴 화면처럼 플레이어가 프레임 끊김을 덜 민감하게 느끼는 구간이라면 GC 비용이 상대적으로 덜 문제가 될 수 있습니다.
하지만 전투 중, 점프 중, 카메라가 빠르게 움직이는 중에 GC가 실행되면 플레이어는 바로 끊김을 느낄 수 있습니다.
따라서 목표는 GC를 완전히 없애는 것이 아닙니다.
👉 불필요한 할당을 줄이고, GC가 민감한 순간에 발생하지 않도록 관리하는 것입니다.
정리
이번 글에서는 유니티 가비지 컬렉션의 기본 동작 원리를 정리했습니다.
- C# 객체는 Managed Heap에 할당됩니다.
- GC는 더 이상 접근할 수 없는 객체를 정리합니다.
- Mark 단계에서는 살아있는 객체를 표시합니다.
- Sweep 단계에서는 표시되지 않은 객체를 제거합니다.
- GC가 실행되면 프레임 시간이 튈 수 있습니다.
- 반복되는 작은 할당은 GC 발생 가능성을 높입니다.
GC를 이해할 때 가장 중요한 것은 이것입니다.
GC는 메모리를 자동으로 정리해주지만, 그 정리 작업에도 비용이 듭니다.
그래서 유니티 프로젝트에서는 메모리 할당을 무심코 반복하지 않도록 주의해야 합니다.
다음 글에서 다룰 내용
다음 글에서는 실제로 유니티에서 GC를 자주 발생시키는 코드 패턴을 살펴보겠습니다.
👉 유니티 GC를 발생시키는 코드 패턴 (Update에서 하면 위험한 것들)
특히 new, 문자열 연산, LINQ, 박싱, 임시 컬렉션 생성 같은 실전 문제를 중심으로 정리하겠습니다.