Web Application/JAVA

[JAVA]Command 패턴

있어요 2016. 1. 18. 16:48
홈페이지의 글은 anti-nhn license에 따릅니다.
출처 - http://egloos.zum.com/iilii/v/5378691


- 패턴에 대해 공부 중 커맨드 패턴에 대해 잘 정리 된 글이 있어서 가져왔다.

[JAVA]Command 패턴



명령의 객체화!! 가 Command 패턴의 핵심입니다. 언어적으로 말하면 명사화 시킨다고 보시면 됩니다. 

모든 문장은 [ 문장 => (문장을 명사화시킨 거)를 한다. ] 와 같이 바꿀 수 있습니다.
 
예를 들면,

난 너를 사랑해 = (난 너를 사랑함)을 한다.
학교에 간다 = (학교에 감)을 한다.

와 같이 무엇이든 명사화시킨 후 "을 한다"를 붙여 버리면 된다는 겁니다. 이것을 코드로 표현하자면.

Person i = new Person();
i.work();

와 같은 코드는

public interface Command{
    void execute();
}

new Command(){
    @Override
    public void execute(){
        Person i = new Person();
        i.work();
    }
}.execute()

와 같습니다.

모하러 저렇게 인터페이스를 만들고 감싸는 짓을 할까 싶습니다.
이유는 보통 둘 중 하나인데요.
첫째는 명령을 만드는 시점과 실행시키는 시점이 다르거나 만드는 놈과 실행시키는 놈이 다를 경우입니다. 윈도우의 예약작업이나 linux의 cronjob 같은 걸 보면, 실행시키는 놈은 실행되는 프로그램과 실행 시점만 알죠. 그 프로그램이 무엇을 할지는 모릅니다. 그와 같이 Command는 execute라는 메쏘드만 가진 인터페이스고, 그것을 실행시키는 놈은 뭔지는 모르겠지만 실행하는 겁니다.
둘째는 command stack 을 이용해 작업의 취소 또는 재실행 등을 할 수 있게 하기 위한 것입니다.  요건 뒷부분에서 다시 자세히 설명하겠습니다.

2. 예제

----- Command를 표현하는 Interface -----
package ch18_Command;

public interface Command {
 void execute();
}

----- 테스트 클래스 -----
package ch18_Command;

import java.util.ArrayList;
import java.util.List;

public class Main {

 public static void main(String[] args) {
  List<Command> cmds = new ArrayList<Command>();
  
  cmds.add(new Command() {
   @Override
   public void execute() {
    System.out.println("조낸 삽질!!");
   }
  });
  
  cmds.add(new Command() {
   @Override
   public void execute() {
    System.out.println("시장가서 어묵 먹기!");
   }
  });
  
  //여기서부터는 실행부..
  for (Command command : cmds) {
   command.execute(); //이놈은 지가 실행시키는 게 뭔지도 모른다!
  }
 }
}

----- 테스트 결과 -----
조낸 삽질!!
시장가서 어묵 먹기!



테스트 프로그램을 보면, 명령을 만들어서 list에 담습니다. 그리고 그것을 돌면서 실행시킵니다. 즉, 명령이 만들어지는 부분과 실행시키는 부분이 다른 경우입니다.

3. Command Stack

command stack을 쓰는 것은 작업의 실행, 취소를 반복할 수 있도록 하는 것입니다. 작업이 일회성이 아니라 언젠가 다시 실행될 수도 있는 게 됩니다. 이 때는 일반적인 Command에서는 execute() 라는 하나의 메쏘드만 있던 것과는 달리 undo()와 redo() 기능이 필요합니다. 명령을 최초에 실행시키는 것은 일반적으로 명령을 추가한 후 redo()하는 것과 같습니다.

그나마 간단한 예제를 보자면 다음과 같습니다.

----- undo와 redo를 정의한 Reversible Command -----
package ch18_Command_2;

public interface ReversibleCommand {
 void redo();
 void undo();
}

----- Command의 list를 관리하고 실행, 실행취소 등을 호출하는 역할을 하는 Command Stack -----
package ch18_Command_2;

import java.util.ArrayList;
import java.util.List;

public class CommandStack {
 private int current = -1;
 private List<ReversibleCommand> commands = new ArrayList<ReversibleCommand>();
 
 public void execute(ReversibleCommand c){
  for (int i = commands.size() -1;  i > current; i--) {
   commands.remove(i);
  }
  commands.add(c);
  redo();
 }
 public void redo() {
  commands.get(++current).redo();
 }
 public void undo(){
  commands.get(current--).undo();
 }
}

----- 테스트의 대상이 될 Panel, 얘는 색깔을 가지며, 그에 대한 getter와 setter만 있다. (초기색은 빨강) -----
package ch18_Command_2;

public class Panel{
 private String color = "빨강";
 public String getColor() {
  return color;
 }
 public void setColor(String color) {
  System.out.println(this.color +" 에서 " + color +" 로 색깔 바뀜.");
  this.color = color;
 }
}

-----  Panel의 색깔을 바꿔주는 Command의 구현체 -----
package ch18_Command_2;

public class PanelChangeCommand implements ReversibleCommand {
 private final Panel panel;
 private final String newColor;
 private final String oldColor;

 public PanelChangeCommand(Panel panel, String newColor) {
  this.panel = panel;
  this.oldColor = panel.getColor();
  this.newColor = newColor;
 }
 @Override
 public void redo() {
  panel.setColor(newColor);
 }
 @Override
 public void undo() {
  panel.setColor(oldColor);
 }
}

----- 테스트 클래스 -----
package ch18_Command_2;

public class Main {
 public static void main(String[] args) {
  Panel panel = new Panel();
  CommandStack cs = new CommandStack();
  cs.execute(new PanelChangeCommand(panel, "주황"));
  cs.execute(new PanelChangeCommand(panel, "노랑"));
  cs.execute(new PanelChangeCommand(panel, "초록"));
  
  System.out.println("--undo 2번--");
  cs.undo();
  cs.undo();
  System.out.println("--redo 1번--");
  cs.redo();
  cs.execute(new PanelChangeCommand(panel, "파랑"));
 }
}
----- 테스트 결과 -----
빨강 에서 주황 로 색깔 바뀜.
주황 에서 노랑 로 색깔 바뀜.
노랑 에서 초록 로 색깔 바뀜.
--undo 2번--
초록 에서 노랑 로 색깔 바뀜.
노랑 에서 주황 로 색깔 바뀜.
--redo 1번--
주황 에서 노랑 로 색깔 바뀜.
노랑 에서 파랑 로 색깔 바뀜.


Panel은 색깔을 가지고 있는  단순한 클래스이며, 이 색깔을 여러가지로 바꿀 수 있습니다.

command stack은 지금 이 순간 어느 Command까지 실행했다는 위치를 가지고 있습니다. 그래서 redo가 실행되면, 한칸 올려서 실행시키고 undo가 실행되면 한칸 내려서 실행합니다. 실제로 명령을 실행시키는 역할과 명령의 리스트를 보관하는 역할을 담당합니다.

ReversibleCommand는 undo와 redo에 대해 정의를 하고 있습니다. 인터페이스는 함스를 정의하는 것까지 밖에 할 수 없지만, 실제 구현체는 Command가 정의되는 순간에 undo에서는 무엇을 하고 redo에서는 무엇을 할지 정확히 알고 있어야 합니다.

PanelChangeCommand는 ReversibleCommand의 구현체입니다. 위에서 말한 대로 명령이 결정되는 순간(PanelChangeCommand가 만들어지는 순간)에 자기가 할 undo는 무엇이고(undo가 호출될 때 어떤 색깔로 바꿀 것이고) redo는 무엇인지(redo가 실행될 때 어떤 색깔로 바꿀 것인지)를 결정합니다. 얘가 편한 점은 자기가 바꿔야할 것이 무엇인지 알고, 그에 적합한 undo와 redo를 약간의 정보(지금 바꾸려는 Panel이 어떤 놈인지와 어떤 색깔로 바꾸려는지 )를 바탕으로 스스로 정의한다는 것입니다.(Constructor의 인자는 redo시 어떤 색깔로 바꿀 것인지는 알려주지 않습니다. 내부적으로 자기 색깔을 스스로 가져와서 redo시 바꿀 색깔을 스스로 가지고 있습니다.)
만약 Panel이 바탕색, 선 색깔 등등 많은 정보를 가지고 있다면, ReversibleCommand의 구현체들도 Panel이 가지는 속성의 숫자만큼 많아질 것이며, 그들이 각각 가지고 있는 정보도 딱 자기한테 적합한 정보만 가지면 될 겁니다. (배경색을 바꾸는 Command가 선색깔에 대한 정보를 가질 필요는 없으며, 현재 배경색이 무엇인지 정보만 가지면 됩니다.) 그리고 그 정보는 현재 상태에서 뽑아내면 됩니다.

일반적으로 CommandStack을 구현할 때는 Boundary condition을 체크해야 합니다. 명령이 10개 뿐인데 redo를 11번 하면 에러가 날 수 있고, 0번째에 있을 때 또 undo를 실행시킬 수도 있기 때문에 반드시 경계 조건을 체크해야합니다. 여기서는 코드의 간결성을 위해서 고려하지 않았습니다. 실제 프로그램 짤 때는 꼭!! 고려하셔야 합니다.