Command

Command Pattern이란?

커맨드 패턴Command Pattern은 요청(명령)을 객체로 캡슐화하여, 실행될 기능을 파라미터화하고, 요청의 실행, 취소, 기록 등을 관리할 수 있게 하는 패턴입니다. 이 패턴은 기능 실행의 책임을 메서드가 아니라 객체로 이동시켜, 기능의 실행과 취소를 유연하게 처리할 수 있게 해줍니다.

Command Pattern의 필요성

소프트웨어 개발에서, 사용자가 수행하는 다양한 요청을 유연하게 처리해야 하는 상황이 자주 발생합니다. 예를 들어, 도서관 관리 시스템에서 대출, 반납, 예약 등의 요청을 처리해야 할 때, 이러한 요청들을 일관된 방법으로 관리하고, 나중에 취소하거나 되돌려야 할 수 있습니다. 커맨드 패턴을 사용하면, 각각의 요청을 독립적인 객체로 캡슐화하여 관리할 수 있습니다. 이를 통해 각 요청을 큐에 저장하거나, 나중에 취소하거나, 로그로 기록하는 등의 작업이 쉽게 가능해집니다.

Command Pattern 구조

D2 Diagram

  • Client → Invoker
    • 클라이언트는 SetCommandCommand를 호출하여 명령을 Invoker에 전달합니다.
  • Invoker → Command
    • Invoker는 명령의 Execute()를 호출하여 작업을 실행합니다.
  • Command → ConcreteCommand
    • 명령의 구체적인 구현은 ConcreteCommand에 의해 처리됩니다.
  • ConcreteCommand → Receiver
    • ConcreteCommand는 요청을 Receiver로 전달하고, Receiver가 작업을 수행합니다.
  • Receiver
    • 요청된 작업을 실제로 수행합니다.

Command Pattern의 구성 요소

커맨드 인터페이스Command Interface

실행될 명령을 정의하는 인터페이스로, Execute()Undo() 메서드를 포함합니다.

구체적인 커맨드Concrete Command

커맨드 인터페이스를 구현하는 클래스이며, 특정 작업을 실행하는 구체적인 명령을 구현합니다.

리시버Receiver

실제로 명령을 수행하는 객체입니다. 커맨드 객체는 리시버를 호출하여 작업을 수행합니다.

인보커Invoker

커맨드 객체를 호출하는 역할을 합니다. 인보커는 어떤 명령을 실행할지 결정하고, 이를 실행하거나 취소할 수 있습니다.

클라이언트Client

인보커에게 어떤 명령을 실행할지 지시합니다. 보통 커맨드 객체와 리시버 객체를 생성하고, 이를 인보커에 전달합니다.

Command Pattern 적용

잘못된 요청 처리 방식

커맨드 패턴을 사용하지 않고, 모든 요청을 하나의 클래스에서 처리하는 경우, 코드가 복잡해지고 유지보수가 어려워집니다. 아래는 도서관 관리 시스템에서 대출, 반납, 예약 등의 요청을 처리하는 잘못된 예시입니다.

public class LibrarySystem
{
    public void ProcessRequest(string requestType, string bookTitle)
    {
        if (requestType == "borrow")
        {
            BorrowBook(bookTitle);
        }
        else if (requestType == "return")
        {
            ReturnBook(bookTitle);
        }
        else if (requestType == "reserve")
        {
            ReserveBook(bookTitle);
        }
    }
    private void BorrowBook(string bookTitle)
    {
        Console.WriteLine($"{bookTitle} 대출되었습니다.");
    }
    private void ReturnBook(string bookTitle)
    {
        Console.WriteLine($"{bookTitle} 반납되었습니다.");
    }
    private void ReserveBook(string bookTitle)
    {
        Console.WriteLine($"{bookTitle} 예약되었습니다.");
    }
}

확장성 부족

새로운 요청이 추가될 때마다 ProcessRequest 메서드에 조건문을 추가해야 하며, 이로 인해 코드의 복잡성이 증가합니다.

단일 책임 원칙SRP 위반

LibrarySystem 클래스가 다양한 요청을 처리하는 모든 로직을 포함하게 되어, 단일 책임 원칙이 위반됩니다.

유지보수 어려움

각 요청이 하나의 메서드에서 처리되므로, 특정 요청에 대한 변경이 다른 코드에 영향을 미칠 수 있습니다.

Command Pattern 적용 예시

커맨드 패턴을 사용하면, 각 요청을 독립적인 객체로 캡슐화하여 관리할 수 있습니다. 이를 통해 요청의 실행, 취소, 재실행 등을 유연하게 처리할 수 있습니다.

// 커맨드 인터페이스
public interface ICommand
{
    void Execute();
    void Undo();
}
// 리시버 클래스
public class Library
{
    public void BorrowBook(string bookTitle)
    {
        Console.WriteLine($"{bookTitle} 대출되었습니다.");
    }
    public void ReturnBook(string bookTitle)
    {
        Console.WriteLine($"{bookTitle} 반납되었습니다.");
    }
    public void ReserveBook(string bookTitle)
    {
        Console.WriteLine($"{bookTitle} 예약되었습니다.");
    }
}
// 구체적인 커맨드 클래스: 대출 명령
public class BorrowCommand : ICommand
{
    private Library _library;
    private string _bookTitle;
    public BorrowCommand(Library library, string bookTitle)
    {
        _library = library;
        _bookTitle = bookTitle;
    }
    public void Execute()
    {
        _library.BorrowBook(_bookTitle);
    }
    public void Undo()
    {
        _library.ReturnBook(_bookTitle);
    }
}
// 구체적인 커맨드 클래스: 반납 명령
public class ReturnCommand : ICommand
{
    private Library _library;
    private string _bookTitle;
    public ReturnCommand(Library library, string bookTitle)
    {
        _library = library;
        _bookTitle = bookTitle;
    }
    public void Execute()
    {
        _library.ReturnBook(_bookTitle);
    }
    public void Undo()
    {
        Console.WriteLine($"{_bookTitle} 반납 취소되었습니다.");
    }
}
// 구체적인 커맨드 클래스: 예약 명령
public class ReserveCommand : ICommand
{
    private Library _library;
    private string _bookTitle;
    public ReserveCommand(Library library, string bookTitle)
    {
        _library = library;
        _bookTitle = bookTitle;
    }
    public void Execute()
    {
        _library.ReserveBook(_bookTitle);
    }
    public void Undo()
    {
        Console.WriteLine($"{_bookTitle} 예약 취소되었습니다.");
    }
}
// 인보커 클래스
public class LibraryInvoker
{
    private ICommand _command;
    public void SetCommand(ICommand command)
    {
        _command = command;
    }
    public void ExecuteCommand()
    {
        _command.Execute();
    }
    public void UndoCommand()
    {
        _command.Undo();
    }
}
public class Program
{
    public static void Main(string[] args)
    {
        Library lib = new Library();
		// 예약 명령 생성 및 실행
        ICommand reserveCommand = new ReserveCommand(lib, "C#");
        invoker.SetCommand(reserveCommand);
        invoker.ExecuteCommand();
        
        // 대출 명령 생성 및 실행
        ICommand borrowCommand = new BorrowCommand(lib, "C#");
        LibraryInvoker invoker = new LibraryInvoker();
        invoker.SetCommand(borrowCommand);
        invoker.ExecuteCommand();
      
        // 대출 취소
        invoker.UndoCommand();
    }
}

Command Pattern 장단점

장점

확장성

새로운 명령을 쉽게 추가할 수 있습니다. 기존 코드를 수정하지 않고도 새로운 커맨드 클래스를 추가하면 됩니다.

유연성

명령의 실행과 취소를 독립적으로 관리할 수 있으며, 명령을 큐에 저장하거나 로그로 기록할 수 있습니다.

단일 책임 원칙SRP 준수

각 커맨드 클래스는 하나의 책임만 가지므로, 클래스가 단일 책임 원칙을 준수할 수 있습니다.

단점

클래스 수 증가

각 명령마다 별도의 커맨드 클래스를 생성해야 하므로, 클래스의 수가 증가할 수 있습니다.

구조적 복잡성 증가

단순한 요청 처리에 커맨드 패턴을 적용하면 코드의 복잡성이 증가할 수 있습니다.

개선 방안

내부 클래스 사용

만약 특정 커맨드 클래스가 한 곳에서만 사용된다면, 해당 클래스의 정의를 해당 클래스가 사용되는 곳에서 내부 클래스로 정의할 수 있습니다. 이를 통해 코드의 구조를 보다 간결하게 유지할 수 있습니다.

익명 클래스 사용

C#에서는 간단한 명령의 경우 익명 클래스를 사용하여 커맨드 객체를 인라인으로 정의할 수 있습니다. 예를 들어, 단순한 커맨드를 만들기 위해 별도의 클래스를 생성하는 대신, 익명 클래스를 사용하여 일회성 커맨드를 정의할 수 있습니다.

// 내부 클래스로 명령 정의
public class Program
{
    public static void Main(string[] args)
    {
        Library library = new Library();
        ICommand borrowCommand = new BorrowCommand(library, "Design Patterns");
        
        // 익명 클래스 활용
        CommandInvoker invoker = new CommandInvoker();
        invoker.SetCommand(borrowCommand);
        invoker.ExecuteCommand();
    }
}

제너릭 커맨드 클래스 사용

유사한 로직을 가지는 커맨드를 일반화하여 제너릭 클래스로 만들면, 특정 작업에 대한 커맨드 클래스를 재사용할 수 있어 클래스의 수를 줄일 수 있습니다.

// 단순한 명령을 처리하는 제너릭 커맨드 클래스
public class GenericCommand<T> : ICommand
{
    private Action<T> _execute;
    private Action<T> _undo;
    private T _parameter;
    public GenericCommand(Action<T> execute, Action<T> undo, T parameter)
    {
        _execute = execute;
        _undo = undo;
        _parameter = parameter;
    }
    public void Execute() => _execute(_parameter);
    public void Undo() => _undo(_parameter);
}

이 예시에서는 GenericCommand 클래스를 사용하여 다양한 명령을 처리할 수 있으며, 클래스를 별도로 생성할 필요 없이 Action 델리게이트를 통해 동작을 정의할 수 있습니다.

패턴 적용 기준 명확화

커맨드 패턴은 복잡한 작업이나 다양한 요청이 필요한 시스템에 적합합니다. 따라서 단순한 작업에는 패턴을 적용하지 않고, 패턴이 필요한 경우에만 신중하게 적용하는 것이 중요합니다.

적용 팁

명령의 재사용성과 취소 기능이 필요할 때 적용

커맨드 패턴은 명령을 재사용하거나, 실행 취소 및 재실행 기능이 필요할 때 특히 유용합니다. 이러한 요구 사항이 있는 경우 패턴 적용을 고려하세요.

리시버 클래스의 역할을 명확하게 정의

리시버 클래스는 실제로 작업을 수행하는 객체입니다. 이 클래스는 가능한 한 명확한 역할을 가지도록 설계해야 합니다. 예를 들어, 도서 대출과 반납은 Library 클래스에서 관리하지만, UI 업데이트나 로그 기록은 별도의 리시버 클래스에서 처리할 수 있습니다.

명령을 스택에 저장하여 나중에 실행

실행해야 할 명령을 큐나 스택에 저장해두고, 나중에 한꺼번에 실행하거나 순차적으로 실행할 수 있습니다. 이는 특히 비동기 작업이나 트랜잭션 관리에 유용합니다.

Undo/Redo 기능 구현

커맨드 패턴을 사용하면, 명령을 스택에 저장하여 취소Undo와 재실행Redo 기능을 쉽게 구현할 수 있습니다. 이는 텍스트 편집기나 그래픽 소프트웨어와 같은 애플리케이션에서 매우 유용합니다.

커맨드 객체 생성을 팩토리 패턴으로 관리

커맨드 객체가 많아질 경우, 이를 팩토리 패턴과 결합해 생성 로직을 캡슐화하면 코드의 복잡성을 줄일 수 있습니다.

맺음말

커맨드 패턴은 요청을 객체로 캡슐화하여, 명령의 실행과 취소를 유연하게 관리할 수 있는 강력한 패턴입니다. 도서관 관리 시스템과 같은 경우, 다양한 요청을 일관된 방식으로 처리하고, 나중에 취소하거나 되돌려야 하는 상황에서 커맨드 패턴을 사용하면 코드의 유연성과 유지보수성을 크게 향상시킬 수 있습니다. 다만, 클래스 수와 복잡성이 증가할 수 있으므로, 필요에 따라 신중하게 적용하는 것이 중요합니다.

심화학습

커맨드 큐

커맨드 패턴은 명령을 큐에 저장하고, 나중에 실행하거나 취소할 수 있도록 지원합니다. 이를 통해 사용자의 명령 이력을 관리하고, 실행 취소Undo 및 재실행Redo을 구현할 수 있습니다.

public class CommandQueue
{
    private Queue<ICommand> _commandQueue = new Queue<ICommand>();
    public void AddCommand(ICommand command)
    {
        _commandQueue.Enqueue(command);
    }
    public void ProcessCommands()
    {
        while (_commandQueue.Count > 0)
        {
            ICommand command = _commandQueue.Dequeue();
            command.Execute();
        }
    }
}

Command와 Factory

커맨드 객체의 생성 로직을 팩토리 패턴을 사용하여 캡슐화하면, 커맨드 객체 생성과 관련된 복잡성을 줄일 수 있습니다. 이는 특히 다양한 종류의 커맨드 객체를 생성할 때 유용합니다.

// 커맨드 객체 생성을 팩토리 패턴으로 관리
public class CommandFactory
{
    public static ICommand CreateBorrowCommand(Library lib, string title)
    {
        return new BorrowCommand(lib, title);
    }
    public static ICommand CreateReturnCommand(Library lib, string title)
    {
        return new ReturnCommand(lib, title);
    }
}