---
title: "상태가 하나 늘면 조건은 하나만 늘지 않는다"
description: "bool 플래그는 있을 수 없는 조합까지 만들어 냅니다. enum 으로 한 번 정리되지만 switch 가 여러 곳에 흩어지면서 다시 무너집니다."
author: "유니얼"
pubDate: 2026-09-09
category: "game-dev/design-patterns"
tags: ["Unity"]
canonical: https://blog.unialgames.com/posts/unity-state-machine/
---
적 하나를 만듭니다. 평소에는 가만히 있다가, 플레이어를 보면 쫓아가고,
가까워지면 공격하고, 체력이 다하면 죽습니다.

처음에는 이렇게 시작합니다.

```csharp
public class Enemy : MonoBehaviour
{
    bool isChasing;
    bool isAttacking;
    bool isDead;

    void Update()
    {
        if (isDead) return;

        if (isAttacking) { Attack(); return; }
        if (isChasing)   { Chase();  return; }

        LookAround();
    }
}
```

동작합니다. 그런데 `bool` 세 개가 만들 수 있는 조합은 여덟 가지인데
**실제로 말이 되는 것은 네 가지뿐**입니다.

<figure class="diagram">
<svg viewBox="0 0 680 210" role="img" aria-label="bool 세 개는 여덟 가지 조합을 만들지만 그중 네 가지만 유효하고 나머지는 있을 수 없는 상태라는 그림">
<text x="30" y="26" font-size="13" fill="var(--text-muted)">bool 세 개가 만드는 조합</text>
<g stroke="var(--border)" fill="var(--bg)">
<rect x="30" y="40" width="150" height="30" rx="6"></rect>
<rect x="30" y="76" width="150" height="30" rx="6"></rect>
<rect x="30" y="112" width="150" height="30" rx="6"></rect>
<rect x="30" y="148" width="150" height="30" rx="6"></rect>
</g>
<g font-family="var(--font-mono)" font-size="12" fill="var(--text-muted)">
<text x="44" y="60">F F F  대기</text>
<text x="44" y="96">F T F  공격</text>
<text x="44" y="132">T F F  추격</text>
<text x="44" y="168">F F T  사망</text>
</g>
<text x="216" y="105" font-size="15" fill="var(--text-muted)">+</text>
<text x="260" y="26" font-size="13" fill="var(--warn)">있을 수 없는 조합</text>
<g stroke="var(--warn)" fill="none">
<rect x="260" y="40" width="210" height="30" rx="6"></rect>
<rect x="260" y="76" width="210" height="30" rx="6"></rect>
<rect x="260" y="112" width="210" height="30" rx="6"></rect>
<rect x="260" y="148" width="210" height="30" rx="6"></rect>
</g>
<g font-family="var(--font-mono)" font-size="12" fill="var(--warn)">
<text x="274" y="60">T T F  추격하며 공격?</text>
<text x="274" y="96">T F T  죽었는데 추격?</text>
<text x="274" y="132">F T T  죽었는데 공격?</text>
<text x="274" y="168">T T T  전부?</text>
</g>
<text x="500" y="100" font-size="13" fill="var(--text-muted)">컴파일러가</text>
<text x="500" y="120" font-size="13" fill="var(--text-muted)">막아 주지 않습니다</text>
</svg>
<figcaption>플래그를 끄는 것을 한 번만 빠뜨리면 오른쪽 상태가 실제로 만들어집니다.</figcaption>
</figure>

`isDead = true` 를 켜면서 `isChasing = false` 를 빠뜨리면
**죽었는데 추격 중인 적**이 생깁니다. 그리고 플래그가 넷, 다섯으로 늘어나면
조합은 열여섯, 서른둘이 됩니다. 유효한 것은 여전히 몇 개뿐인데요.

## `enum` 으로 한 걸음

문제의 뿌리는 **상태가 여러 변수에 흩어져 있다**는 것입니다.
하나에 모으면 애초에 잘못된 조합이 만들어지지 않습니다.

```csharp
enum State { Idle, Chase, Attack, Dead }

State state = State.Idle;

void Update()
{
    switch (state)
    {
        case State.Idle:   LookAround(); break;
        case State.Chase:  Chase();      break;
        case State.Attack: Attack();     break;
        case State.Dead:                 break;
    }
}
```

**변수 하나에 값 하나뿐이라 두 상태가 동시에 참일 수 없습니다.**
대부분의 현장 코드가 여기까지 와 있고, 상태가 서너 개면 이걸로 충분합니다.

## 그런데 왜 다시 무너지나

`enum` 이 무너지는 것은 상태가 늘어서가 아니라 **같은 `enum` 을 여러 곳에서
다시 검사하기 시작해서**입니다.

```csharp
void Update()          { switch (state) { ... } }   // 매 프레임 무엇을 하나
void OnPlayerSpotted() { switch (state) { ... } }   // 발견했을 때 어디로 가나
void OnDamaged()       { switch (state) { ... } }   // 맞았을 때 어디로 가나
void OnAnimEnd()       { switch (state) { ... } }   // 애니메이션이 끝나면
```

상태를 하나 추가하면 이 `switch` 를 **전부 찾아서** `case` 를 넣어야 합니다.
하나를 빠뜨리면 컴파일은 되고 그 상태에서만 아무 반응이 없습니다.
제목의 이야기가 이것입니다. 상태 하나가 늘면 고칠 곳은 하나가 아닙니다.

여기에 하나가 더 붙습니다. **"이 상태에 들어올 때 딱 한 번만 할 일"** 을 적을 자리가 없습니다.
추격을 시작할 때 울음소리를 한 번 내려면 결국 이렇게 됩니다.

```csharp
case State.Chase:
    if (!hasRoared) { Roar(); hasRoared = true; }   // 플래그가 돌아왔습니다
    Chase();
    break;
```

애써 없앤 `bool` 이 다시 기어 들어옵니다.

## 상태를 객체로

`switch` 의 각 `case` 를 클래스 하나씩으로 뜯어냅니다.
그리고 **들어올 때와 나갈 때를 문법으로 만들어 줍니다.**

```csharp
public interface IState
{
    void Enter();    // 이 상태가 될 때 한 번
    void Update();   // 이 상태인 동안 매 프레임
    void Exit();     // 이 상태를 떠날 때 한 번
}
```

```csharp
public class ChaseState : IState
{
    readonly Enemy enemy;
    public ChaseState(Enemy e) => enemy = e;

    public void Enter()  => enemy.Roar();              // 한 번만, 플래그 없이
    public void Update() => enemy.MoveTowardPlayer();
    public void Exit()   => enemy.StopMoving();
}
```

바꾸는 쪽은 이렇게 짧아집니다.

```csharp
public class StateMachine
{
    public IState Current { get; private set; }

    public void Change(IState next)
    {
        Current?.Exit();
        Current = next;
        Current.Enter();
    }

    public void Tick() => Current?.Update();
}
```

<x-flow>
<script type="application/json">
{
  "label": "대기에서 추격으로 넘어가는 순간",
  "lines": [
    "// 매 프레임",
    "machine.Tick();",
    "",
    "// 플레이어를 발견하면",
    "machine.Change(chaseState);",
    "",
    "void Change(IState next)",
    "{",
    "    Current?.Exit();",
    "    Current = next;",
    "    Current.Enter();",
    "}"
  ],
  "cases": [
    {
      "label": "Idle 에서 Chase 로",
      "state": "Current = Idle",
      "path": [
        { "line": 1, "why": "대기 상태의 Update 가 돕니다.", "state": "Current = Idle" },
        { "line": 1, "why": "주변을 둘러보며 계속 돕니다.", "state": "Current = Idle" },
        { "line": 4, "why": "플레이어를 발견했습니다.", "state": "Current = Idle" },
        { "line": 8, "why": "떠나는 상태를 먼저 정리합니다.", "state": "Current = Idle", "out": "Idle.Exit" },
        { "line": 9, "why": "참조만 바꿉니다.", "state": "Current = Chase" },
        { "line": 10, "why": "새 상태의 준비를 합니다. 여기가 한 번만 실행되는 자리입니다.", "state": "Current = Chase", "out": "Chase.Enter 울음소리" },
        { "line": 1, "why": "다음 프레임부터는 추격 Update 가 돕니다.", "state": "Current = Chase" }
      ],
      "skip": []
    }
  ],
  "note": "Exit 이 Enter 보다 먼저입니다. 순서를 바꾸면 새 상태가 준비해 둔 것을 옛 상태가 정리하면서 지웁니다."
}
</script>
</x-flow>

이제 상태를 추가하려면 **클래스 파일 하나를 새로 만들면 됩니다.**
`Enemy` 도 `StateMachine` 도 고치지 않습니다.
`switch` 를 찾아 헤맬 일도, 빠뜨릴 일도 없습니다.

## 전이는 누가 정하나

여기가 이 패턴의 진짜 결정입니다. **다음 상태를 누가 고르느냐**입니다.

가장 흔한 답은 **상태 자신**입니다.

```csharp
public class ChaseState : IState
{
    public void Update()
    {
        if (enemy.DistanceToPlayer < 2f)
            enemy.Machine.Change(enemy.AttackState);   // 스스로 넘긴다

        enemy.MoveTowardPlayer();
    }
}
```

추격 상태만이 "언제 공격으로 넘어가는가" 를 압니다.
대기 상태는 그 조건을 몰라도 되고, 알 필요도 없습니다.
**각 상태가 자기 출구만 아는 것**이 이 구조의 핵심입니다.

> 전이 규칙을 상태 밖의 표로 빼는 방식도 있습니다.
> 전이가 복잡해서 한눈에 보여야 하거나, 데이터로 편집하고 싶을 때 씁니다.
> 대신 상태와 규칙이 두 곳으로 갈리므로 처음부터 그렇게 갈 이유는 없습니다.

## 유니티에서 챙길 것

**상태 객체를 매 전이마다 만들지 않습니다.**

```csharp
// 전이할 때마다 new 하면 쓰레기가 계속 쌓입니다
machine.Change(new ChaseState(this));

// 미리 하나씩 만들어 두고 재사용합니다
void Awake()
{
    idleState   = new IdleState(this);
    chaseState  = new ChaseState(this);
    attackState = new AttackState(this);
}
```

상태 객체에 이번 회차에만 쓰는 값이 있다면 `Enter()` 에서 초기화하면 됩니다.
그러면 재사용해도 문제가 없습니다.

**상태는 `MonoBehaviour` 가 아니어도 됩니다.** 순수 C# 클래스로 두면
오브젝트에 컴포넌트가 잔뜩 붙지 않고, 인스펙터도 깔끔합니다.
유니티 API 는 `Enemy` 참조를 통해 쓰면 됩니다.

**그리고 이미 쓰고 있습니다.** Animator 의 상태 머신이 정확히 이 구조입니다.
상태가 있고, 전이 조건이 화살표로 붙어 있고, 각 상태에 진입과 종료가 있습니다.
`StateMachineBehaviour` 를 상속하면 `OnStateEnter` 와 `OnStateExit` 를 직접 받습니다.

## 언제 쓰지 않는가

상태가 둘이고 앞으로도 둘이면 `bool` 하나가 정답입니다.
`IState` 인터페이스와 클래스 두 개를 만드는 것은 그 자체가 비용입니다.

기준은 이렇게 잡습니다.

> **상태가 셋을 넘고, 상태마다 진입이나 종료 처리가 있고,
> 앞으로 더 늘어날 것 같으면** 객체로 뺀다.

프로토타입은 `enum` 과 `switch` 로 빠르게 만들고, 확장이 보일 때 옮기는 것이
현실적인 순서입니다. 처음부터 완성된 구조로 시작하려다 아무것도 못 만드는 것보다 낫습니다.

## 정리

- `bool` 플래그는 있을 수 없는 조합까지 만든다. 플래그 셋이면 여덟 가지가 생기고 넷은 오류다.
- `enum` 은 상태를 한 변수에 모아 그 문제를 없앤다. 셋넷까지는 이걸로 충분하다.
- `enum` 이 무너지는 것은 같은 값을 여러 곳에서 다시 `switch` 하기 때문이다.
- 상태 하나를 추가하면 그 `switch` 를 전부 고쳐야 하고, 빠뜨려도 컴파일된다.
- "들어올 때 한 번만" 을 적을 자리가 없어서 `bool` 이 다시 등장한다.
- 상태를 객체로 만들면 `Enter` 와 `Exit` 가 문법이 된다. 추가는 파일 하나다.
- 전이는 보통 상태 자신이 정한다. 각 상태가 자기 출구만 알면 된다.
- `Exit` 이 `Enter` 보다 먼저다. 상태 객체는 미리 만들어 재사용한다.