본문 바로가기

Design Pattern

[디자인 패턴_Again] Command Pattern.

이 패턴은 요청을 한 녀석과 요청을 처리하는 녀석을 완전히 분리시키는 패턴입니다.

즉, 명령어 그 자체를 하나의 클래스로 만들어서

요청을 처리하는 녀석과 요청을 하는 녀석의 중간 고리를 느슨하게 만드는 역할을 하는겁니다.

실생활에서 비슷하게 예를 들만한 것이

멀티리모컨이 있을 것 같습니다.

요즘 리모컨은 하나의 리모컨에 TV, DVD, 케이블, 비디오등 여러개의 장치들을 하나의

리모컨에서 작동 시킬 수 있도록 만들어져 나옵니다.

물론, DVD나 TV등 각각의 고유한 동작들도 있겠지만

전원 on, off와 같은 장치의 종류에 상관없이 공통적으로 동작하는 명령도 있습니다.

이런 것을 명령어 자체를 하나의 인터페이스로 규약을 정해 놓고

(실행해야 하는 메소드가 어떤 것인지만 알고 있으면..)
 
요청측과 처리측을 분리하여 프로그래밍하는 커맨드 패턴을 사용하여 구현 할 수 있습니다.

이 커멘드 클래스들은 자기가 어느 객체에게 요청을 해야하는지를 알고 있어야합니다.

즉, 이 커멘드 클래스들 자체가 독립적으로도 사용이 될 수 있다는 것이죠.

만들다만, 와우짝퉁으로 한번 이야기해보겠습니다.

전투와 방어버튼이 있습니다.

그리고 전사와 사제, 도적, 마법사는 전투버튼을 누를 때 각각 고유의 행동을 합니다.

전사 - 칼을 뽑는다.
사제 - 축복 주문을 시전한다.
도적 - 그림자 숨기를 한다.
마법사 - 파이어볼을 캐스팅한다.

방어버튼을 누르면

전사 - 방패를 앞으로 든다.
사제 - 방어력 업 버프를 시전한다.
도적 - 백스탭을 한다.
마법사 - 블링크를 시전한다.

전투와 방어버튼은 하나뿐이지만, 여기에 어떤 대상이 연결되어 있느냐에 따라서
그 행동이 달라집니다. 물론 이 버튼 자체는 자기가 어떤 캐릭터에게 명령을 내리는지
알지 못 합니다.

게임 유저는 알고 있겠죠, 지금 전사를 조종하고 있으니까
전투 버튼을 누르면 칼을 뽑을 것이고 방어 버튼을 누르면 방패를 들 것이다 라고..

하지만 중요한 것은 버튼 그 자체는 대상을 모른다는 것입니다.

만약, 버튼자체가 자기가 명령을 내려야 하는 대상을 알아야 한다면
전사가 나왔을때는 전사용 버튼이, 사제가 나왔을 때는 사제용 버튼이 ..
이런식으로 대상에 맞는 버튼이 각각 나와줘야 할 것입니다.

그럼 이게 어떤식으로 구현이 되는 것인지 알아보겠습니다.

일단, 명령어 자체를 캡슐화 시킨다고 했습니다. 그리고 캐릭터와 버튼은 이 명령어를
둘 사이의 인터페이스로 사용합니다.

public interface Command {
 public void execute();
}

일단, 인터페이스를 하나 작성합니다.

이것이 버튼으로부터 객체에게 요청을 할 때 사용 할 Command 인터페이스입니다.

전사 클래스는 아래와 같습니다.

public class Warrior {
 
 public void attack() {
  System.out.println("칼을 뽑습니다.");
 }
 
 public void guard() {
  System.out.println("방패를 앞으로 듭니다.");
 }
}
battle은 칼을 뽑고 defence는 방어를 합니다.



일단 전사에 대한, 두개의 Command를 만들어보겠습니다.

public class WarriorBattleCommand implements Command {
 
 Warrior warrior;
 
 public WarriorBattleCommand(Warrior warrior) {
  this.warrior = warrior;
 }
 
 public void execute() {
  warrior.attack();
 }
}
------------------------------------------------------------

public class WarriorDefenceCommand implements Command {
 
 Warrior warrior;
 
 public WarriorDefenceCommand(Warrior warrior) {
  this.warrior = warrior;
 }
 
 public void execute() {
  warrior.guard();
 }
}

대충 감이 오시는 분도 계실 것입니다.
이 커멘드 클래스는 자기가 어떤 대상에게 요청을 해야하는지 알고 있습니다.
위의 경우에는 Warrior가 그 대상이죠. 그리고, 클래스명을 봐도 알 수 있듯이
명령어 그 자체가 클래스로 구현되어져 있는 것입니다.

그러면 이것을 호출 할 버튼은 어떻게 구현이 될까요..?

public class ButtonControl {
 Command command;
 
 public void setCommand(Command command) {
  this.command = command;
 }
 
 public void buttonPress() {
  command.execute();
 }

}

버튼은 보시면 아시겠지만, 전투와 방어 버튼의 구분이 없습니다.

전투와 방어라는 것 자체는 Command 인터페이스를 구현한 클래스로 만들어지고

이 버튼은 단지 그 command의 execute()메서드를 실행하면 된다라는 것만 알고 있을 뿐입니다.

대상과 처리를 요청하는 부분이 분리가 되는 것입니다.

일단 테스트를 해보겠습니다.

public class Test {
 public static void main(String[] args) {
  ButtonControl bc = new ButtonControl();
  Warrior warrior = new Warrior();
  WarriorBattleCommand warriorBattleCommand = new WarriorBattleCommand(warrior);
 
  bc.setCommand(warriorBattleCommand);
  bc.buttonPress();
 
  WarriorDefenceCommand warriorDefenceCommand = new WarriorDefenceCommand(warrior);
  bc.setCommand(warriorDefenceCommand);
  bc.buttonPress();
 }
}

일단 버튼 객체를 생성하고, 명령을 수행 할 캐릭터(Warrior)를 생성합니다.
그리고, 그 캐릭터가 수행 할 명령을 생성 한 후 Warrior를 셋팅합니다.
그리고 buttonPress를 수행하면

칼을 뽑습니다.
방패를 앞으로 듭니다.
이렇게 실행이 됩니다.

잘 생각해보시면.. 일단 warriorBattleCommand 이 자체로도 자기가 이 명령을 요청 할 대상을
알고 있습니다. 이게 어떤 장점이 있는가하면..

만약에 버튼이 바뀌었다고 해보겠습니다. UI가 변경되어서 키보드가 아닌 마우스로 조종을
하게 된 것이죠. 그렇다고해도 크게 문제 될 부분이 없는 것이, command의 execute() 메소드만 실행하면 된다는 것만 알고 있으면, 행동을 요청하는 측이 무엇이 되던지간에 상관이 없기 때문입니다. 즉

public class MouseControl {
 Command command;
 
 public void setCommand(Command command) {
  this.command = command;
 }
 
 public void click() {
  command.execute();
 }

}

이런식으로 말이죠..

이 커멘드 패턴에서 요청을 받아 처리하는 측(Warrior)을 Receiver라고 얘기합니다.
그리고 ButtonControl과 같은 클래스를 Invoker라고 합니다. 이 인보커는 명령을 가지고 있고.
execute() 메소드를 호출하면 커멘드 객체에게 특정 작업을 수행하도록 요청 할 수 있다는 사실을
알고 있습니다.

그리고 Command는 모든 커멘드 객체에서 구현해야 할 인터페이스입니다.

이것을 구현해야만 Invoker가 (ButtonControl) execute()를 호출하면 명령을 처리 할 수
있다.라는 서로의 약속을 지킬 수 있게 됩니다.

그리고 이 Command 인터페이스를 구현한 클래스를
ConcreteCommand라고 합니다. 이녀석은 Receiver(Warrior)의 메소드를 호출하여
명령을 처리합니다.

그리고 Test 클래스에 해당하는 것이 Client입니다.

이제, 사제 클래스를 한번 보겠습니다.

사제 클래스는 앞에서 얘기했듯이

battle버튼을 누르면
사제 - 축복 주문을 시전하고
방어버튼을 누르면
사제 - 방어력 업 버프를 시전합니다.

그런데 이 주문 시전의 절차가 조금 복잡스러워서

1. 책을 꺼내고
2. 대상을 설정하고
3. 주문을 외운다.

이 3단계를 거쳐야 주문이 시전된다고 합니다.

이런 것은 어떻게 구현하면 될까요?

public class Priest {
 
 public void castBless() {
  System.out.println("축복 주문을 시전!!");
 }
 
 public void defenceUpBuff() {
  System.out.println("방어도 업!!");
 }
 
 public void takeBook() {
  System.out.println("책을 꺼냅니다.");
 }
 
 public void targeting() {
  System.out.println("대상을 설정한다.");
 }
 
 public void casting() {
  System.out.println("주문을 외우는 중입니다...");
 }
}
주문을 외우기 위해 사제클래스는 위와 같은 행동들을 가지고 있습니다.

그러면, battle버튼을 눌렀을때의 command는 어떻게 만들어지면 될까요?

public class PriestBattleCommand {
 Priest priest;
 
 public PriestBattleCommand(Priest priest) {
  this.priest = priest;
 }
 
 public void execute() {
  priest.takeBook();
  priest.targeting();
  priest.casting();
  priest.castBless();

 }
}
눌려지는 Button은 battle 버튼 하나지만 실제로 실행되는 것은 각각의 receiver(Priest와 Warrior)의 메소드들입니다.

public class Test {
 public static void main(String[] args) {
  ButtonControl bc = new ButtonControl();
  Warrior warrior = new Warrior();
  WarriorBattleCommand warriorBattleCommand = new WarriorBattleCommand(warrior);
 
  bc.setCommand(warriorBattleCommand);
  bc.buttonPress();
 
  WarriorDefenceCommand warriorDefenceCommand = new WarriorDefenceCommand(warrior);
  bc.setCommand(warriorDefenceCommand);
  bc.buttonPress();
 
  System.out.println("\n사제는?");
  Priest priest = new Priest();
  PriestBattleCommand priestBattleCommand = new PriestBattleCommand(priest);
  bc.setCommand(priestBattleCommand);
  bc.buttonPress();
 }
}

Button클래스는 한번만 생성되었지만
셋팅되는 Command에 따라서 각각의 Receiver가 알맞은 행동을 할 수 있습니다.

이것은 Command 클래스를 사용하는 법만 알고 있다면 그것이 버튼이던 마우스던
손가락이던 리모컨이던 어느 곳에서도 위와 똑같은 행동을 요청하고 처리 할 수 있다는 뜻입니다.

요청하는측과 실제 일을 하는 측이 완전히 분리되어버린거죠.

그리고 이것은 Command가 그 자체로도 의미가 있도록 캡슐화가 되어있기 때문에
각각의 Command 클래스들을 큐에 넣어놓고, 스케쥴링이나 작업큐등 다양한 용도로도 사용 할
수 있습니다.

만약에 쓰레드 A가 priestBattleCommand를 큐에 넣어놨는데
쓰레드 B가 이 priestBattleCommand를 처리 할 수도 있습니다.
단지 Command의 execute 메소드를 호출하면 되니까요
(실제로 무슨 명령어인지 누가 그 명령을 처리 할건지 몰라도 상관없습니다!)