Command 패턴
의도
커맨드는 요청을 요청에 대한 모든 정보가 포함된 독립실행형 객체로 변환하는 행동 디자인 패턴입니다. 이 변환은 다양한 요청들이 있는 메서드들을 인수화 할 수 있도록 하며, 요청의 실행을 지연 또는 대기열에 넣을 수 있도록 하고, 또 실행 취소할 수 있는 작업을 지원할 수 있도록 합니다.

문제
당신이 새로운 텍스트 편집기 앱을 개발하고 있다고 상상해 봅시다. 당신이 현재 하는 작업은 편집기의 다양한 작업을 위한 여러 버튼이 있는 도구 모음(툴바)을 만드는 것입니다. 당신은 도구 모음의 버튼들과 다양한 대화 상자들의 일반 버튼들에 사용할 수 있는 매우 깔끔한 Button(버튼) 클래스를 만들었습니다.

앱의 모든 버튼은 같은 클래스에서 파생됩니다.
이 버튼들은 모두 비슷해 보이지만 각각 다른 기능들을 수행해야 합니다. 그러면 이 버튼들의 다양한 클릭 핸들러들에 대한 코드는 어디에 두겠습니까? 가장 간단한 해결책은 버튼이 사용되는 각 위치에 수많은 자식 클래스들을 만드는 것입니다. 이러한 자식 클래스들에는 버튼 클릭 시 실행되어야 하는 코드가 포함됩니다.

많은 버튼 자식 클래스들이 있습니다. 무엇이 잘못될 수 있을까요?
머지않아 당신은 이 접근 방식에 심각한 결함이 있음을 깨닫게 됩니다. 일단, 당신은 이제 엄청난 수의 자식 클래스들이 있으며, 기초 Button 클래스를 수정할 때마다 이러한 자식 클래스의 코드를 깨뜨릴 위험이 있습니다. 간단히 말해서, 그래픽 사용자 인터페이스 코드는 비즈니스 로직의 불안정한 코드에 어색하게 의존하게 되었습니다.

여러 클래스가 같은 기능을 구현합니다.
그리고 당신이 고려해야 할 최악의 사항은, 텍스트 복사/붙여넣기와 같은 일부 작업은 여러 위치에서 호출될 수 있다는 사실입니다. 예를 들어, 사용자는 무언가를 복사하기 위하여 도구 모음에서 작은 '복사' 버튼을 클릭하거나 콘텍스트 메뉴를 통해 무언가를 복사하거나 키보드에서 Ctrl+C를 누를 수 있습니다.
당신의 앱에 처음에 하나의 도구 모음만 있었을 때는 다양한 작업의 구현을 버튼의 자식 클래스들에 배치해도 괜찮았습니다. 즉, CopyButton 자식 클래스의 내부에 텍스트를 복사하는 코드가 있어도 괜찮았습니다. 그러나 복사를 할 수 있도록 하는 콘텍스트 메뉴, 바로 가기 및 기타 항목들을 구현하면 당신은 이제 해당 작업의 코드를 많은 클래스에 복제하거나 버튼에 의존하는 메뉴들을 만들어야 하는데, 이것은 오히려 더 나쁜 옵션입니다.
해결책
올바른 소프트웨어 디자인은 종종 관심사 분리의 원칙을 기반으로 합니다. 가장 일반적인 예로는 그래픽 사용자 인터페이스용 레이어와 비즈니스 로직용 레이어의 분리입니다. 그래픽 사용자 인터페이스용 레이어는 모든 입력을 캡처하고 화면에 아름다운 그림을 렌더링하고 사용자와 앱이 수행하는 작업의 결과를 나타내는 역할을 합니다. 그러나 달의 행성 궤도를 계산하거나 연간 보고서를 작성하는 것과 같은 중요한 작업을 수행할 때 그래픽 사용자 인터페이스 레이어는 비즈니스 논리의 배경 레이어들에 작업을 위임합니다.
위의 내용은 코드로 다음과 같이 표현될 수 있습니다. 그래픽 사용자 인터페이스 객체가 비즈니스 논리 객체의 메서드를 호출하고 일부 인수를 전달합니다. 위 프로세스는 일반적으로 한 객체가 다른 객체에 요청을 보내는 것이라고 불립니다.
그래픽 사용자 인터페이스 객체들은 비즈니스 논리 객체들에 직접 접근할 수 있습니다.
커맨드 패턴은 그래픽 사용자 인터페이스 객체들이 이러한 요청을 직접 보내서는 안된다고 합니다. 대신 모든 요청 세부 정보들(예: 호출되는 객체, 메서드 이름 및 인수 리스트)을 요청을 작동시키는 단일 메서드를 가진 별도의 커맨드 클래스로 추출하라고 제안합니다.
커맨드 객체들은 다양한 그래픽 사용자 인터페이스 객체들과 비즈니스 논리 객체들 간의 링크 역할을 합니다. 이제부터 그래픽 사용자 인터페이스 객체는 어떤 비즈니스 논리 객체가 요청을 받을지와 이 요청이 어떻게 처리할지에 대하여 알 필요가 없습니다. 그래픽 사용자 인터페이스 객체는 커맨드를 작동시킬 뿐이며, 그렇게 작동된 커맨드는 모든 세부 사항을 처리합니다.
커맨드를 통해 비즈니스 논리 레이어를 접근합니다.
이제 다음 단계는 당신의 커맨드들이 같은 인터페이스를 구현하도록 하는 것입니다. 일반적으로 커맨드는 매개 변수들을 받지 않는 단일 실행 메서드만을 가집니다. 이 인터페이스는 다양한 커맨드들을 커맨드들의 구상 클래스들과 결합하지 않고 같은 요청 발신자와 사용할 수 있게 해줍니다. 이제 당신은 발신자에 연결된 커맨드 객체들을 전환할 수 있으며, 그렇게 하여 런타임에 발신자의 행동을 변경할 수 있습니다.
당신은 요청 매개변수들이 빠져있다는 점을 눈치채셨을 것입니다. 그래픽 사용자 인터페이스 객체가 비즈니스 레이어 객체에 일부 매개변수들을 제공했을 수 있습니다. 커맨드 실행 메서드에 매개변수들이 없는데, 그러면 어떻게 요청의 세부 정보를 수신자에게 전달할 수 있을까요? 커맨드를 이러한 데이터로 미리 설정해놓거나, 이 데이터를 자체적으로 가져올 수 있도록 해야 합니다.
그래픽 사용자 인터페이스 객체들은 작업을 커맨드들에 위임합니다.
다시 당신의 텍스트 편집기를 살펴봅시다. 커맨드 패턴을 적용한 후에는 더 이상 다양한 클릭 행동들을 구현하기 위한 여러 버튼 자식 클래스들이 필요하지 않습니다. 기초 Button 클래스에 커맨드 객체에 대한 참조를 저장하는 단일 필드를 넣은 후 이 버튼이 클릭 될 때 그 커맨드를 시행하도록 하면 됩니다.
이제 가능한 모든 작업에 대해 많은 커맨드 클래스들을 구현하고 이 클래스들을 버튼의 의도된 동작에 따라 특정 버튼들과 연결해야 합니다.
메뉴, 단축키 또는 대화 상자와 같은 다른 그래픽 사용자 인터페이스 요소들도 같은 방식으로 구현할 수 있습니다. 이들은 사용자가 그래픽 사용자 인터페이스 요소와 상호 작용할 때 실행되는 커맨드에 연결될 것입니다. 지금쯤 짐작하셨겠지만 같은 작업과 관련된 요소들은 같은 커맨드들에 연결되어 코드 중복을 방지할 것입니다.
결과적으로 커맨드들은 그래픽 사용자 인터페이스 레이어와 비즈니스 로직 레이어 간의 결합도를 줄이는 편리한 중간 레이어들이 됩니다. 그리고 이것은 커맨드 패턴이 제공할 수 있는 이점의 극히 일부에 불과합니다!
실제상황 적용
레스토랑에서 주문하기.
당신은 도시를 한참 걷다가 멋진 레스토랑에 도착하여 창가 테이블에 앉습니다. 친절한 웨이터가 다가와 신속하게 당신의 주문을 받아 종이에 적습니다. 웨이터는 부엌으로 가서 주문을 벽에 붙입니다. 잠시 후 요리사에게 주문이 전달되고 요리사는 주문을 읽고 그에 따라 음식을 요리합니다. 요리사는 주문과 함께 식사를 트레이에 놓습니다. 웨이터는 트레이를 발견한 후 당신이 주문한 대로 식사가 요리되었는지 확인하고 완성된 주문을 당신의 테이블로 가져옵니다.
종이에 적힌 주문은 커맨드 역할을 합니다. 이 주문은 요리사가 요리할 준비가 될 때까지 대기열에 남아 있습니다. 주문에는 식사를 요리하는 데 필요한 모든 관련 정보가 포함되어 있습니다. 이를 통해 요리사는 당신에게서 주문 세부 사항을 직접 전달받는 대신 바로 요리를 시작할 수 있습니다.
/**
* The Command interface declares a method for executing a command.
*/
class Command {
public:
virtual ~Command() {
}
virtual void Execute() const = 0;
};
/**
* Some commands can implement simple operations on their own.
*/
class SimpleCommand : public Command {
private:
std::string pay_load_;
public:
explicit SimpleCommand(std::string pay_load) : pay_load_(pay_load) {
}
void Execute() const override {
std::cout << "SimpleCommand: See, I can do simple things like printing (" << this->pay_load_ << ")\n";
}
};
/**
* The Receiver classes contain some important business logic. They know how to
* perform all kinds of operations, associated with carrying out a request. In
* fact, any class may serve as a Receiver.
*/
class Receiver {
public:
void DoSomething(const std::string &a) {
std::cout << "Receiver: Working on (" << a << ".)\n";
}
void DoSomethingElse(const std::string &b) {
std::cout << "Receiver: Also working on (" << b << ".)\n";
}
};
/**
* However, some commands can delegate more complex operations to other objects,
* called "receivers."
*/
class ComplexCommand : public Command {
/**
* @var Receiver
*/
private:
Receiver *receiver_;
/**
* Context data, required for launching the receiver's methods.
*/
std::string a_;
std::string b_;
/**
* Complex commands can accept one or several receiver objects along with any
* context data via the constructor.
*/
public:
ComplexCommand(Receiver *receiver, std::string a, std::string b) : receiver_(receiver), a_(a), b_(b) {
}
/**
* Commands can delegate to any methods of a receiver.
*/
void Execute() const override {
std::cout << "ComplexCommand: Complex stuff should be done by a receiver object.\n";
this->receiver_->DoSomething(this->a_);
this->receiver_->DoSomethingElse(this->b_);
}
};
/**
* The Invoker is associated with one or several commands. It sends a request to
* the command.
*/
class Invoker {
/**
* @var Command
*/
private:
Command *on_start_;
/**
* @var Command
*/
Command *on_finish_;
/**
* Initialize commands.
*/
public:
~Invoker() {
delete on_start_;
delete on_finish_;
}
void SetOnStart(Command *command) {
this->on_start_ = command;
}
void SetOnFinish(Command *command) {
this->on_finish_ = command;
}
/**
* The Invoker does not depend on concrete command or receiver classes. The
* Invoker passes a request to a receiver indirectly, by executing a command.
*/
void DoSomethingImportant() {
std::cout << "Invoker: Does anybody want something done before I begin?\n";
if (this->on_start_) {
this->on_start_->Execute();
}
std::cout << "Invoker: ...doing something really important...\n";
std::cout << "Invoker: Does anybody want something done after I finish?\n";
if (this->on_finish_) {
this->on_finish_->Execute();
}
}
};
/**
* The client code can parameterize an invoker with any commands.
*/
int main() {
Invoker *invoker = new Invoker;
invoker->SetOnStart(new SimpleCommand("Say Hi!"));
Receiver *receiver = new Receiver;
invoker->SetOnFinish(new ComplexCommand(receiver, "Send email", "Save report"));
invoker->DoSomethingImportant();
delete invoker;
delete receiver;
return 0;
}
1. Command 패턴
목적: Command 패턴은 요청을 객체로 캡슐화하여 요청을 매개변수화하거나, 요청을 큐에 저장하거나, 취소할 수 있도록 만드는 패턴입니다. 이 패턴은 행동 패턴(Behavioral Pattern)의 하나입니다.
구성요소:
- Command 인터페이스: 실행될 작업을 정의합니다.
- Concrete Command: Command 인터페이스를 구현하고, 구체적인 작업을 실행합니다.
- Invoker: Command 객체에 작업을 요청하는 역할을 합니다.
- Receiver: 실제 작업을 수행하는 객체입니다.
사용 예:
- 여러 명령을 취소하거나 재실행해야 할 때.
- 작업을 큐에 저장하고 나중에 실행해야 할 때.
- 버튼을 클릭했을 때 다양한 작업(명령)을 실행할 수 있게 하는 경우.
2. MVC 패턴
목적: MVC 패턴은 Model, View, Controller로 구성된 소프트웨어 디자인 패턴으로, 주로 UI 기반 애플리케이션에서 데이터를 분리하고 관리하는 데 사용됩니다. 이 패턴은 아키텍처 패턴(Architectural Pattern)에 속합니다.
구성요소:
- Model: 애플리케이션의 데이터와 그 데이터를 처리하는 로직을 포함합니다.
- View: 데이터를 사용자에게 표시하는 역할을 합니다. 주로 UI 요소를 의미합니다.
- Controller: 사용자의 입력을 받고, Model과 View 사이의 흐름을 제어하는 역할을 합니다. 사용자의 액션에 따라 Model을 업데이트하거나 View를 갱신합니다.
사용 예:
- 웹 애플리케이션, 모바일 애플리케이션에서 데이터를 다루는 구조로 많이 사용됩니다.
- 데이터를 독립적으로 관리하면서도 그 데이터의 표현을 유연하게 수정해야 하는 경우.
차이점
- 목적의 차이
- Command 패턴은 작업이나 명령을 객체로 캡슐화하여 작업의 실행, 취소, 저장 등을 유연하게 처리하는 것이 목적입니다.
- MVC 패턴은 데이터를 처리하는 로직(Model), 데이터를 표시하는 화면(View), 그리고 사용자 입력을 처리하는 흐름(Controller)을 분리하여 애플리케이션의 아키텍처를 설계하는 것이 목적입니다.
- 적용 범위
- Command 패턴은 특정 작업이나 명령을 유연하게 관리해야 할 때, 주로 행동 관련 문제를 해결하는 데 사용됩니다.
- MVC 패턴은 애플리케이션 전체의 아키텍처를 설계하는 데 사용되며, 데이터와 그 데이터의 표현, 사용자 입력을 분리하여 개발을 용이하게 만듭니다.
- 구조
- Command 패턴은 단일 작업에 대한 요청을 처리하는 구조를 가지며, 명령 실행, 취소, 스케줄링과 같은 기능을 구현하는 데 중점을 둡니다.
- MVC 패턴은 애플리케이션의 여러 레이어를 명확하게 분리하여 애플리케이션의 복잡성을 줄이고 유지보수를 용이하게 합니다.
결론
Command 패턴은 주로 작업의 유연한 실행과 관리를 위해 사용되며, MVC 패턴은 애플리케이션의 구조적인 설계를 목적으로 합니다. Command 패턴은 특정 행동과 관련이 있고, MVC는 애플리케이션의 데이터 흐름과 구조를 관리하는 데 더 중점을 둡니다.
1. Command 패턴이 MVC에서 수행할 수 있는 역할
Command 패턴이 MVC 패턴의 Controller와 유사한 기능을 담당하면서, 명령 캡슐화라는 고유한 기능을 함께 수행할 수 있습니다. 다음과 같은 상황에서 Command 패턴은 MVC 구조 내에서 유용하게 적용될 수 있습니다.
- Controller에서 명령을 캡슐화: MVC 패턴에서 Controller는 사용자 입력을 받아서 이를 Model과 View로 전달하는 역할을 합니다. 이때 Controller가 처리해야 하는 동작이 많고, 이러한 동작을 객체화해야 하는 경우 Command 패턴을 적용할 수 있습니다. 이렇게 하면 각각의 동작을 캡슐화하여 관리할 수 있고, 이를 통해 명령의 큐잉, 취소, 재실행 등의 기능을 쉽게 구현할 수 있습니다.
- 명령 실행의 유연성: 사용자의 여러 입력을 처리하는 동작들이 복잡한 경우, Controller가 직접 명령을 처리하는 대신, 각 명령을 Command 객체로 분리하여 처리할 수 있습니다. 이 방식은 특히 복잡한 UI 애플리케이션에서 유용하며, 실행 취소 및 반복 실행(redo/undo) 등의 기능을 제공하기 쉽습니다.
2. Command 패턴과 MVC 패턴의 결합
Command 패턴을 MVC 구조 내에 적용하면, 다음과 같은 방식으로 패턴들이 상호 보완적으로 사용될 수 있습니다.
- Controller에서 Command 객체를 사용: Controller는 사용자의 입력을 처리할 때, 해당 입력에 따른 명령(Command) 객체를 생성하여 이를 실행하거나 큐에 저장할 수 있습니다. 이렇게 하면 Controller는 실제 동작에 대한 복잡한 로직을 처리하지 않고, 명령의 캡슐화된 실행만을 담당하게 되어 **단일 책임 원칙(Single Responsibility Principle)**을 유지할 수 있습니다.
- Undo/Redo 기능: Command 패턴의 가장 큰 장점 중 하나는 명령의 취소(undo) 및 재실행(redo) 기능입니다. MVC 구조에서 사용자 입력을 처리하는 Controller가 Command 패턴을 사용하여 이러한 기능을 제공할 수 있습니다. Controller는 사용자 입력에 따른 Command 객체를 생성하고 이를 실행하면서, 필요한 경우 실행한 명령을 저장하여 취소 또는 재실행할 수 있습니다.
3. 실제 예시
웹 애플리케이션에서 Command 패턴이 MVC 구조와 결합되는 예를 들어보겠습니다.
- Model: 애플리케이션의 비즈니스 로직과 데이터를 관리합니다.
- View: 사용자에게 데이터를 보여줍니다.
- Controller: 사용자의 입력을 받아 Model과 View 사이의 상호작용을 처리합니다.