디자인 패턴(구)/행위 패턴

커맨트(Command) 패턴이란?

공대키메라 2022. 4. 17. 20:13

1. 커맨트(Command) 패턴이란?

의도

요청 자체를 캡슐화하는 것. 이를 통해 요청이 서로 다른 사용자를 매개변수로 만들고, 요청을 대기시키거나 로딩하며, 되돌릴 수 있는 연상을 지원.

사용 시기

  • 수행할 동작을 객체로 매개변수화하고자 할 때
  • 서로 다른 시간에 요청을 명시하고, 저장하며, 실행하고 싶을 때
  • 실행 취소 기능을 지원하고 싶을 때
  • 시스템이 고장났을 때 재적용이 가능하도록 변경 과정에 대한 로깅을 지원하고 싶을 때
  • 기본적인 연산의 조합으로 만든 상위 수준 연산을 써서 시스템을 구조화하고 싶을 때

구조

  • Command : 연산 수행에 필요한 인터페이스 선언
  • ConcreteCommand : Receiver객체와 액션 간의 연결성을 정의, 처리 객체에 정의된 연산을 호출하도록  Execute를 구현
  • Client : ConcreteCommand 객체를 생성하고 처리 객체로 정의
  • Invoker : 명령어에 처리를 수행할 것을 요청
  • Receiver : 요청에 관련된 영산 수행 방법을 알고 있음. 어떤 클래스도 요청 수신자로서 동작 할 수 있음.

2. 구현

구현(Implementation) 동화

필자 공대키메라는 수 많은 가전제품을 사용한다.

선풍기, 컴퓨터, TV, 가스레인지 등 많은 작동 스위치가 존재한다. 

각각을 한번 코드로 사용하고 싶다. 

우선은 선풍기부터 사용을 해볼까?

Before

Fan.java

public class Fan {

    private boolean isOn;

    public void on() {
        System.out.println("선풍기를 켭니다.");
        this.isOn = true;
    }

    public void off() {
        System.out.println("선풍기를 끕니다.");
        this.isOn = false;
    }

    public boolean isOn() {
        return this.isOn;
    }
}

FanSwitch.java

public class FanSwitch {

    private Fan fan;

    public FanSwitch(Fan fan) {
        this.fan = fan;
    }

    public void press() {
        fan.off();
    }
}

TestMain.java

package me.whiteship.designpatterns._03_behavioral_patterns._14_command._my_code_before;

public class TestMain {

    public static void main(String[] args) {
        Fan fan = new Fan();
        FanSwitch fanSwitch = new FanSwitch(fan);
        fanSwitch.press();
        fanSwitch.press();
        fanSwitch.press();
        /*
        결과
        
        선풍기를 끕니다.
        선풍기를 끕니다.
        선풍기를 끕니다.
        */
    }

}

 

이번에는 컴퓨터를 키고 끄도록 할 것이다. 

 

Computer.java

public class Computer {

    private boolean isOn;

    public void on() {
        System.out.println("컴퓨터를 켭니다.");
        this.isOn = true;
    }

    public void off() {
        System.out.println("컴퓨터를 끕니다.");
        this.isOn = false;
    }

    public boolean isOn() {
        return this.isOn;
    }
}

ComputerSwitch.java

public class ComputerSwitch {

    private Computer computer;

    public ComputerSwitch(Computer computer) {
        this.computer = computer;
    }

    public void press() {
        computer.off();
    }
}

TestMain.java - modified

public class TestMain {

    public static void main(String[] args) {

        Fan fan = new Fan();
        FanSwitch fanSwitch = new FanSwitch(fan);
        fanSwitch.press();
        fanSwitch.press();
        fanSwitch.press();

        Computer computer = new Computer();
        ComputerSwitch computerSwitch = new ComputerSwitch(computer);
        computerSwitch.press();
        computerSwitch.press();
        computerSwitch.press();

		/*
        결과
        
        선풍기를 끕니다.
        선풍기를 끕니다.
        선풍기를 끕니다.
        컴퓨터를 끕니다.
        컴퓨터를 끕니다.
        컴퓨터를 끕니다.
        */
    }

}

 

자. 이렇게 작성을 해봤는데 무언가 겹치는코드도 많고,

(행위적으로 스위치의 과정이 겹침)

 

선풍기를 켜야지 끌 필요가 있는데 끄는 작업만 하고 있다.

 

즉, 우리가 이러한 상황에 대해서 제어를 하고자 한다면 직접 코드를 내부적으로 변경을 해야 한다.(OCP 위반)

 

이러한 상황을 Command 패턴을 이용하면 해결이 가능하다!

 

즉, 요청 자체를 캡슐화해서 그 요청 안에 어떤 일을 하고, 어떤 작업을 호출하는지 하는 명령들을 수행하기 위한 모든 것을 Command 인터페이스안으로 캡슐화하는 것이다. 

 

그리고 우리는 단 하나의 메소드만 호출하면 된다. 

 

해결책 

공용 switch를 만들려고 한다.

어떤 스위치를 동작시키는 것은 사실 그렇게 어려운 일이 아니다. 

다만, 직접 눌러줘야 하기에 하나의 remote controller로 제어하고 싶을 뿐이다. 

컴퓨터 전원도, 가스불도, 선풍기 버튼도 사실 스윗치 아닌가?
(엄밀히 따지면 조금씩 다를 수 있으나 넘어가주길 바란다.)

그래서 이러한 공통적인 작업을 제어하려고 한다. 

물론 리모콘은 우리가 어떤 명령을 넣어 주느냐에 따라 같은 작업을 반복할 뿐이지만,

우리에게는 넣어준 명령을 다르게 수행하기에 범용적인 리모콘이 되는 것이다. 

 

그러면 코드를 이에 맞추어 수정하기 전에 다시 한 번 구조를 살펴보자.

Invoker가 Command 와 연관을 맺고 있고, Command의 구현체인 ConcreteCommand를 Receiver과 연관을 맺고 있다.

 

연관을 맺고 있다는 말은 내부적으로 직접 관련 코드를 사용하고 있다고 보면 된다.

코드 구현 - 수정 후

우리가 구현할 코드

Invoker에 해당하는 클래스는 Switch이다.

 

Switch는 Command와 연관을 맺고 있다. 내부적으로 Command를 Field에 선언해서 사용하고 있다. 

 

Command를 구현한 ConcreteCommand는 ComputerOffCommand, ComputerOnCommand, FanOffCommand, FanOnCommand가 있다.

 

Receiver는 각각 Computer, Fan이다.

 

Command.java

public interface Command {
    void execute();
}

Computer.java

public class Computer {

    private boolean isOn;

    public void on() {
        System.out.println("컴퓨터를 켭니다.");
        this.isOn = true;
    }

    public void off() {
        System.out.println("컴퓨터를 끕니다.");
        this.isOn = false;
    }

    public boolean isOn() {
        return this.isOn;
    }
}

ComputerOffCommand.java

public class ComputerOffCommand implements Command{

    private Computer computer;

    public ComputerOffCommand(Computer computer) {
        this.computer = computer;
    }

    @Override
    public void execute() {
        computer.off();
    }
}

ComputerOnCommand.java

public class ComputerOnCommand implements Command{

    private Computer computer;

    public ComputerOnCommand(Computer computer) {
        this.computer = computer;
    }

    @Override
    public void execute() {
        computer.on();
    }
}

Fan.java

public class Fan {

    private boolean isOn;

    public void on() {
        System.out.println("선풍기를 켭니다.");
        this.isOn = true;
    }

    public void off() {
        System.out.println("선풍기를 끕니다.");
        this.isOn = false;
    }

    public boolean isOn() {
        return this.isOn;
    }
}

FanOffCommand.java

public class FanOffCommand implements Command{

    private Fan fan;

    public FanOffCommand(Fan fan) {
        this.fan = fan;
    }

    @Override
    public void execute() {
        fan.off();
    }
}

FanOnCommand.java

public class FanOnCommand implements Command{

    private Fan fan;

    public FanOnCommand(Fan fan) {
        this.fan = fan;
    }

    @Override
    public void execute() {
        fan.on();
    }
}

Switch.java

public class Switch {

    private Command command;

    public Switch(Command command) {
        this.command = command;
    }

    public void press() {
        command.execute();
    }

}

TestMain.java

public class TestMain {

    public static void main(String[] args) {

        //switching 기능을 수행하는 리모콘에 어떤 작업을 할지 command를 넘겨줌.
        Switch aSwitch = new Switch(new ComputerOnCommand(new Computer()));
        aSwitch.press();
        aSwitch.press();
        aSwitch.press();

        Switch aSwitch1 = new Switch(new FanOnCommand(new Fan()));
        aSwitch1.press();
        aSwitch1.press();
        aSwitch1.press();
    }

}

 

여기서 크게 무슨 장점이 있는지는 모르겠지만, 

 

우선 Invoker가 변하지 않는 것이 중요하다. 

 

여기에 만족하지 않고 우리는 Stack을 이용해서 한 번 더 코드를 수정할 수 있다.


Command.java - modified


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

 

Switch.java - modified 

import java.util.Stack;

public class Switch {

    private Stack<Command> commands = new Stack<>();

    public void press(Command command) {
        command.execute();
        commands.push(command);
    }

    public void undo() {
        if (!commands.isEmpty()) {
            Command command = commands.pop();
            command.undo();
        }
    }

}

ComputerOffCommand.java

public class ComputerOffCommand implements Command{

    // 동일 코드
    
    //undo 기능 구현 - 모든 ConcreteCommand 동일하게 반대로 구현
    @Override
    public void undo() {
        new ComputerOnCommand(this.computer).execute();
    }
}

TestMain.java

public class TestMain {

    public static void main(String[] args) {
        //switching 기능을 수행하는 리모콘에 어떤 작업을 할지 command를 넘겨줌.
        Switch aSwitch = new Switch();
        aSwitch.press(new ComputerOnCommand(new Computer()));
        aSwitch.press(new FanOnCommand(new Fan()));
        aSwitch.undo();
        aSwitch.undo();
    }

}

 

장점

  • 기존 코드를 변경하지 않고 새로운 커맨드를 만들 수 있다.
  • 수신자의 코드가 변경되어도 호출자의 코드는 변경되지 않는다.
  • 커맨드 객체를 로깅, DB에 저장, 네트워크로 전송 하는 등 다양한 방법으로 활용할 수도 있다.

장점에 대해 생각해볼 때 Command 인터페이스를 수정하지 않고 새로운 커맨드를 만들어 낸것을 확인했고, Invoker인 switch는 현재 변경되어도 호출자는 변경되지 않음을 확인했다.

단점

  • 코드가 복잡하고 클래스가 많아진다.

이렇게 command패턴에 대해 알아보았다. 무언가 이상하거나 수정하면 좋을 사항이 있다면 피드백 부탁헤엉