[Unity] 콜백 대신 Command패턴을 사용하여 구조 만들기

Date:     Updated:

카테고리:

태그:

Unity에서 시스템 간 의존성을 줄이고 계층 구조를 구현하기 위해 콜백을 사용하게 됩니다.
하지만 콜백 방식은 Action이나 이벤트가 점점 많아지고, 흐름을 추적하기 어렵다는 단점이 있습니다.
이 문제를 해결하기 위해 Command 패턴을 도입해 설계를 하였습니다.

이번 포스팅에서는 콜백 기반 구조와 Command 패턴 적용 과정을 비교하며, 의존성을 줄이면서도 간단하게 유지하는 방법을 정리하였습니다.

콜백 기반 설계

먼저 콜백 방식으로 아이템 사용 로직을 구현한 예제입니다.

public class GameManager : MonoBehaviour
{
    private ItemManager itemManager = null;
    private Player player = null;

    private void Awake()
    {
        itemManager = GetComponentInChildren<ItemManager>();
    }

    private void Start()
    {
        itemManager.Init(OnItemUsed);
    }

    private void OnItemUsed(Item _item)
    {
        player.UseItem(_item);
    }
}

GameManagerItemManager을 초기화하면서 콜백을 전달합니다. 아이템이 사용되면 OnItemUsed 콜백이 호출되고, 그 안에서 Player이 해당 아이템을 사용합니다.

public class ItemManager : MonoBehaviour
{
    private List<Item> itemList = null;
    private Action<Item> onItemUsedCallback = null;

    private void Awake()
    {
        itemList = GetComponentsInChildren<Item>().ToList();
    }

    public void Init(Action<Item> _onItemUsedCallback)
    {
        onItemUsedCallback = _onItemUsedCallback;

        foreach (var item in itemList)
        {
            item.Init(OnItemUsed);
        }
    }

    private void OnItemUsed(Item _item)
    {
        itemList.Remove(_item);
    }
}

ItemManager은 아이템 리스트를 관리하며, 각 아이템에 콜백을 전달합니다. 아이템이 사용되면 OnItemUsed가 호출되어 리스트에서 제거합니다.

public class Item : MonoBehaviour
{
    private Action<Item> onItemUsedCallback = null;

    public void Init(Action<Item> _onItemUsedCallback)
    {
        onItemUsedCallback = _onItemUsedCallback;
    }

    public void Use()
    {
        Debug.Log("아이템 사용");
    }

    private void OnTriggerEnter(Collider _other)
    {
        onItemUsedCallback?.Invoke(this);
    }
}

Item11은 충돌 시 onItemUsedCallback을 호출합니다. 여기서 ItemItemManager에게 콜백을 주어, 사용되었음을 알리고 ItemManager1GameManager1Player11 순으로 흐름이 전달됩니다.

콜백의 문제점

지금은 구조가 단순하고 스크립트도 몇개 없어서, 흐름이 명확하지만 게임을 만들다 보면 구조가 복잡해지고 스크립트로 많아지게 됩니다. 그렇게 되면 흐름을 쉽게 파악하기 어려워지고, Action이 많아지다 보니 Init을 해줄 때 인자로 수많은 Action들을 주입해주어야 합니다.

따라서, 모듈간 의존성은 줄이면서 특정 클래스의 함수를 사용할 수 있도록 Command패턴을 사용합니다.

Command 패턴 사용 예제

Command 패턴은 함수 자체를 객체로 캡슐화하여 의존성을 줄이는 방식입니다.
콜백 대신 “아이템 사용”이라는 명령(Command)을 만들어서 처리하게 하면 구조가 단순해집니다. 콜백에서는 Item이 사용되었다는 정보를 ItemManager와 GameManager에게 알려주고, GameManager가 Player에게 특정 함수를 실행시키도록 하는 방식이었습니다. 하지만 Command를 사용하면 Item이 사용되었을 때, Player의 Command를 호출해주기만 하면 됩니다.

1. 기본 구조

Command 패턴의 핵심은 “실행할 동작을 객체(Command)로 감싸서 Invoker를 통해 실행”하는 것입니다.
이번 예제의 구조는 아래와 같습니다.

GameManager1 : 전체 시스템 초기화 ItemManager1: 씬 안의 아이템 관리 Item11: 실제 아이템 Player11 : 플레이어, 아이템을 사용하는 주체
CommandInvoker : 명령을 저장하고 실행하는 Invoker ICommand / Command_UseItem : 명령 인터페이스와 구현체

using UnityEngine;

public class GameManager1 : MonoBehaviour
{
    private ItemManager1 itemManager = null;
    private Player11 player = null;

    private void Awake()
    {
        itemManager = GetComponentInChildren<ItemManager1>();
        player      = GetComponentInChildren<Player11>();
    }

    private void Start()
    {
        // 플레이어에서 "아이템 사용" 명령을 등록
        player?.Init();
        itemManager?.Init();
    }
}
using UnityEngine;

public class Item11 : MonoBehaviour
{
    public void Init()
    {
        // 필요 시 아이템 초기화 코드
    }

    public void Use()
    {
        Debug.Log("아이템 사용!");
    }

    private void OnTriggerEnter(Collider _other)
    {
        // 플레이어가 닿으면 명령 실행
        CommandInvoker.Execute(this);
    }
}
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class ItemManager1 : MonoBehaviour
{
    private List<Item11> itemList = null;

    private void Awake()
    {
        itemList = GetComponentsInChildren<Item11>().ToList();
    }

    public void Init()
    {
        // 아이템 초기화 필요 시 구현
    }
}
using System;
using UnityEngine;

public class Player11 : MonoBehaviour
{
    public void Init()
    {
        // "아이템 사용" 명령 등록
        CommandInvoker.Add(new Command_UseItem(UseItem));
    }

    public void UseItem(Item11 _item)
    {
        _item?.Use();
    }
}
using System;
using System.Collections.Generic;

public class CommandInvoker
{
    private static List<ICommand> commandList = new List<ICommand>();

    public static void Add(ICommand _command)
    {
        commandList.Add(_command);
    }

    public static void Execute(Item11 _item)
    {
        // 등록된 첫 번째 커맨드 실행
        commandList[0].Execute(_item);
    }
}

public interface ICommand
{
    void Execute(Item11 _item11);
}

public class Command_UseItem : ICommand
{
    private Action<Item11> action = null;

    public Command_UseItem(Action<Item11> _action)
    {
        action = _action;
    }

    public void Execute(Item11 _item)
    {
        action?.Invoke(_item);
    }
}

위 코드들을 보면 복잡했던 콜백들은 사라지고, Item이 TriggerEnter 되었을 때, Player의 아이템 사용이라는 커맨드를 호출하게 됩니다.

이렇게 하면, 모듈간의 의존성을 줄이면서 보기 쉽고, 사용하기 쉬운 구조가 만들어 집니다.

Unity 카테고리 내 다른 글 보러가기

댓글 남기기