내일배움캠프 7주차 2일차 - 명령 패턴(Command Pattern)

백흰범·2024년 5월 28일
1
post-thumbnail

오늘 한 일

  • 스파르타 코딩클럽 강의 수강 (~ 1-15)
  • 챌린지반 특강 수강하기 (디자인 패턴 상편)
  • 명령 패턴에 대해서 자세히 파고 들기


명령 패턴(Command Pattern)


개념

발송자에서 수신자로 곧바로 명령을 전달하는 것이 아닌 중간 과정에서 새로운 명령 클래스를 생성하여 명령에 대한 처리를 독자적으로 수행하는 객체를 만드는 패턴이다. (명령의 객체화)




구조와 특징

구조


발송자 클래스(Invoker)

  • 요청(명령)의 시작을 관리하는 역할
    • 해당 클래스에는 명령 객체에 대한 참조를 저장하기 위한 필드가 있어야한다. (참조 변수든 리스트 형태든 뭐든간에)
    • 발송자는 요청을 수신자에게 직접 보내는 대신 해당 명령을 작동시킨다.
    • 발송자는 명령 객체를 생성할 책임은 없고 일반적으로 생성자를 통해 클라이언트로부터 미리 생성된 명령을 받는다.

명령(command) 인터페이스

  • 일반적으로 명령을 실행하기 위한 단일 메서드만을 선언한다.

구체적_명령

  • 다양한 유형의 요청(명령)을 구현
    • 구체적_명령은 자체적으로 작업을 수행해서는 안되며, 대신 수신자 객체 중 하나에다가 호출을 전달해야한다.
      (그러나 코드의 단순화를 위해서 작업 수행 로직이 병합될 수 있다.)
    • 수신자 객체에서 메서드를 실행하는 데 필요한 매개변수들은 구체적_명령 필드들로 선언할 수 있다. 생성자를 통해서만 이러한 필드의 초기화를 허용함으로써 명령 객체들을 불변으로 만들 수 있다.

수신자 클래스(Receiver)

  • 일부 작업 수행이 포함된 클래스
    • 거의 모든 객체는 수신자 역할을 할 수 있다.
    • 대부분의 명령 클래스들은 요청이 수신자에게 전달되는 방법에 대한 세부 정보만 처리하는 반면 수신자 자체는 실제 작업을 수행한다.

클라이언트

  • 구체적_명령 인스턴스를 만들고 설정한다.
    • 클라이언트는 수신자 인스턴스를 포함한 모든 요청 매개변수들을 명령의 생성자로 전달해야하며, 그렇게 만들어진 명령은 하나 또는 여러 발송자에게 전달될 수 있다.


특징

명령의 캡슐화

  • 특정 작업을 수행하는데 필요한 정보를 객체로 만드는 것
    이 객체는 작업을 수행하는 메서드와 그 메서드를 호출할 때 필요한 매개변수 등의 정보를 포함한다. 이렇게하면 명령 객체는 작업을 수행하는데 필요한 모든 정보를 가지고 있으므로, 이 객체를 저장하거나 전달하거나 나중에 실행하는 등의 작업을 할 수 있다.

! 캡슐화를 통해서 명령의 저장 및 재사용이나 명령의 실행 및 취소가 가능하다.



예시

코드 출처 : 리팩토링 구루

namespace command_Pattern
{
   // Command 인터페이스는 명령을 실행하는 메서드를 선언한다
    public interface ICommand
    {
        void Execute();
    }

    // 일부 명령은 자체적으로 간단한 작업을 구현할 수 있다
    class SimpleCommand : ICommand
    {
        private string _payload = string.Empty;

        public SimpleCommand(string payload) // 생성자
        {
            this._payload = payload;
        }

        public void Execute() // 단순 작업
        {
            Console.WriteLine($"단순명령: 저는 출력과 같이 단순한 일을 해낼 수 있습니다.({this._payload})");
        }
    }

    // 그러나, 일부 명령들은 더 복잡한 작업을 수신자(Receiver)라고 불리는 객체에게 위임할 수 있다
    class ComplexCommand : ICommand
    {
    	// 내 명령을 수행해줄 수신자
        private Receiver _receiver;

        // 수신자의 메서드를 실행하는 데 필요한 컨텍스트 데이터
        private string _a;

        private string _b;

        // 복합 명령은 생성자를 통해 하나 이상의 수신자 객체와 함께 모든 컨텍스트 데이터를 받아들일 수 있다
        public ComplexCommand(Receiver receiver, string a, string b)
        {
            this._receiver = receiver;
            this._a = a;
            this._b = b;
        }

        // 명령은 수신자의 어떤 메서드에게든 위임할 수 있다
        public void Execute()
        {
            Console.WriteLine("복합 명령: 복잡한 작업은 수신자 객체들에 의해 수행되어야합니다.");
            this._receiver.DoSomething(this._a);
            this._receiver.DoSomethingElse(this._b);
        }
    }

    // 수신자 클래스는 어떤 중요한 작업 로직을 포함하고 있다
    // 그들은 요청을 수행하는 데 관련된 모든 종류의 작업을 수행하는 방법을 알고 있다
    // 사실, 어떤 클래스든지 수신자로써 역할을 할 수 있다
    class Receiver
    {
    	// 작업 처리 메서드들
        public void DoSomething(string a)
        {
            Console.WriteLine($"수신자: ({a})에서 작업.");
        }

        public void DoSomethingElse(string b)
        {
            Console.WriteLine($"수신자: ({b})에서도 작업");
        }
    }

    // 발송자는 하나 또는 여러 명령어와 연결되어 있고, 명령에 요청을 보내는 역할을 한다.
    class Invoker
    {
        private ICommand _onStart; // 자신의 일을 시작하기 전에 호출할 명령

        private ICommand _onFinish; // 자신의 일이 끝나고 나서 호출할 명령

        // 명령 초기화 (명령 할당)
        public void SetOnStart(ICommand command)
        {
            this._onStart = command;
        }

        public void SetOnFinish(ICommand command)
        {
            this._onFinish = command;
        }

        // 호출자는 구체적인 명령이나 수신자 클래스에 의존하지 않는다
        // 명령을 실행함으로써 간접적으로 수신자에게 요청을 전달한다
        public void DoSomethingImportant()
        {
            Console.WriteLine("발송자: 제가 시작하기 전에 작업을 수행하시길 원하시는 분 있나요?");
            if (this._onStart is ICommand)
            {
                this._onStart.Execute();
            }

            Console.WriteLine("발송자: ..중요한 일을 수행하는 중입니다...");

            Console.WriteLine("발송자: 제가 끝나고 나서 작업을 수행하시길 원하시는 분 있나요?");
            if (this._onFinish is ICommand)
            {
                this._onFinish.Execute();
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // 클라이언트 코드는 호출자를 어떤 명령어로든 매개변수화할 수 있습니다
            Invoker invoker = new Invoker(); // 발송자 인스턴스
            invoker.SetOnStart(new SimpleCommand("안녕!")); // 시작 부분에 단순 명령 할당
            Receiver receiver = new Receiver(); // 수신자 인스턴스 (복합 명령을 위한 것)
            invoker.SetOnFinish(new ComplexCommand(receiver, "이메일 보내기", "보고서 작성")); // 끝나는 부분에 복합 명령 할당

            invoker.DoSomethingImportant(); // 설정된 명령 실행
        }
    }
}

실행 결과


해당 구조 분석

  • 클라이언트에서 명령을 생성하여 발송자 인스턴스에게 전달하는 방식임을 알 수 있다.
  • 발송자는 명령을 호출하는 순서를 정할 수 있는 것을 볼 수 있다.
  • 명령 클래스들은 작업이 단순하다면 스스로 수행이 가능하고 복잡한 작업일 경우 수신자를 통해서 수신자의 메서드를 활용한다.


장단점

장점

1. 단일 책임 원칙

  • 작업을 호출하는 클래스들은 이러한 작업을 수행하는 클래스들로부터 분리할 수 있다.

2. 개방/폐쇄 원칙

  • 기존 코드를 수정하지 않고 새 명령들을 도입할 수 있다.

3. 실행 취소/다시 실행을 구현

4. 작업들의 지연된실행을 구현

5. 간단한 명령들의 집합을 복잡한 명령으로 조합


단점

1. 발송자와 수신자 사이에 완전히 새로운 레이어를 도입하기 때문에 코드가 복잡해질 수 있다.



주요 용도

주요 기능

시간을 되돌리는 기능 (되돌리기, 시간 역행 등)

로그를 기록하고 복구하는 기능

주로 사용하는 게임

RTS 멀티 플레이 환경에서 플레이어들의 입력을 동기화 및 통일화(결정론적 락스텝)

체스나 바둑 같은 게임에서 기보를 기록 및 수 되돌리기 기능



직접 만들어보기

... coming soon ...



참고 자료

리팩토링 구루 - 커맨드 패턴




TIL을 작성하면서 느낀점

디자인 패턴이라는 것은 사실은 뭔가 흐릿흐릿했던 코드 구조들을 정형화하게 된 일종의 도구라고 생각한다. 물론 이 패턴을 알지 않아도 점차 코딩을 하다보면 디자인 패턴에 가까워지긴 하지만, 디자인 패턴을 대략적으로 아는 것과 뚜렷하게 아는 거랑은 큰 차이가 있다고 생각한다. 앞으로 코딩할 때 다양한 디자인 패턴을 배워서 내 개발 실력을 한층 끌어올리기 위해 노력해야겠다.

profile
게임 개발 꿈나무

0개의 댓글