한 객체가 혼자 수행할 수 없는 작업을 여러 개의 객체로 어떻게 분배하는지, 또 그렇게 하면서도 객체 사이의 결합도를 최소화하는 것에 중점을 둡니다.
템플릿 메소드, 인터프리터, 역할 변경, 비지터, 커맨드, 스트래티지, 미디에이터, 이터레이터, 메멘토, 옵저버, 스테이트 등이 있습니다.
커맨드 패턴을 부르는 다양한 명칭
커맨드 패턴
명령 패턴
작동(Action) 패턴
트랜잭션(Transaction) 패턴
커맨드 패턴이란?
커맨드 패턴(Command pattern)이란 요청을 객체의 형태로 캡슐화하여 사용자가 보낸 요청을 나중에 이용할 수 있도록 매서드 이름, 매개변수 등 요청에 필요한 정보를 저장 또는 로깅, 취소할 수 있게 하는 패턴입니다.
가장 주의 깊게 볼 사항은 커맨드 자체를 객체화한다는 점입니다.
재사용성이 높은 클래스를 설계하는 패턴을 말합니다.
요청 자체를 캡슐화합니다.
절차지향 프로그램에서는 이를 콜백 함수, 즉 어딘가 등록되었다가 나중에 호출되는 함수를 사용해서 이러한 매개변수화를 표현할 수 있습니다. 명령 패턴은 콜백을 객체지향 방식으로 나타낸 것입니다.
이 패턴의 핵심은 Command 인터페이스입니다.
커맨드 패턴 들여다 보기
실행될 기능을 캡슐화함으로써 기능의 실행을 요구하는 호출자(Invoker) 클래스와 실제 기능을 실행하는 수신자(Receiver) 클래스 사이의 의존성을 제거합니다. 따라서 실행될 기능의 변경에도 호출자 클래스를 수정 없이 그대로 사용 할 수 있습니다.
이렇게 하기 위해 행동과 리시버를 한 객체에 집어넣고 메소드 하나만 외부에 공개하는 방법을 사용한다.
보통 OOP에서는 주체 객체와 대상 객체가 존재하고, 대상 객체에 대한 액션은 주체 객체에서의 메쏘드로 처리하는데, 이 액션까지 객체로 만든 뒤에 처리하는 것입니다.
개방-폐쇄 원칙(OCP, Open-Closed Principle)
명령어를 인보커에 언제든지 등록할 수 있고, 원하는 언제든지 인보커로 간단하게 실행시킬 수 있습니다. 추후 명령이나 리시버가 더 추가될 경우 기존 코드에 수정 없이, 두 객체를 만들고 setCommand() 로 추가해주면 됩니다. 즉, OCP 원칙을 잘 지켜냅니다.
커맨드 패턴의 확장
메타 커맨드 패턴을 이용하게되면 명령으로 이루어진 메크로를 만들어서 여러 명령을 실행 가능합니다.
메크로 커맨드는 커맨드를 확장해서 여러 개의 커맨드를 한꺼번에 호출할 수 있게 해 주는 간단한 방법입니다. 매크로 커맨드에서도 어렵지 않게 작업취소 기능을 지원할 수 있습니다.
프로그래밍을 하다 보면 요청 자체를 리시버한테 넘기지 않고 자기가 처리하는 스마트 커맨드 객체를 사용하는 경우도 종종 있습니다.
장점
작업을 수행하는 객체(리시버)와 작업을 요청하는 객체를 분리하기 때문에 SRP 원칙(단일 책임 원칙)을 잘 지킵니다.
이벤트가 발생했을 때 실행될 기능이 다양하면서도 변경이 필요한 경우에 이벤트를 발생시키는 클래스를 변경하지 않고 재사용하고자 할 때 유용합니다.
기존 코드 수정 없이 새로운 리시버, 명령어 추가가 가능하기 때문에 OCP 원칙(개방 폐쇄 원칙)을 잘 지킵니다.
커맨드 단위의 별도의 액션(undo, redo) 등이 가능하고, 커맨드 상속 및 조합을 통해 더 정교한 커맨드를 구현할 수 있습니다.
단점
전체적으로 이해가 필요하고 복잡한 설계구조를 가집니다.
활용 상황
커맨드 발생 시점을 사용자가 커스터마이징 해야 하는 경우
서로 다른 시간에 요청을 명시하고, 저장하며, 실행하고 싶을 때 사용합니다.
여러 커맨드를 조합하여 하나의 커맨드처럼 사용할 필요가 있는 경우
커맨드 실행 취소, 재 실행 등의 기능을 구현해야 하는 경우
연산의 반복 사용을 통해 수행과 취소를 무한 반복할 수 있습니다.
수행할 동작을 객체로 매개변수화하고자 하는 경우
기본적인 연산의 조합으로 만든 상위 수준 연산을 써서 시스템을 구조화하고 싶을 때 사용합니다.
Command 패턴 사용 방법
커맨드 패턴에는 명령(command), 수신자(receiver), 발동자(invoker), 클라이언트(client)의 네개의 용어가 있습니다.
Command 인터페이스와 ConcreteCommand 구현체만 있어도 커맨드 패턴을 사용했다고 볼 수 있습니다. 다만 Receiver를 이용해 실제 명령의 실행 코드와 개념적인 명령을 분리하고, Invoker를 이용해 명령의 실제 실행을 Client와 분리하는 것이 좋습니다.
코드를 실행시키면, 장군이(client)이 전략(invoker)을 통하여 명령(command)를 내려 명령을 수행(Receiver)하게 됩니다.
식당 주문 시스템에 비유해서 알아보겠습니다.
어떤 식당에는 주방장과 웨이터가 있습니다. 이 식당에 어떤 손님이 들어와, 주문서에 스파게티를 작성하고 이를 웨이터에게 전달했습니다. 웨이터는 이 주문서를 읽고 주방장에게 스파게티 요리를 요청합니다.
손님 : Client
웨이터 : Invoker 객체
주문을 받는 것 : setCommand()
주문서 : Command 객체
주문을 주방장에게 전달하여 요리를 요청 하는 것 : execute()
주방장: Receiver 객체
Client
말 그대로 명령을 내릴 사용자입니다.
클라이언트는 ConcreteCommand를 생성하고 Receiver를 설정합니다.
누가(Receiver) 무엇(Command)을 할지에 대해서만 알고 있습니다.
클라이언트 객체는 발동자 객체와 하나 이상의 커맨드 객체를 보유합니다.
명령을 수행하려면, 클라이언트 객체는 발동자 객체로 커맨드 객체를 전달합니다.
클라이언트 객체는 어느 시점에서 어떤 명령을 수행할지를 결정합니다.
Receiver
리시버는 요구 사항을 수행하기 위해 어떤 일을 처리해야 하는지 알고 있는 객체입니다.
실제로 Client의 원하는 요청을 수행하는 객체입니다.
누가 언제 요청해왔는지 모릅니다. 액션에 대해서만 관심(구현)을 가집니다.
Invoker
인보커에는 명령이 들어 있으며, execute() 메소드를 호출함으로써 커맨드 객체에서 특정 작업을 수행해 달라는 요구를 하게 됩니다.
Client의 요청을 받아 실제로 Receiver의 액션을 호출하는 객체입니다.
Client는 Invoker를 거쳐 Receiver에게 요청하기 때문에, Invoker에서 Receiver에 도달하기 전, 요청에 대한 이런저런 설정들을 할 수가 있습니다.
발동자 객체는 필요에 따라 명령 발동에 대한 기록을 남길 수 있습니다.
한 발동자 객체에 다수의 커맨드 객체가 전달될 수 있습니다.
Command
ConcreteCommand의 추상 클래스입니다.
공통 인터페이스로 가집니다. 이 메소드에서는 리시버에 특정 작업을 처리하라는 지시를 전달합니다.
execute()
undo()
redo()
ConcreteCommand
커맨드 객체는 수신자 객체를 가지고 있으며, 수신자의 메서드를 호출하고, 이에 수신자는 자신에게 정의된 메서드를 수행합니다.
커맨드 객체는 별도로 발동자 객체에 전달되어 명령을 발동하게 합니다.
특정 행동과 리시버 사이를 연결해 줍니다.
인보커에서 execute() 호출을 통해 요청을 하면 ConcreteCommand 객체에서 리시버에 있는 메소드를 호출함으로써 그 작업을 처리합니다.
실제 구체적인 요청 내용을 담는 클래스입니다.
누가(Receiver)가 무엇을 해야하는지를 구현합니다.
execute 메소드 안의 로직
NoCommand 객체
NoCommand 객체는 일종의 널 객체(null object)입니다. 딱히 리턴할 객체는 없지만 클라이언트 쪽에서 null을 처리하지 않아도 되도록 하고 싶을 때 널 객체를 활용하면 좋습니다.
널 객체는 여러 디자인 패턴에서 유용하게 쓰입니다. 널 객체를 일종의 디자인 패턴으로 분류하기도 합니다.
스트레티지 패턴
추상화 수준이 더 많아진 스트레티지 패턴처럼 느껴질 수 있습니다.
차이가 있다면, 스트레티지 패턴은 전략을 실행하는 객체와 전략 객체의 관계가 중요하다면, 커맨드 패턴은 명령이 객체화되어 다루어지는 것이 중요합니다.
UML
Command interface
interfaceCommand{execute:()=>void;}
Invoker class
classInvoker{private theCommand: Command;// 생성자에서 버튼을 눌렀을 때 필요한 기능을 인지로 받습니다.constructor(theCommand: Command){this.setCommand(theCommand);}publicsetCommand(newCommand: Command){this.theCommand = newCommand;}// 버튼이 눌리면 주어진 Command의 execute 메서드를 호출한다.publicpressed(){this.theCommand.execute();}}
const carManager ={requestInfo:function(model, id){return`The information for ${model} with ID ${id} is foobar`;},buyVehicle:function(model, id){return`You have successfully purchased Item ${id}, a model`;},arrangeViewing:function(model, id){return`You have successfully booked a viewing of ${model} ( ${id} )`;},};
carManager.execute=function(name){return(
carManager[name]&&
carManager[name].apply(carManager,[].slice.call(arguments,1)));};
console.log(carManager.execute('buyVehicle','Ford Escort','453543'));
console.log(carManager.execute('arrangeViewing','Ferrari','14523'));
console.log(carManager.execute('requestInfo','Ford Mondeo','54323'));
console.log(carManager.execute('requestInfo','Ford Escort','34232'));
console.log(carManager.execute('buyVehicle','Ford Escort','34232'));