이 문서는 FSM 머신 패턴을 사용하다가 생긴 문제점을 해결함을 기록한 회고록에 가깝습니다.
상태(State)를 기반으로 오브젝트의 동작을 제어하며, 특정 트리거(Trigger)나 조건(Condition)에 의해 명확하게 정의된 상태 간 전이(State Transition)가 발생함.
상태 기반 제어 흐름의 명확성 확보
각 상태는 독립적인 책임을 가지며, 전이 조건과 함께 관리됨으로써
게임 오브젝트의 행위 흐름을 구조적으로 이해하고 디버깅하기 용이함.
복잡도 분산 및 코드 유지보수성 향상
상태별 로직을 분리함으로써 거대하고 모놀리식한 조건 분기를 제거하고,
유지보수 및 확장 시 변경 범위를 최소화할 수 있음.
유기적인 동작 조합을 통한 행위 다양성 구현
전이 조건과 상태 간 상호작용을 기반으로 복잡한 게임 플레이 흐름을 유연하게 설계할 수 있으며,
상태 재사용이 가능하여 설계 효율 또한 높아짐.

FSM의 작동 FlowChart를 간단하게 그려보았다.
Attack State은 공격 기능만, Move State는 객체의 이동만 구현하면 되니, 만약 어떤 지점에서 문제가 생긴다면 해당 State만 수정하면 된다.
위 도식에서는 외부에서의 트리거로 Die 상태가 된다.
이러면 뭔가 스크립트가 꼬이지 않을까?
각 State는, Enter(), Update(), Exit()를 반드시 구현해야 한다.
(Update()는 실질적으로 상태의 '동작'을 담은 함수이다.)
SetState()라는 함수로 상태를 설정하게 된다. 이때 Enter()~Exit()를 모두 거치게 되므로, 모든 상태가 정상적으로 종료됨이 보장된다.

만약 Attack State에서 Die State로 SetState()될 경우 반드시 Exit()를 거치므로 안전하다.

// 상태 인터페이스 정의 - 상태 전이 및 상태 동작 메서드를 명시
public interface IState<TEnum, TOwner>
{
TEnum StateType { get; } // 상태 식별용 Enum
void Enter(TOwner owner); // 상태 진입 시 호출
void Exit(TOwner owner); // 상태 종료 시 호출
void Update(TOwner owner); // 상태 중 로직 (매 프레임 등)
}
using System.Collections.Generic;
public class StateMachine<TEnum, TOwner>
{
private Dictionary<TEnum, IState<TEnum, TOwner>> _states = new();
private IState<TEnum, TOwner> _currentState;
private TOwner _owner;
public TEnum CurrentStateType => _currentState.StateType;
public StateMachine(TOwner owner)
{
_owner = owner;
}
// 상태 등록
public void AddState(IState<TEnum, TOwner> state)
{
if (!_states.ContainsKey(state.StateType))
_states.Add(state.StateType, state);
}
// 상태 전이
public void ChangeState(TEnum newStateType)
{
if (_states.TryGetValue(newStateType, out var newState))
{
_currentState?.Exit(_owner);
_currentState = newState;
_currentState.Enter(_owner);
}
}
// 매 프레임 호출 등에서 상태 업데이트
public void Update()
{
_currentState?.Update(_owner);
}
}
public enum EnemyStateType
{
Idle,
Chase,
Attack,
Die
}
public class EnemyIdleState : IState<EnemyStateType, EnemyFSM>
{
public EnemyStateType StateType => EnemyStateType.Idle;
public void Enter(EnemyFSM enemy)
{
// Idle 상태 진입 시 초기화 로직
UnityEngine.Debug.Log("Enter Idle");
}
public void Exit(EnemyFSM enemy)
{
// Idle 상태 종료 시 처리
UnityEngine.Debug.Log("Exit Idle");
}
public void Update(EnemyFSM enemy)
{
// Idle 중 처리 (예: 시야 감지)
}
}
public class EnemyFSM : UnityEngine.MonoBehaviour
{
private StateMachine<EnemyStateType, EnemyFSM> _stateMachine;
private void Awake()
{
_stateMachine = new StateMachine<EnemyStateType, EnemyFSM>(this);
// 상태 등록
_stateMachine.AddState(new EnemyIdleState());
_stateMachine.AddState(new EnemyChaseState());
_stateMachine.AddState(new EnemyAttackState());
// 초기 상태 설정
_stateMachine.ChangeState(EnemyStateType.Idle);
}
private void Update()
{
_stateMachine.Update();
}
public void SetState(EnemyStateType state)
{
_stateMachine.ChangeState(state);
}
}
기존의 상태 시스템은 설계 초기에 본체 클래스에 대한 직접 접근을 제한하여 응집도를 높이려는 의도에서 출발했습니다.
그러나 이로 인해 각 상태 클래스의 생성자가 점점 복잡해졌고, 상태 간 전이 또한 일관성이 부족해지며 전이 유효성 검증을 테스트 코드에 의존하는 비효율적인 구조로 발전하게 되었습니다.
결과적으로 디버깅 및 유지보수 비용이 증가하고, 시스템 신뢰성이 떨어지는 문제를 직면하게 되었습니다.
이를 해결하기 위해 제네릭 기반의 FSM 구조로 리팩토링하면서,
StateMachine<TEnum, TOwner>를 통한 타입 안정성과 재사용성 확보,Owner에 대한 직접 참조를 허용함으로써 상태 구현의 간결화,Dictionary 기반 상태 등록으로 전이의 명시성과 안전성 강화이번 개선 과정을 통해, 설계 철학이 지나치게 엄격하거나 이상적일 경우 오히려 유지보수성과 실용성을 해칠 수 있음을 체감했습니다. 궁극적으로는 구조적 완성도와 실제 운영 효율성 사이의 균형을 맞추는 것이 클라이언트 아키텍처 설계에 있어 가장 중요한 요소임을 다시금 확인할 수 있었습니다.