---
title: "DOTween 은 모든 트윈을 배열 하나에 두고 빈칸은 한 번에 메운다"
description: "DOTween 의 TweenManager 는 모든 트윈을 배열 하나에 만든 순서대로 두고, 뺄 때는 null 만 쓴 뒤 다음 갱신에 한 번에 당깁니다. 풀은 타입을 선형으로 찾고, 용량은 넘칠 때 2.5배로 늘며 할당이 몰립니다."
author: "유니얼"
pubDate: 2026-09-25
updatedDate: 2026-10-18
category: "game-dev/unity/libraries"
tags: ["Unity", "라이브러리", "트윈", "GC"]
canonical: https://blog.unialgames.com/posts/unity-dotween-manager/
---
```csharp
for (int i = 0; i < 300; i++)
    coins[i].DOMove(wallet.position, 0.5f);   // 보상 연출 한 프레임
```

돌려받은 트윈을 버려도 트윈은 끝까지 돕니다. 누군가 참조를 쥐고 매 프레임 불러 준다는 뜻이고, 트윈이 한 프레임에 수백 개 생기고 죽으면 그 자료구조가 무엇을 치르는지가 문제가 됩니다.

DOTween 의 `TweenManager` 는 **정적 클래스 하나가 모든 트윈을 활성 배열 하나에 만든 순서대로 두고, 뺄 때는 칸에 `null` 만 쓴 뒤 다음 갱신 때 한 번에 당겨 메웁니다.** 풀은 타입을 선형으로 찾고, 용량은 넘칠 때 한꺼번에 늘어납니다. 기준은 DOTween 1.3.030(GitHub 커밋 `4846808`)입니다.

## 활성 트윈은 배열 하나에 만든 순서대로 있다

`Update`, `LateUpdate`, `FixedUpdate`, 수동 갱신 트윈이 모두 `_activeTweens` 한 배열에 섞여 있습니다. 넣을 때는 늘 끝에 붙이고, 트윈은 자기 칸 번호를 `activeId` 로 기억합니다. 뺄 때는 그 칸에 `null` 을 쓰고 "가장 앞 빈칸" 만 기억해 둡니다. 당기는 일은 다음 추가나 다음 갱신의 시작에서 `ReorganizeActiveTweens` 가 한 번에 합니다.

다섯 칸 [A, B, C, D, E] 에서 B 와 D 가 죽은 경우와, 맨 끝 E 만 죽은 경우입니다.

```csharp
if (totActiveTweens <= 0)
{
    _maxActiveLookupId = -1;               // 살아 있는 트윈이 없다
    return;
}

// 빈칸이 맨 끝 하나뿐이면 끝 번호만 줄인다
if (_reorganizeFromId == _maxActiveLookupId)
{
    _maxActiveLookupId--;
    return;
}

int shift = 1;                             // 앞으로 당길 거리
int len = _maxActiveLookupId + 1;
_maxActiveLookupId = _reorganizeFromId - 1;   // 첫 빈칸 앞까지는 손대지 않는다

for (int i = _reorganizeFromId + 1; i < len; ++i)
{
    Tween t = _activeTweens[i];

    if (t == null)
    {
        shift++;                           // 빈칸을 하나 더 만났다
        continue;
    }

    // 살아 있는 트윈을 shift 칸 앞으로 옮긴다
    t.activeId = _maxActiveLookupId = i - shift;
    _activeTweens[i - shift] = t;
    _activeTweens[i] = null;
}

_requiresActiveReorganization = false;
_reorganizeFromId = -1;
```

`[A, B, C, D, E]` 에서 B 와 D 가 죽어 `[A, _, C, _, E]` 가 됐을 때입니다. 첫 빈칸은 1번입니다.

| 칸 | 만난 것 | `shift` | 배열 |
| --- | --- | --- | --- |
| 시작 | | 1 | `[A, _, C, _, E]` |
| 2 | C. 1번 칸으로 | 1 | `[A, C, _, _, E]` |
| 3 | 빈칸 | 2 | `[A, C, _, _, E]` |
| 4 | E. 2번 칸으로 | 2 | `[A, C, E, _, _]` |

한 번 훑어서 빈칸 둘을 메웠고, 순서는 A, C, E 그대로입니다. 맨 끝 E 만 죽었다면 둘째 `if` 에서 끝 번호만 줄이고 훑지 않습니다.

이 압축은 **순서를 지킵니다.** 맨 뒤 원소를 빈칸에 옮기면 훑지 않고 끝나지만 E 가 C 앞으로 옵니다. DOTween 은 첫 빈칸부터 끝까지를 밀어서, 활성 배열의 순서가 곧 만든 순서이고 매 갱신도 그 순서로 돕니다. 같은 값에 트윈 둘이 걸리면 나중에 만든 쪽이 나중에 setter 를 불러 이기는 이유입니다. 한 프레임에 백 개가 죽어도 미는 일은 한 번입니다.

## 풀은 타입 셋을 위에서부터 훑는다

재활용은 기본으로 꺼져 있어서(`DOTween.defaultRecyclable = false`) 보통은 풀이 비어 있습니다. 켜면 죽은 트위너가 `_pooledTweeners` 배열에 들어가는데, 트위너는 제네릭 인자가 제각각이라 "아무거나 하나" 를 꺼낼 수 없습니다. 생성자가 적어 둔 `Type` 세 개를 맨 위 칸부터 비교합니다.

```csharp
if (totPooledTweeners > 0)
{
    // 풀을 맨 위 칸부터 내려가며 타입 셋이 모두 같은 것을 찾는다
    for (int i = maxPooledId; i >= minPooledId; --i)
    {
        Tween tween = _pooledTweeners[i];

        if (tween != null && 타입 셋이 모두 같다)
        {
            AddActiveTween(tween);
            _pooledTweeners[i] = null;

            // 끝 칸일 때만 범위를 좁힌다. 가운데 칸이면 범위 안에 빈칸이 남는다
            if (i == maxPooledId) maxPooledId--;
            else if (i == minPooledId) minPooledId++;

            totPooledTweeners--;
            return tween;                  // new 없음
        }
    }

    // 맞는 것이 없고 한도에 닿았으면 풀의 하나를 버려 자리를 만든다
    if (totTweeners >= maxTweeners)
    {
        _pooledTweeners[maxPooledId--] = null;
        totPooledTweeners--;
        totTweeners--;
    }
}
else if (totTweeners >= maxTweeners - 1)
{
    IncreaseCapacities(TweenersOnly);      // 한도를 늘리고 경고 로그를 남긴다
}

var t = new TweenerCore<T1, T2, TPlugOptions>();
totTweeners++;
AddActiveTween(t);
return t;
```

| 상황 | 지나는 길 | 결과 |
| --- | --- | --- |
| float 요청, 풀 `[V3, float, V3]` | 위에서 둘째 칸에서 찾음 | 풀에서 꺼냄. 가운데 칸이라 빈칸이 남음 |
| Color 요청, 같은 풀 | 풀 셋을 다 헛돈 뒤 | `new` |
| 풀이 비었고 200번째 트위너 | 한도 200 에 닿음 | 한도를 500 으로 늘리고 `new`. `Max Tweens reached: 200/50 → 500/50` 경고 |

재활용이 꺼져 있으면 풀은 늘 비어 있어 셋째 줄의 길만 탑니다.

맞는 타입이 없으면 풀을 끝까지 돈 뒤에 `new` 합니다. float 트윈 1,000개를 한 프레임에 만드는 시간을 풀에 든 것에 따라 비교했습니다.

| 풀에 든 것 | Mono | IL2CPP |
| --- | --- | --- |
| float 트윈 | 0.12 ms | 0.30 ms |
| Vector3 트윈 1,000개 | 5.3 ms | 7.6 ms |

Vector3 가 깔린 풀에서는 할당도 풀이 빈 경우와 같은 1개당 344 / 352 B 였습니다. 헛돈 끝에 결국 만드니 **할당은 하나도 아끼지 못하고 시간만 25~45배**를 씁니다. 시퀀스 풀은 타입이 하나라 `Stack` 에서 `Pop` 한 번이면 됩니다.

## 용량은 넘칠 때 한꺼번에 2.5배가 된다

기본 용량은 트위너 200, 시퀀스 50 입니다. 지금 있는 트위너(시퀀스 안의 자식 포함)가 `maxTweeners - 1` 개일 때 하나를 더 만들면 용량을 늘립니다.

```csharp
// DOTween/Core/TweenManager.cs (IncreaseCapacities 줄임)
int increaseTweenersBy = Mathf.Max((int)(maxTweeners * 1.5f), _DefaultMaxTweeners);
maxTweeners += increaseTweenersBy;
Array.Resize(ref _pooledTweeners, maxTweeners);
Array.Resize(ref _activeTweens, maxActive);
if (killAdd > 0) _KillList.Capacity += killAdd;
```

지금의 1.5배를 **더하므로** 200 → 500 → 1,250 → 3,125 입니다. 그때마다 풀 배열, 활성 배열, 파기 목록을 새로 잡아 복사하고, 설정 에셋의 기본 로그 수준(Default)에서는 `Max Tweens reached` 경고도 찍습니다. 늘어난 프레임의 순 할당입니다.

| 경계 | 로그 수준 | 늘어난 프레임 할당 | 만드는 호출 시간 |
| --- | --- | --- | --- |
| 200 → 500 | ErrorsOnly | 14.1 / 14.1 KB | 0.03 / 0.04 ms |
| 200 → 500 | Default | 21.9 / 24.2 KB | 0.84 / 0.28 ms |
| 500 → 1,250 | ErrorsOnly | 32.1 / 32.1 KB | 0.02 / 0.03 ms |
| 1,250 → 3,125 | ErrorsOnly | 77.1 / 77.1 KB | 0.07 / 0.08 ms |

ErrorsOnly 줄은 새로 잡은 참조 칸 수 × 8 B 와 맞고(200 → 500 이면 1,600 칸, 12.8 KB), Default 와의 차이는 경고 로그 몫입니다. 프레임 간격은 늘지 않은 프레임과 구분되지 않았습니다. **한 번의 확장이 치르는 것은 한 프레임에 몰리는 할당**입니다. 한 번 늘어난 용량은 줄지 않고, 갱신은 용량이 아니라 마지막으로 찬 칸까지만 훑으므로 큰 용량이 매 프레임 비용이 되지는 않습니다(`SetTweensCapacity(20000, 200)` 에 트윈 0개인 프레임이 기준선과 같았습니다).

## 주의할 점

### 1. 용량은 게임 시작 때 넉넉하게 한 번 잡는다

**안 지키면 생기는 문제.** 기본 용량 200 은 금방 찹니다. 트윈이 몰리는 연출 프레임에 경계를 넘으면 배열 셋의 재할당(200 경계에서 14~24 KB, 1,250 경계에서 77 KB)과 경고 로그가 그 프레임에 한꺼번에 얹힙니다.

**이렇게 쓴다.** Utility Panel 의 Editor Report(`showUnityEditorReport`)를 켜고 플레이를 끝내면 동시에 활성이었던 최대 수가 찍힙니다. 이 수에는 시퀀스 자식이 빠져 있으니 자식 수를 더해 넉넉하게 `DOTween.Init().SetCapacity(t, s)` 로 시작할 때 잡습니다.

### 2. 풀에 여러 타입을 쌓아 두지 않는다

**안 지키면 생기는 문제.** 트위너 풀은 맨 위 칸부터 `Type` 셋을 비교하는 선형 탐색입니다. Vector3 트윈이 천 개 깔린 풀에서 float 트윈을 요청하면 천 칸을 돈 뒤 결국 새로 만들어, 할당은 그대로이고 시간만 수십 배가 됩니다.

**이렇게 쓴다.** 재활용은 전역으로 켜기보다 같은 종류를 대량으로 반복하는 트윈에만 `.SetRecyclable(true)` 로 붙입니다.

### 3. 트윈이 도는 도중에 SetTweensCapacity 를 부르지 않는다

**안 지키면 생기는 문제.** `SetTweensCapacity` 는 받은 값에 맞춰 배열을 `Array.Resize` 하므로 용량을 줄이기도 합니다. 소스 주석도 "use this method only when there are no tweens running" 이라고 적습니다. 게임 도중 부르면 도는 트윈이 있는 배열을 다시 잡습니다.

**이렇게 쓴다.** 용량은 첫 트윈을 만들기 전, `DOTween.Init` 과 함께 정합니다.

## 정리

`TweenManager` 는 **모든 트윈을 배열 하나에 만든 순서대로 두고, 빈칸은 다음 갱신에 한 번에 메웁니다.**

- 넣을 때는 끝에 붙이고, 뺄 때는 칸에 `null` 만 씁니다. 압축은 순서를 지켜서 갱신 순서가 곧 만든 순서입니다.
- 풀은 타입 셋을 위에서부터 훑는 선형 탐색이라, 타입이 섞이면 할당은 못 아끼고 시간만 씁니다.
- 용량은 200 에서 넘칠 때마다 2.5배가 되며 그 프레임에 할당이 몰립니다. 시작할 때 한 번 잡으면 매 프레임 비용 없이 피합니다.