카테고리 없음

[헤드퍼스트 디자인패턴] 6. 커맨드 패턴

혀내 2022. 8. 9. 18:03
반응형

커맨드 패턴(Command Pattern)

커맨드 패턴은 외부에서 들어오는 요청과 요청과 관련된 정보를 독립적으로 실행할 수 있는 객체로 매개변수화합니다.

기능을 실행하는 역할인 인보커(Invoker)는 리시버(Receiver)의 요청 사항을 캡슐화한 커맨드 객체의 execute() 메소드만 호출하면 됩니다.

  • 인보커(Invoker)는 리시버가 어떤 일을 하는지 몰라도 됩니다.
  • 인보커가 실행해야 하는 기능이 변경되더라도 인보커의 코드를 수정할 필요 없이 인보커에 주입된 커맨드 객체만 변경하면 됩니다.

 

커맨드 패턴을 사용하면 요청을 대기열 큐에 넣어 지연시키거나, 로그로 기록하거나, 실행을 취소(Undo)할 수 있습니다.
 
 

 

인보커에서 커맨드 객체를 로딩하는  과정

1. 클라이언트(리시버)에서 요청 사항에 맞는 커맨드 객체를 생성합니다.
2. 인보커 객체의 setCommand()를 호출해서 인보커 객체에 커맨드 객체를 저장합니다.
3. 나중에 클라이언트(리시버)에서 인보커에게 그 커맨드 객체를 실행하라고 요청합니다.
 
 


 

직접 구현해보자!

 
 책에서는 예제로 리모컨 각 버튼 슬롯을 누를 때 전자기기를 켜고 끌 수 있는 기능을 연결하는 프로그램을 구현하고 있습니다.

제시 상황
전자기기를 켜고 끌 수 있는 기능이 담긴 클래스들은 하나의 인터페이스 구현체가 아닌 각기 모두 다른 클래스로 구현되어 있으며 심지어 메소드명까지 다릅니다. (조명을 켜는 Light 클래스는 on(), off()로 기능이 구현되어 있지만 보일러를 켜는 Boiler 클래스는 turnOn(), turnOff() 로 구현되어 있다고 해요.)

Light, Boiler처럼 전자기기를 다루는 클래스는 이후에도 계속 추가될 예정입니다. 각 버튼 슬롯을 눌렀을 때 연결될 전자기기도 계속해서 바뀔 가능성이 높다고 합니다.
 
 이 리모컨은 커맨드 패턴에 가장 적합한 예제입니다. 리모컨의 각 버튼 슬롯마다 커맨드 객체를 주입한 다음 버튼을 누를 때마다 커맨드 객체의 execute()를 실행하면 되기 때문입니다. 새로운 전자기기 클래스가 생기면 해당 전자기기의 커맨드 객체만 생성해 확장할 수 있고, 버튼 슬롯에 다른 전자기기를 연결하고 싶다면 그 버튼 슬롯에 다른 커맨드 객체를 주입하면 됩니다.
 
 
 

커맨드 인터페이스 구현

public interface Command {
    public void execute();
}

 
 

조명을 켤 때 필요한 커맨드 클래스 구현

public class LightOnCommand implements Command {
    Light light;
    
    public LightOnCommand(Light light) {
        this.light = light;
    }
    
    public void execute() {
        light.on();
    }
}

각 전자기기마다 커맨드 구현체를 생성합니다. 커맨드 구현 객체에서는  각 전자기기를 다루는 클래스 변수를 생성하고, execute() 메소드에서 그 클래스의 메소드를 실행합니다.
 
 

리모컨에서 커맨드 객체 사용하기

public class SimpleRemoteControl {
    Command slot;
    public SimpeRemoteControl() {}
    
    public void setCommand(Command command) {
        slot = command;
    }
    
    public void buttonWasPressed() {
        slot.execute();
    }
}

 SimpleRemoteControl은 리모컨의 버튼 슬롯을 눌렀을 때 어떤 전자기기의 전원을 켤 것인지 연결하는 프로그램입니다. slot이라는 Command 변수를 만들고 setCommand() 메소드로 커맨드 객체를 주입할 수 있도록 합니다. 버튼을 누르면 주입받은 커맨드 객체의 execute() 메소드를 실행합니다. 이렇게 리모컨은 주입받은 커맨드 객체가 무엇인지 모르지만 execute() 메소드를 통해 기능을 실행할 수 있게 되었습니다!
 
 
 

리모컨 버튼 슬롯에 조명 연결하기

SimpleRemoteControl remote = new SimpleRemoteControl();
Light light = new Light();
LightOnCommand lightOn = new LightOnCommand(light);

remote.setCommand(lightOn);
remote.buttonWasPressed();

 실제로 리모컨의 버튼을 눌렀을 때 조명을 켜게 만들고 싶다면 이렇게 코드를 짜면 됩니다.
 
 
 

public void onButtonWasPushed(int slot) {
    if (onCommands[slot] != null) {
        onCommands[slot].execute();
    }
}

 마지막으로 책에서는 execute() 메소드를 실행하기 전 객체가 null인지 확인하는 반복적인 구문을 줄이기 위해 NoCommand 클래스를 제안했는데요.
 

public class NoCommand implements Command {
    public void execute() {}
}

 NoCommand 클래스는 아무 일도 수행하지 않는 일종의 널 객체(null object)로 null 대신 사용할 수 있는 커맨드 객체입니다. Command 변수에 NoCommand 객체를 넣어준다면 변수가 null인지 확인할 필요 없이 execute() 구문을 바로 실행할 수 있다는 장점이 있습니다. 이렇게 널 객체를 도입하는 것은 리팩토링 기법 중 하나로 사용되고 있습니다.
 
 
 
 


 

작업 취소(Undo) 기능 구현하기

 커맨드 패턴은 가장 큰 장점은 Undo 기능을 구현할 수 있다는 것입니다! 리모컨에 작업 취소 기능을 한 번 추가해봅시다.
 
 

Command 인터페이스에 undo() 메소드 추가하기

public interface Command {
    public void execute();
    public void undo();
}

모든 커맨드에 대해 작업 취소 기능을 수행할 undo() 메소드를 추가합니다.
 

구현체에 undo() 메소드 추가하기

public class LightOnCommand implements Command {
    Light light;
    
    public LightOnCommand(Light light) {
        this.light = light;
    }
    
    public void execute() {
        light.on();
    }
    
    public void undo() {
        light.off();
    }
}

각 커맨드 구현체마다 undo() 메소드를 호출했을 때 수행할 기능에 대해 구현합니다.
 
 

리모컨에도 UNDO 버튼용 커맨드 객체 등록하기

public class SimpleRemoteControl {
    Command slot;
    Command undoSlot;
    
    public SimpeRemoteControl() {
    	Command noCommand = new NoCommand();
        slot = noCommand;
    	undoSlot = noCommand;
    }
    
    public void setCommand(Command command) {
        slot = command;
    }
    
    // 여러개의 Command 객체를 사용할 경우를 대비해 버튼을 누를 때마다 undo용 커맨드 객체를 바꿔준다.
    public void buttonWasPressed() {
        slot.execute();
        undoSlot = slot;
    }
    
    public void undoButtonWasPressed() {
        undoSlot.undo();
    }
}

리모컨에서 작업 취소 버튼을 눌렀을 때 실행할 Command 변수를 하나 추가합니다. 불 켜기 버튼을 누를 때마다 LightOnCommand 객체를 undo용 Command 레퍼런스에 저장하면 작업 취소 기능을 쉽게 구현할 수 있습니다.
 
 

Q) 만약 UNDO 버튼을 여러 번 누를 수 있도록 하려면 어떻게 해야 할까?

 단일 작업 취소가 아닌 연속적인 여러 개의 작업을 취소하고 싶을 때에는 이전에 실행했던 커맨드들을 undo용 Command 레퍼런스에 저장하지 않고 스택에 저장하도록 합니다. 사용자가 UNDO 버튼을 누를 때마다 스택 맨 위의 항목을 꺼내 undo() 메소드를 호출하면 여러 번 작업 취소가 가능해집니다:)
 
 
 
 
 

커맨드 패턴을 활용한다면,

큐에 요청을 저장하는 방식으로 스케줄러, 스레드 풀, 작업 큐 등을 구현할 수 있습니다.

실행 방식은 다음과 같습니다.
1. 커맨드 인터페이스의 구현 객체를 큐에 추가합니다.
2. 스레드는 큐에서 커맨드 구현 객체를 하나씩 꺼내 execute() 메소드를 호출합니다.
 
 
 

로그를 기록할 수 있습니다.

 로그 기록은 명령들을 실행하면서 히스토리를 기록하고, 애플리케이션이 다운되면 커맨드 객체를 다시 로딩해 execute() 메소드를 자동으로 순서대로 실행하는 방식으로 작동합니다.
 
 
 
참조:

 

헤드 퍼스트 디자인 패턴(개정판)

유지관리가 편리한 객체지향 소프트웨어 만들기! “『헤드 퍼스트 디자인 패턴(개정판)』 한 권이면 충분합니다!”

m.hanbit.co.kr

 

반응형