---
title: "DOTween 은 트윈을 만들 때만 할당한다"
description: "DOTween 의 DOMove 한 번은 캡처 객체, 델리게이트 둘, 트윈 객체로 656 B 를 할당하고, 재활용은 기본으로 꺼져 있으며 켜도 람다 몫 280 B 가 남습니다. 도는 동안은 0 B 라, 줄이는 길은 만드는 횟수를 줄이는 것입니다."
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-alloc/
---
```csharp
void OnClick(Vector3 dest)
{
    transform.DOMove(dest, 0.3f).SetEase(Ease.OutQuad);
}
```

"DOTween 은 트윈을 풀링하니까 할당이 없다" 는 말을 믿고 이벤트마다 트윈을 만들면, 프로파일러의 GC Alloc 열에 클릭할 때마다 수백 바이트가 찍힙니다.

DOTween 의 비용은 **만들 때 할당이 몰리고, 도는 동안에는 할당이 없는** 모양입니다. 재활용은 기본으로 꺼져 있고, 켜도 람다 몫은 매번 생깁니다. 기준은 DOTween 1.3.030(GitHub 커밋 `4846808`)이고, 할당은 `ProfilerRecorder` 의 "GC Allocated In Frame" 으로 쟀습니다.

## DOMove 한 번은 힙 객체 넷을 만든다

`DOMove` 는 `target` 을 붙잡는 두 람다를 `DOTween.To` 에 넘깁니다. 컴파일러는 붙잡힌 변수를 담은 캡처 객체 하나를 두 람다가 함께 쓰게 만들고, 거기에 델리게이트 둘과 트윈 객체가 더해집니다. 설치된 DLL 의 `DOMove` IL 에도 `newobj` 가 셋(캡처 클래스, `DOGetter<Vector3>`, `DOSetter<Vector3>`)입니다. 트윈 객체는 `TweenManager.GetTweener` 에서 갈립니다.

```csharp
// ShortcutExtensions.DOMove
// 람다 둘이 target 을 붙잡는다. 캡처 객체 1개, DOGetter 1개, DOSetter 1개가 여기서 생긴다
var t = DOTween.To(() => target.position, x => target.position = x, endValue, duration);

// DOTween.ApplyTo: 트윈 객체를 매니저에게 청한다
var tweener = TweenManager.GetTweener<T1, T2, TPlugOptions>();

// TweenManager.GetTweener
if (totPooledTweeners > 0)                       // 재활용이 꺼져 있으면 풀은 늘 비어 있다
{
    for (int i = _maxPooledTweenerId; i > _minPooledTweenerId - 1; --i)
    {
        Tween tween = _pooledTweeners[i];

        // 세 타입이 모두 같은 트윈을 찾으면 그것을 다시 쓴다
        if (tween != null && tween.typeofT1 == typeofT1 && tween.typeofT2 == typeofT2
            && tween.typeofTPlugOptions == typeofTPlugOptions)
        {
            t = (TweenerCore<T1, T2, TPlugOptions>)tween;
            AddActiveTween(t);
            _pooledTweeners[i] = null;
            totPooledTweeners--;
            return t;                            // new 없음
        }
    }
}

t = new TweenerCore<T1, T2, TPlugOptions>();     // 풀에 없으면 새로 만든다
totTweeners++;
AddActiveTween(t);
return t;

// 다시 DOTween.ApplyTo: 델리게이트를 필드에 넣고 기본값을 채운다. 새로 만드는 것은 없다
bool setupSuccessful = Tweener.Setup(tweener, getter, setter, endValue, duration, plugin);

// 다시 DOMove
t.SetOptions(snapping).SetTarget(target);
```

| | 람다 쪽 | 트윈 객체 | 새 힙 객체 |
| --- | --- | --- | --- |
| 재활용 끔 (기본) | 캡처, getter, setter 3개 | 새로 만듦 | 4개 (656 B) |
| 재활용 켬, 풀에 같은 타입 | 캡처, getter, setter 3개 | 풀에서 꺼냄 | 3개 (280 B) |

재활용을 켜도 람다는 호출마다 새로 만듭니다. 줄어드는 것은 트윈 객체 하나입니다.

한 프레임에 트윈 100개를 만들어 한 개당으로 나눈 값입니다.

| 경로 | 에디터 배치모드 | Mono 플레이어 | IL2CPP 플레이어 |
| --- | --- | --- | --- |
| `DOMove`, 재활용 OFF (기본) | **656 B** | **656 B** | **656 B** |
| `DOMove`, 재활용 ON, 풀이 찬 둘째 회 | **280 B** | **280 B** | **280 B** |

차이 376 B 가 트윈 객체이고, **280 B 는 재활용을 켜도 남는 캡처 객체와 델리게이트 둘**입니다. 세 조건이 바이트 단위로 같아서, 이 크기는 백엔드가 아니라 객체 구성에서 나옵니다. 손으로 쓴 `Update` 보간은 필드에 값을 넣을 뿐 시작할 때 아무것도 만들지 않고, 코루틴은 시작할 때마다 이터레이터 객체를 만듭니다.

## 도는 동안에는 할당하지 않는다

매 프레임 경로(갱신 루프 → 위치 계산 → 플러그인 → setter)에는 `new` 가 없습니다. 값은 제네릭 필드와 인자로 흐르고, 콜백은 이미 있는 델리게이트를 부릅니다. 플레이어에서 `DOMove` 10,000개가 도는 프레임의 할당 중앙값이 트윈 없는 프레임과 같은 686 B(Mono), 722 B(IL2CPP)였습니다. 트윈이 더한 몫은 0 B 입니다.

프레임 시간은 트윈 수에 비례합니다(서로 다른 `Transform` 에 건 `DOMove`, PlayerLoop 중앙값의 기준선 대비 증가분).

| 동시 트윈 수 | Mono 플레이어 | 트윈 하나당 | IL2CPP 플레이어 | 트윈 하나당 |
| --- | --- | --- | --- | --- |
| 1,000 | +0.194 ms | 194 ns | +0.100 ms | 100 ns |
| 10,000 | +1.990 ms | 199 ns | +1.384 ms | 138 ns |

에디터 배치모드에서도 10,000개에서 트윈당 178 ns 였습니다. 할당이 문제가 되는 것은 트윈이 많을 때가 아니라 **트윈을 자주 만들 때**입니다.

## 트윈 하나를 들고 다시 쓰면 0 B 다

재활용을 켜도 람다 몫이 남으니, 할당을 없애는 길은 만드는 호출 자체를 줄이는 것입니다. 되풀이되는 연출은 트윈 하나를 살려 두고 목표만 바꿉니다.

```csharp
TweenerCore<Vector3, Vector3, VectorOptions> _move;   // Tweener 가 아니라 TweenerCore 로 든다

void Awake()
{
    _move = transform.DOMove(transform.position, 0.3f)
        .SetEase(Ease.OutQuad).SetAutoKill(false).SetLink(gameObject).Pause();
}

public void MoveTo(Vector3 dest)
{
    _move.ChangeEndValue(dest, true);   // 소스가 "NO-GC METHOD" 라고 적은 제네릭 오버로드
    _move.Restart();
}
```

<x-compare>
<script type="application/json">
{
  "label": "같은 이동을 반복할 때 한 번에 만드는 것",
  "tabs": [
    {
      "label": "매번 DOMove",
      "code": "void MoveTo(Vector3 dest)\n{\n    transform.DOMove(dest, 0.3f);\n}",
      "stats": [
        [
          "새 트윈 객체",
          "매번"
        ],
        [
          "새 람다",
          "캡처 1 + 델리게이트 2"
        ],
        [
          "실측",
          "656 B"
        ],
        [
          "옛 참조 위험",
          "없음"
        ]
      ],
      "tone": "bad"
    },
    {
      "label": "재활용 ON",
      "code": "DOTween.Init(recycleAllByDefault: true);\n\nvoid MoveTo(Vector3 dest)\n{\n    transform.DOMove(dest, 0.3f);\n}",
      "stats": [
        [
          "새 트윈 객체",
          "풀에 있으면 없음"
        ],
        [
          "새 람다",
          "캡처 1 + 델리게이트 2"
        ],
        [
          "실측",
          "280 B"
        ],
        [
          "옛 참조 위험",
          "있음. OnKill 에서 참조를 끊어야 한다"
        ]
      ]
    },
    {
      "label": "하나를 들고 재사용",
      "code": "_move.ChangeEndValue(dest, true);\n_move.Restart();",
      "stats": [
        [
          "새 트윈 객체",
          "없음"
        ],
        [
          "새 람다",
          "없음"
        ],
        [
          "실측",
          "0 B"
        ],
        [
          "옛 참조 위험",
          "없음. autoKill 을 껐다"
        ]
      ],
      "tone": "good"
    }
  ]
}
</script>
</x-compare>

세 방식을 100회씩 되풀이한 1회당 할당은 656 B, 280 B, **0 B** 였습니다. 필드를 `Tweener` 로 선언하면 `ChangeEndValue(object ...)` 가 골라져 `Vector3` 박싱으로 **28 B** 가 생겼습니다. 트윈을 들고 있을 수 없는 자리에서는 델리게이트 둘을 `Awake` 에서 한 번 만들어 `DOTween.To(_get, _set, dest, 0.3f).SetRecyclable(true)` 로 넘기면, 풀이 찬 상태에서 1회당 0.2 B(Mono), 0.0 B(IL2CPP)였습니다.

## 주의할 점

### 1. "DOTween 은 풀링한다" 를 믿고 할당을 재지 않고 넘어가지 않는다

**안 지키면 생기는 문제.** 재활용은 기본으로 꺼져 있어서(`DOTween.defaultRecyclable = false`) 설정을 건드리지 않은 프로젝트에서는 `DOMove` 한 번이 656 B 입니다. 켜도 280 B 가 남습니다. 이벤트마다 트윈을 만드는 코드가 자주 불리는 자리에서 GC 가 치울 거리를 계속 쌓습니다.

**이렇게 쓴다.** 프로파일러의 GC Alloc 열에서 트윈을 만드는 프레임을 직접 봅니다. 자주 불리는 연출은 트윈 하나를 `SetAutoKill(false)` 로 살려 두고 `ChangeEndValue` 와 `Restart` 로 다시 씁니다.

### 2. 다시 쓸 트윈 참조를 Tweener 로 선언하지 않는다

**안 지키면 생기는 문제.** `Tweener` 에는 AOT 때문에 `object` 를 받는 `ChangeEndValue` 만 보여서, 값 타입 목표를 넘길 때마다 박싱이 일어납니다. 할당을 없애려고 트윈을 재사용했는데 여기서 1회당 28 B 가 다시 생깁니다.

**이렇게 쓴다.** 필드를 숏컷의 반환 타입 그대로 `TweenerCore<Vector3, Vector3, VectorOptions>` 로 선언하거나 `var` 로 받습니다. 제네릭 `ChangeEndValue(T2 ...)` 가 골라집니다.

### 3. 매 프레임 트윈을 새로 만들지 않는다

**안 지키면 생기는 문제.** 마우스를 따라가게 하려고 `Update` 안에서 `DOMove` 를 부르면 프레임마다 트윈 객체와 람다를 만들고, 앞 트윈과 새 트윈이 같은 위치를 두고 겹칩니다. 60 fps 면 초당 약 39 KB 가 쌓이고 동시 트윈 수가 늘어납니다.

**이렇게 쓴다.** 목표가 매 프레임 바뀌는 추적은 손으로 쓴 `Update` 보간(`Vector3.MoveTowards`, `SmoothDamp`)으로 합니다. 트윈을 꼭 써야 하면 하나를 들고 `ChangeEndValue(target, true)` 로 목표만 바꿉니다.

## 정리

DOTween 은 **트윈을 만들 때만 할당하고, 도는 동안에는 할당하지 않습니다.**

- `DOMove` 한 번은 캡처 객체, 델리게이트 둘, 트윈 객체로 656 B 입니다. 재활용은 기본 꺼짐이고, 켜도 람다 몫 280 B 는 남습니다.
- 도는 동안의 할당은 0 B 이고, 프레임 시간은 트윈 수에 비례해 트윈당 약 100~200 ns 입니다.
- 트윈 하나를 `TweenerCore` 로 들고 `ChangeEndValue` 와 `Restart` 로 다시 쓰면 1회당 0 B 입니다.