---
title: "증분 GC 는 일을 여러 프레임에 나눈다"
description: "기본으로 켜져 있고 프레임 튐을 줄여 줍니다. 다만 없애 주지는 않고, 공짜도 아닙니다."
author: "유니얼"
pubDate: 2026-09-09
category: "game-dev/unity/runtime"
tags: ["Unity", "최적화", "GC"]
canonical: https://blog.unialgames.com/posts/unity-incremental-gc/
---
앞에서 Boehm 이 힙 전체를 훑는다고 했습니다. 힙이 크면 그 한 번이 깁니다.
그동안 우리 코드는 멈춰 있고, 그 프레임이 통째로 늦습니다.

GC 를 빠르게 만드는 것은 우리 손 밖입니다. 그런데 **한 번에 다 하지 않게** 할 수는 있습니다.

> **30밀리초짜리 한 번 대신 3밀리초짜리 열 번이면 화면에서는 안 걸립니다. 총 시간은 오히려 늘어납니다.**

유니티는 이걸 기본으로 켜 둡니다. `Project Settings > Player > Use Incremental GC` 입니다.

## 무엇을 쪼개나

GC 가 하는 일은 크게 둘입니다. 살아 있는 것을 표시하고, 나머지를 쓸어 담습니다.

쪼갤 수 있는 것은 **표시하는 쪽**입니다.
뿌리에서 출발해 참조를 따라가다가 중간에 멈추고, 다음 프레임에 그 자리부터 이어서 갑니다.
앞에서 본 `yield return` 이 하던 것과 구조가 같습니다. 하다 멈추고 그 자리부터 다시 갑니다.

<x-compare>
<script type="application/json">
{
  "tabs": [
    {
      "label": "증분 끄면",
      "code": "프레임 1:  [내 코드 3ms]\n프레임 2:  [내 코드 3ms]\n프레임 3:  [내 코드 3ms][GC 25ms]  ← 여기서 걸림\n프레임 4:  [내 코드 3ms]",
      "stats": [["가장 나쁜 프레임", "28 ms"], ["GC 총 시간", "25 ms"]],
      "tone": "bad"
    },
    {
      "label": "증분 켜면",
      "code": "프레임 1:  [내 코드 3ms][표시 2ms]\n프레임 2:  [내 코드 3ms][표시 2ms]\n프레임 3:  [내 코드 3ms][표시 2ms]\n...\n프레임 15: [내 코드 3ms][쓸어담기 3ms]",
      "stats": [["가장 나쁜 프레임", "6 ms"], ["GC 총 시간", "30 ms 이상"]],
      "tone": "good"
    }
  ]
}
</script>
</x-compare>

**총 시간은 늘어납니다.** 매번 멈췄다 다시 시작하는 비용이 붙기 때문입니다.
그런데 게임에서 중요한 것은 총합이 아니라 **가장 나쁜 프레임**입니다.
평균 프레임레이트가 조금 떨어져도 걸리는 순간이 없는 쪽이 훨씬 낫습니다.

## 왜 공짜가 아닌가

여기가 핵심입니다. 표시하다 멈추면 문제가 하나 생깁니다.

멈춰 있는 동안 **우리 코드가 참조를 바꿉니다.**

이미 다 봤다고 표시해 둔 객체 A 가 있는데, 우리 코드가 거기에 새 객체 B 를 매답니다.
GC 입장에서 A 는 이미 검사가 끝난 것이라 다시 안 봅니다. 그럼 B 는 아무도 안 가리키는 것으로 보입니다.
**살아 있는 객체가 지워집니다.**

그래서 증분 GC 는 **쓰기 장벽**을 답니다.
참조를 대입하는 코드마다 "지금 뭔가 바뀌었다" 고 GC 에 알리는 코드가 끼어 들어갑니다.

```csharp
_적 = new 적();     // 이 대입 한 줄에 알림 코드가 같이 들어갑니다
```

그래서 증분 GC 를 켜면 **참조를 대입하는 모든 코드가 아주 조금씩 느려집니다.**
GC 가 안 도는 순간에도 그렇습니다. 이게 총 시간이 늘어나는 이유의 절반입니다.

## 없애 주지는 않는다

증분 GC 에는 확실한 한계가 하나 있고, 유니티 문서도 이걸 명시합니다.

**참조가 계속 바뀌면 표시가 영영 안 끝납니다.**

쓰기 장벽이 알려 준 자리를 다시 검사해야 하는데, 그 사이에 또 바뀌면 또 검사해야 합니다.
매 프레임 참조를 크게 뒤흔드는 코드가 있으면 표시 작업이 계속 늘어나기만 합니다.

그러면 유니티는 포기하고 **전체 수집으로 떨어집니다.** 멈추고 한 번에 끝냅니다.
증분을 켜 뒀는데도 가끔 크게 튀는 프레임이 보인다면 이것일 수 있습니다.

즉 증분 GC 는 튐을 **줄여 주는** 것이지 **없애 주는** 것이 아닙니다.
할당이 많으면 여전히 튑니다. 근본 대응은 그대로입니다. 할당을 줄이는 것입니다.

## 언제 꺼야 하나

기본으로 켜 두는 것이 맞습니다. 끄는 것을 검토할 만한 경우는 좁습니다.

- **프레임 튐보다 총 처리량이 중요한 경우.** 프레임이라는 개념이 옅은 도구나 서버 쪽
- **참조를 매 프레임 크게 뒤흔드는 구조라** 어차피 전체 수집으로 떨어지는 경우.
  그러면 쓰기 장벽 비용만 내고 이득이 없습니다

그리고 켜고 끄는 것 말고 세 번째 선택지가 있습니다.

```csharp
GarbageCollector.GCMode = GarbageCollector.Mode.Disabled;
// 여기서는 GC 가 안 돕니다
GarbageCollector.GCMode = GarbageCollector.Mode.Enabled;
```

**GC 를 잠깐 꺼 두는 것**입니다. 전투 한 판이나 컷신처럼 절대 걸리면 안 되는 구간에 씁니다.
대신 그동안 힙은 계속 늘어나기만 합니다. 앞에서 봤듯이 한 번 늘어난 힙은 안 줄어듭니다.

그래서 이건 **끝이 정해진 짧은 구간**에서만 씁니다.
켜 두는 것을 깜빡하면 힙이 끝없이 커지다 메모리가 터집니다.
꺼 둔 구간이 끝나면 로딩 화면처럼 어차피 멈춰 있는 자리에서 한 번 치우는 것이 짝입니다.

## 정리

- 증분 GC 는 **표시 작업을 여러 프레임에 나눕니다.** 유니티 기본값입니다
- 총 시간은 오히려 늘어납니다. 대신 **가장 나쁜 프레임**이 짧아집니다
- 멈춘 사이에 참조가 바뀌는 것을 잡으려고 **쓰기 장벽**을 답니다.
  참조를 대입하는 코드가 전부 아주 조금씩 느려집니다
- **참조가 계속 바뀌면 표시가 안 끝나서 전체 수집으로 떨어집니다.** 튐이 사라지지는 않습니다
- `GarbageCollector.GCMode` 로 짧은 구간 동안 아예 꺼 둘 수 있습니다. 힙이 늘어나는 것은 감수해야 합니다