---
title: "호출을 객체로 만들면 저장할 수 있게 된다"
description: "메서드는 부르고 나면 아무것도 남지 않습니다. 부르라는 지시를 객체로 만들어 두면 되돌리기, 기록, 리플레이, 전송이 한꺼번에 딸려 옵니다."
author: "유니얼"
pubDate: 2026-09-09
category: "game-dev/design-patterns"
tags: ["Unity"]
canonical: https://blog.unialgames.com/posts/unity-command/
---
레벨 에디터를 만들고 있습니다. 오브젝트를 옮기고, 지우고, 색을 바꿉니다.
그리고 되돌리기를 붙여야 합니다.

```csharp
selected.transform.position = newPos;
```

이 한 줄을 실행한 뒤에 되돌릴 방법이 있을까요. 없습니다.
**실행이 끝나면 아무것도 남지 않기 때문**입니다.
무엇을 했는지도, 하기 전에 어땠는지도 어디에도 적혀 있지 않습니다.

되돌리려면 두 가지가 남아 있어야 합니다.

- 무엇을 했는가
- 하기 전에 어땠는가

메서드 호출은 둘 다 남기지 않습니다. **그래서 호출 자체를 물건으로 만듭니다.**

## 커맨드는 이렇게 생겼다

```csharp
public interface ICommand
{
    void Execute();
    void Undo();
}
```

```csharp
public class MoveCommand : ICommand
{
    readonly Transform target;
    readonly Vector3 from;    // 하기 전에 어땠는가
    readonly Vector3 to;      // 무엇을 할 것인가

    public MoveCommand(Transform target, Vector3 to)
    {
        this.target = target;
        this.from = target.position;   // 만들 때 지금 상태를 적어 둔다
        this.to = to;
    }

    public void Execute() => target.position = to;
    public void Undo()    => target.position = from;
}
```

핵심은 생성자입니다. **명령을 만드는 시점에 "지금 어땠는지" 를 자기 안에 적어 둡니다.**
이게 있어야 나중에 되돌릴 수 있습니다.

부르는 쪽은 명령을 만들고, 실행하고, 쌓아 둡니다.

```csharp
public class CommandInvoker
{
    readonly Stack<ICommand> done = new();
    readonly Stack<ICommand> undone = new();

    public void Execute(ICommand command)
    {
        command.Execute();
        done.Push(command);
        undone.Clear();      // 새 작업을 하면 다시하기 기록은 버린다
    }

    public void Undo()
    {
        if (done.Count == 0) return;
        var c = done.Pop();
        c.Undo();
        undone.Push(c);
    }

    public void Redo()
    {
        if (undone.Count == 0) return;
        var c = undone.Pop();
        c.Execute();
        done.Push(c);
    }
}
```

<x-steps>
<script type="application/json">
{
  "steps": [
    { "tag": "STEP 1", "title": "명령을 만든다", "text": "이 순간의 위치를 명령 안에 적어 둡니다. 아직 아무 일도 일어나지 않았습니다." },
    { "tag": "STEP 2", "title": "실행한다", "text": "오브젝트가 실제로 움직입니다. 여기까지는 메서드를 부른 것과 결과가 같습니다." },
    { "tag": "STEP 3", "title": "쌓아 둔다", "text": "실행한 명령을 스택에 넣습니다. 달라지는 것은 이 한 줄입니다. 한 일이 남습니다." },
    { "tag": "STEP 4", "title": "되돌린다", "text": "스택에서 꺼내 Undo 를 부릅니다. 명령이 들고 있던 이전 위치로 돌아갑니다." }
  ]
}
</script>
</x-steps>

`Ctrl+Z` 를 여러 번 누르면 스택에서 계속 꺼내면 됩니다.
쌓는 순서와 되돌리는 순서가 반대라서 스택이 정확히 맞는 자료구조입니다.

## 되돌리기만 딸려 오는 게 아닙니다

한 일이 물건으로 남기 시작하면 되는 일이 몇 가지 더 생깁니다.

**기록과 리플레이.** 스택 대신 리스트에 담아 두면 그게 그대로 재생 목록입니다.
처음부터 순서대로 `Execute` 를 부르면 같은 일이 다시 일어납니다.

```csharp
foreach (var c in recorded)
{
    c.Execute();
    yield return new WaitForSeconds(0.1f);
}
```

**전송.** 온라인 게임에서 위치를 보내는 대신 **"앞으로 이동" 이라는 명령을 보냅니다.**
결과가 아니라 의도를 보내는 것이라 데이터가 훨씬 작고,
서버가 그 명령이 정당한지 검사할 수 있습니다.
결과만 받으면 클라이언트가 보낸 좌표를 그대로 믿어야 합니다.

**입력과 동작을 떼어 놓기.** 키 설정을 바꾸는 기능이 필요할 때 이게 답입니다.

```csharp
Dictionary<KeyCode, ICommand> bindings = new()
{
    [KeyCode.W] = new MoveCommand(player, Vector3.forward),
    [KeyCode.Space] = new JumpCommand(player),
};

void Update()
{
    foreach (var (key, command) in bindings)
        if (Input.GetKeyDown(key)) invoker.Execute(command);
}
```

키를 바꾸는 것이 **딕셔너리의 열쇠 하나를 바꾸는 일**이 됩니다.
`if (Input.GetKeyDown(KeyCode.W)) Move();` 로 짜여 있으면 이게 안 됩니다.

## 밟기 쉬운 것 넷

**1. `Undo` 는 `Execute` 의 반대 계산이 아닙니다.**
위치를 `+5` 했으니 `-5` 하면 될 것 같지만, 그 사이에 다른 힘이 작용했다면 안 맞습니다.
**하기 전 상태를 저장해 두고 그대로 되돌리는 것**이 안전합니다.
그래서 명령 객체는 결과를 만들 재료가 아니라 **이전 상태의 사본**을 들고 있습니다.

**2. 그 사본이 커질 수 있습니다.** 위치 하나면 12바이트지만,
"모든 타일을 지웠다" 를 되돌리려면 지운 타일 전체를 들고 있어야 합니다.
되돌리기 스택에 그런 명령이 백 개 쌓이면 메모리가 눈에 띄게 늘어납니다.
**개수를 제한하는 것이 기본입니다.** 대부분의 에디터가 되돌리기 횟수에 한계를 두는 이유입니다.

**3. 리플레이는 결정론이 필요합니다.** 명령만 다시 실행한다고 같은 결과가 나오지 않습니다.
난수를 쓰면 시드를 같이 저장해야 하고, 물리 연산이 끼면 프레임 시간까지 맞아야 합니다.
**명령 기록은 리플레이의 필요조건이지 충분조건이 아닙니다.**

**4. 명령이 참조를 붙잡고 있습니다.** `MoveCommand` 는 `Transform` 을 들고 있습니다.
그 오브젝트를 파괴해도 되돌리기 스택이 살아 있는 한 참조가 남습니다.
파괴된 오브젝트를 되돌리려 하면 예외가 납니다.
오브젝트를 지울 때 관련 명령을 같이 정리하거나, 스택 자체를 비워야 합니다.

## 언제 쓰지 않는가

되돌리기도, 기록도, 전송도 필요 없다면 **그냥 메서드를 부르면 됩니다.**

명령 하나를 만드는 데 클래스 하나가 듭니다. 동작이 스무 개면 클래스가 스무 개입니다.
"나중에 되돌리기를 붙일지도 모르니까" 로 시작하면 붙이지도 않을 기능 때문에
파일만 스무 개 늘어납니다.

기준은 이렇게 잡습니다.

> **한 일이 남아 있어야 하는가.** 남아 있어야 하면 커맨드, 아니면 메서드 호출.

레벨 에디터, 턴제 게임, 리플레이가 있는 게임, 키 설정을 바꿀 수 있는 게임.
이런 것들은 남아 있어야 합니다.

## 정리

- 메서드 호출은 실행되면 사라진다. 무엇을 했는지도 이전이 어땠는지도 남지 않는다.
- 커맨드는 호출을 객체로 만든다. 만드는 시점에 **지금 상태를 자기 안에 적어 둔다.**
- 실행한 명령을 스택에 쌓으면 되돌리기가 된다. 쌓는 순서와 되돌리는 순서가 반대라 스택이 맞다.
- 리스트에 담으면 리플레이, 보내면 네트워크 명령, 딕셔너리에 담으면 키 리매핑이 된다.
- `Undo` 는 반대 계산이 아니라 저장해 둔 상태로 되돌리는 것이다.
- 그 저장이 커진다. 되돌리기 개수는 제한한다.
- 명령 기록만으로 리플레이가 되지 않는다. 난수와 물리까지 결정론이어야 한다.
- 남길 이유가 없으면 그냥 메서드를 부른다. 클래스 하나가 공짜가 아니다.