커맨드 패턴
Last updated
Last updated
요청 내역을 객체로 캡슐화해서 객체를 서로 다른 요청 내역에 따라 매개변수화 할 수 있습니다. 이러면 요청을 큐에 저장하거나 로그로 기록하거나 작업 취소 기능을 사용할 수 있습니다.
IOT 리모컨의 API를 만들어야 합니다. 리모컨에는 7개의 슬롯과 ON/OFF 버튼, UNDO(뒤로) 버튼이 있네요.
문제는 제어해야하는 객체의 클래스가 너무 많다는 거죠. 또, 앞으로 이와 비슷한 클래스가 더 추가될 수도 있을 겁니다. 버튼은 ON/OFF 2개 뿐인데 말이죠. 어떻게 해야 할까요 ?
커맨드 패턴은 음식 주문 과정과 유사합니다. 양쪽 그림을 비교해보면서 읽어보세요.
현실에서는 주문서(Command)에 주방장(Receiver)가 들어가진 않으니 그거 빼고는 유사한 것 같습니다.
Command(명령), Invorker(호출자), Execute(실행하다), Receiver(수신자)
음식 주문 과정
createOrder(): 고객(Client)는 테이블에 놓여있는 주문서(Command)에 먹을 메뉴를 적습니다.
takeOrder(): 고객(클라이언트)는 "저기요~ 메뉴 다적었어요~!"하며 주문서(Command)를 종업원(Invorker)에게 전달합니다.
orderUp(): 주문이 들어왔으니 알려야겠죠? 종업원(Invorker)은 카운터에가서 "주문 들어왔어요~!" 하고 알립니다. 실제와 다르게 무슨 주문이 들어왔는진 몰라도 됩니다. 전달만 하면 되니까요.
makeBurger(), makeShake(): 주방장(Receiver)은 아무것도 모르고 있다가 주문이 들어온 것을 확인하고 주문서대로 메뉴를 만듭니다.
커맨드 패턴
createCommandObject(): 클라이언트는 Command 객체를 생성합니다. Command에는 Receiver가 있어요.
setCommand(): 클라이언트는 Invorker 객체의 setCommand()를 호출하며, Command 객체를 넘겨줍니다. 인보커 객체는 Command 객체를 보관합니다.
execute(): Invorker는 Command 객체의 execute()를 실행합니다. 뭐가 실행되는지는 몰라도 됩니다.
action1(), action2(): Command 객체의 execute()가 실행되면 커맨드 안에 있는 Receiver의 특정 동작이 실행됩니다. TVon()/TVoff()/Lighton()/Lightoff() 같은 것들이죠.
가장 간단하게 조명만 켜는 딱 한가지 일을 하는 API를 만들어볼게요.
먼저 Command 인터페이스를 만듭니다. execute() 만 있으면 될거에요.
그리고, 조명만 켜는 일만 하는 LightOnCommand를 만들겁니다. 다음에 나오겠지만, 조명을 꺼야하는 LightOffCommand는 따로 만들어야 해요. 위에 그림에서 뭐랬죠? 커맨드는 Receiver를 받는다고 했죠. Light 객체가 Receiver 입니다. Command의 execute()가 실행되면 Receiver에 있는 on() 메소드가 실행되죠.
RemoteControl(리모컨)을 만들어 줄 건데요. RemoteControl에는 Command를 매개변수로 받아서 보관하는 setCommand가 있네요. 위에 그림에서는 종업원이 주문서를 takeOrder()하면서 받아갔었죠.
그러면 이제 실행을 해볼건데, 클라이언트는 LightOnCommand를 만들고 SimpleRemoteControl에게 LightOnCommand를 넘겨줍니다. 그러면 SimpleRemoteControl는 그 LightOnCommand를 잘 보관하고 있다가, buttonWasPressed() 하게 되면 Command의 execute()가 실행되죠.
커맨드 객체는 일련의 행동을 특정 리시버가 연결함으로써 요청을 캡슐화합니다. 그러기 위해서 커맨드에 receiver.action()과 receiver를 넣고, execute()라는 메소드 하나만 외부에 공개합니다. 밖에서 볼 때는 어떤 객체가 receiver 역할을 하는지, 그 receiver가 무슨 일을 하는지 알 수 없습니다. 그냥 execute()가 실행되면 해당 요청이 처리된다는 사실만 알죠.
명령으로 객체를 매개변수화 할 수도 있습니다. 아래의 코드를 보면 알 수 있는데요. 위의 코드에서 GarageDoorOpenCommand, GarageDoor 클래스를 추가했고 기존에 있던 remote에 새로운 커맨드를 할당하고 실행하면 "차고 문이 열렸습니다."라는 출력을 볼 수 있죠. GarageDoorOpenCommand 라는 객체가 마치 매개변수처럼 된 것이죠.
나머지 기능도 넣어봅시다. 차이점은 RemoteControl을 새로 만들 건데, Command 객체를 담을 배열을 하나 만들겁니다. 그리고 매개변수로 몇번째 요소를 execute()한 것인지 알아야하기 때문에 index를 받을거에요.
또 NoCommand() 라는 애를 하나 받을건데, 얘는 safety code 입니다. 커맨드에 아무것도 없을 때 배열의 없는 index 번호로 접근하면 crash가 발생하니까 그걸 방지하는 용도죠.
마지막으로는 예쁘게 출력하기 위해서 toString() 이라는 메소드를 하나 만들겁니다.
작업 취소 기능은 생각보다는 간단한데요.
먼저 Commmand 인터페이스에 undo()를 추가합니다. 그리고 각 서브클래스에 undo()를 구현해주어야 해요. 예를 들어, LightOnCommand 이면 undo() 내부에는 light.off() 를 구현해야 겠죠.
그리고, RemoteControl로 와서 undoCommmand 라는 변수를 생성합니다. 그리고 on/offButtonWasPushed 할 때마다 해당 커맨드를 undoCommand에 넣어주는 거에요.
마지막으로 undoButtonWasPushed() 라는 메소드를 만들어서 execute()를 실행시켜줬던 것처럼 undoCommand.undo()를 실행시켜 주면 되겠죠.
중요한 포인트는 excute() 에서 getSpeed()로 가져온 상태값을 prevSpeed에 할당해줘야 하는 부분입니다.
그리고 Speed는 static의 int 변수로 선언하는 것보단 enum을 활용하는게 더 좋겠죠.
다른 커맨드를 실행할 수 있는 새로운 종류의 커맨드를 생성합니다. 좋은 아이디어죠.
커맨드 패턴을 활용하면 커맨드를 받는 큐를 만들어서 A 작업을하다가 전혀 상관없는 B 작업을 하게 할 수 있습니다.
커맨드 패턴을 활용하면 로그기록기를 생성해서 복구 시스템을 구축할 수 있습니다.
커맨드 패턴 적용 전의 코드
하이브리드 앱을 구현 중입니다. 그리고 브릿지가 연결된 상태이죠.
웹뷰에서 어떤 이벤트가 올 때마다 SNS 공유하기, 새 창열기, 현재 창 닫기 등 많은 이벤트가 발생합니다.
예시 case는 3개지만 실제로는 20개가 넘고, 앞으로도 더 생길 수 있죠. 코드마다 최소 20줄에서 최대 100줄 이상도 갈 수가 있구요. 이러면 앱의 유지보수가 너무 힘들어집니다. 계속 기존 코드 부분을 바꿔야 하구요.
커맨드 패턴 적용 후의 코드
해야 될 작업을 각각 커맨드 객체로 묶습니다. 그리고 dictionary를 생성해서 키값으로 해당 커맨드를 가져오죠. 마지막으로 단 1줄로 execute()를 호출합니다.
이렇게 하면 나중에 브릿지가 추가되도 WebViewController의 다른 부분은 건드리지 않아도 됩니다. 새로 커맨드를 만들고, commands에 맞는 키값으로 만든 커맨드 서브 클래스를 넣어주기만 하면 되겠죠. 코드 가독성도 훨씬 뛰어나네요.
https://www.hanbit.co.kr/store/books/look.php?p_code=B6113501223