Notice
Recent Posts
Recent Comments
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Archives
Today
Total
관리 메뉴

DeFacto-Standard IT

Command Pattern 본문

Design Pattern/Behavioral Pattern

Command Pattern

defacto standard 2017. 9. 26. 10:44

1. 가정

 - TV리모컨을 만든다.

 - 버튼을 클릭 시 전원이 켜진다

2. Naive Code

  - NaivePowerSupply

public class NaivePowerModule {


public void turnOn(){
System.out.println("TV ON");
}

}

실제로 TV를 켜고 끄는 TV의 모듈이다.


 - NaiveButton

public class NaiveButton {
private NaivePowerModule naivePowerModule;

public NaiveButton(NaivePowerModule naivePowerModule) {
this.naivePowerModule = naivePowerModule;
}

public void press() {
naivePowerModule.turnOn();
}

}

버튼에 해당한다. 버튼을 누르면(press()) 연관된 PowerModule의 기능(turnOn())을 수행한다.


 - NaiveClient

public class NaiveClient {
public static void main(String args[]) {

NaivePowerModule naivePowerModule = new NaivePowerModule();
NaiveButton naiveButton = new NaiveButton(naivePowerModule);

naiveButton.press();

}
}


3. 문제점


위 코드에는 2가지 문제점이 존재한다.


다음과 같은 요구사항이 추가됐다고 생각해보자.


1. 현재 수행하는 기능이 아닌, 다른 기능을 수행하게 할 때

   - 전원을 켜는 동작이 아니라, 채널을 변경하는 동작을 하도록 변경

   - 또는 전원을 켜는 동작이 아니라, 전원을 끄는 동작을 하도록 변경


현재 코드는 Button에서 PowerModule에 대한 레퍼런스가 있고,

버튼을 누르면(Button# press()) PowerModule의 레퍼런스를 통해 전원을 켜는 코드(PowerModule# turnOn())가 실행된다.


2가지 예를 든 이유는 다음과 같다.

1) 채널을 변경하도록 Button의 동작을 변경

이 경우는, 현재 버튼의 레퍼런스가 Navie Code와 다른 클래스를 추가하는 경우이다.

PowerModule에 채널변경과 관련된 메서드를 선언하는 것은 SRP에 어긋난다. (단순히 네이밍만으로 판단한 것이다)


따라서, ChannelModule 과 같은 클래스를 선언하고, 메서드를 선언해야한다.

이 메서드명은 channelUp()이라고 하겠다.


그 다음은 버튼을 눌렀을 때(press()) ChannelModule의 channelUp()을 호출하도록 Button 클래스를 수정하여야 한다.



2) 전원을 끄도록 Button의 동작을 변경


이 경우는, 현재 버튼의 레퍼런스가 Navie Code와 동일한 PowerModule의 메서드를 추가하는 경우이다.

SRP에 의해 PowerModule는 전원을 켜는, 끄는 메서드 2가지를 정의하는 것은 크게 문제되지 않을 것이다.


그리고 전원을 끄는 turnOff()메서드를 정의했다고 치자.


하지만, 결국 NaiveButton의 press() 메서드 내부의 내용은

naivePowerModule.turnOn()에서

naivePowerModule.turnOff()로 바뀌어야 한다.


또한 새로운 기능(ChannelModule# channelUp()) 을 추가하기 위해 기존의 소스코드(Button# press())가 수정되어야 하므로, OCP에 위배된다.




또 다른 추가적인 요구사항의 발생에 대한 문제는 다음과 같다.


2. 버튼을 누를 때 마다 다르게 동작하게 할 때

   - 처음 누를 때는 TV의 전원을 켜고, 두번째 누를 때는 자신이 원하는 채널로 변경, 세번째 누를 때는 TV의 전원을 끄도록 변경


1번에서 말했던, TV의 전원을 끄는 로직을 그대로 가지고 있다고 해보자.

Button을 실행시킬 수 있는 메서드는, 생성자를 제외하고 press()단 하나밖에 없다.


버튼에 press()말고 다른 public메서드를 추가한다는 것은 누르는 것 말고 다른 기능이 존재한다는 말이 된다.

버튼에는 누르기 말고는 존재하지 않으므로 public메서드를 추가하는 적절하지 않다.


그렇다면 기존의 press() 메서드의 내용을 변경해야한다.


첫 번째, 두 번째, 세 번째 누를 때마다 기능이 변경된 것을 작동해야하므로

if-else if-else 문 내지 switch문을 사용할 것이다.


이는 의도한대로는 작동하지만, n 번째 기능이 추가된다면 기존의 소스코드를 n번 변경하여야 하므로 OCP에 위배된다.



4. 해결방안

현재 문제가 되는 것은 Button 클래스의 변경이다.

Button 클래스는 커맨드 패턴에서 Invoker의 역할에 해당한다. Invoker는 변경이 되면 안된다.

하지만 기능을 추가했을 때 Invoker는 변경이 되면 OCP에 위배된다.


이를 해결할 수 있는 방법은, 외부에서 특정 객체를 주입받아서 그 객체에게 수행을 위임(Delegation)하는 것이다.


외부에서 어떠한 객체를 넘기냐에 따라서 그 행동이 결정되는 것이지, Invoker내부에서는 그것을 결정하면 안된다는 것이다.

Invoker는 오로지 객체를 받아서, 그 객체의 특정 메서드를 수행시키는 기능만을 제공해야한다.


이 Invoker가 넘겨받는, 특정 동작을 수행할 수 있는 객체를 Command라고한다. 이는 추상화된 개념으로서, 인터페이스로 구성된다.

Invoker를 사용하는 클래스(Client)는 어떤 기능을 수행할 지에 대한 객체(Command)를 만들어서 Invoker에게 알려야한다.


그러기 위해서는 Invoker에 setter()가 필요하다. 


Command의 경우는 추상적인 개념이고 인터페이스라고 하였다.

이 Command의 경우는 각 기능마다 하나씩 할당된다고 생각하면 된다.


가령, 버튼(Invoker)하나에 전원을 켜는 기능, 전원을 끄는 기능, 채널을 변경하는 기능 3가지를 동작할 수 있도록 한다면

이 3가지에 대한 ConcreteCommand 클래스가 각각 구현되어야 한다. 


그리고, 이 ConcreteCommand클래스는 실질적으로 일을 하는 Receiver의 레퍼런스를 가지고 있다.


Receiver의 경우 예제에서는 PowerModule, ChannelModule 등이 해당된다.


즉, Client는 Invoker를 생성할 때 Command와 Receiver를 정의해야한다.


Receiver는 ConcreteCommand의 생성자를 통해 레퍼런스를 넘기는데 이용된다.

Receiver의 레퍼런스를 가진ConcreteCommand 객체는 추상적인 Command 레퍼런스에 할당되고,

최종적으로 Invoker에 추상적인 Command 객체를 생성자 혹은 setter()를 통해 객체를 주입(DI, Dependency Injection)하게 된다.


Invoker 입장에서는 추상적인 객체 Command가 들어오게 되지만 사실은 ConcreteCommand가 들어오기 때문에, 외부에서 어떤 명령을 수행할지 결정해서 넘겨주기에 Invoker는이게 뭐든 관심이 없고 Command 객체이기만 하면 된다.


ConcreteCommand는 Command 인터페이스를 상속받는 클래스로서 메서드를 오버라이딩하여야한다.

이 메서드의 내용은 Receiver의 기능에 해당한다.


말로 설명하면 이해가 안되니 5번에서 소스코드를 살펴보자.



5. Solution Code

 - Receiver1, 2

// PowerModule, 실질적으로 작업을 수행할 개체
public class Receiver1 {
// executed by ConcreteCommand1
public void turnOn() {
System.out.println("TV On");
}

// executed by ConcreteCommand3
public void turnOff() {
System.out.println("TV Off");
}

}
// ChannelModule, 실질적으로 작업을 수행할 개체
public class Receiver2 {
// executed by ConcreteCommand2
public void channelUp() {
System.out.println("Channel Up");
}
}


 실질적으로 작업을 하는 개체이다.

Receiver1의 경우는 예제로 치면 전원 모듈이라서 TV를 켜고 끄는 기능이 존재한다.

Receiver2의 경우는 예제로 치면 채널 모듈이라서 채널을 올리는 기능만 존재한다. 내리는 기능은 고려하지 않았다.



 - Command

// 추상적인 개념의 명령
public interface Command {
//executed by Invoker
public void execute();
}

 추상적인 개념의 명령이다. Invoker의 입장에서는 이 Command# execute()를 실행함으로서 ConcreteCommand간의 결합도를 줄인다.


 - ConcreteCommand 1~3

// Receiver1을 통해 특정 기능(turnOn()) 1개만 수행하는 ConcreteCommand
public class ConcreteCommand1 implements Command {
private Receiver1 receiver1;

// Dependency Injected by Client
public ConcreteCommand1(Receiver1 receiver1) {
this.receiver1 = receiver1;
}

public void execute() {
receiver1.turnOn(); // Delegation
}
}
// Receiver2를 통해 특정 기능(channelUp()) 1개만 수행하는 ConcreteCommand
public class ConcreteCommand2 implements Command {
private Receiver2 receiver2;

// Dependency Injected by Client
public ConcreteCommand2(Receiver2 receiver2) {
this.receiver2 = receiver2;
}

public void execute() {
receiver2.channelUp(); // Delegation
}
}
// Receiver1을 통해 특정 기능(turnOff()) 1개만 수행하는 ConcreteCommand
public class ConcreteCommand3 implements Command {
private Receiver1 receiver1;

// Dependency Injected by Client
public ConcreteCommand3(Receiver1 receiver1) {
this.receiver1 = receiver1;
}

public void execute() {
receiver1.turnOff(); // Delegation
}
}

특정 명령어에 해당한다. Command 인터페이스를 상속받아 Invoker에 의해 수행된다.

중요한 것은, ConcreteCommand 1개당 단 하나의 기능만을 담당한다.

내부적으로 Receiver에 대한 레퍼런스가 있고, execute()메서드는 receiver에 대한 기능 1개만을 호출한다.


 - Invoker

// Button
public class Invoker {
private Command command;

// Dependency Injected by Client
public void setCommand(Command command) {
this.command = command;
}

// executed by Client
public void pressed() {
command.execute(); // Delegation
}
}

예제의 버튼에 해당하는 Invoker이다. 외부(Client)로부터 추상적인 명령인 Command객체를 넘겨받아 세팅하고 외부(Client)에서 실행(pressed())을 하면 구체적인 명령이 무엇이든 상관 없이 현재 레퍼런스가 가리키고 있는 명령(Command)을 수행한다.


 - Client

public class Client {
public static void main(String[] args) {

// Invoker(Button) 정의
Invoker invoker = new Invoker();

// 실질적으로 작업을 하는 개체 정의
Receiver1 receiver1 = new Receiver1();
Receiver2 receiver2 = new Receiver2();

// 추상적인 객체(Command)로 수행시킬 구체적인 명령(ConcreteCommand1~3)을 캡슐화하여 정의
Command command1 = new ConcreteCommand1(receiver1); // 명령1
Command command2 = new ConcreteCommand2(receiver2); // 명령2
Command command3 = new ConcreteCommand3(receiver1); // 명령3

invoker.setCommand(command1);
invoker.pressed(); // 첫 번째 클릭 - 명령 1 - TV ON

invoker.setCommand(command2);
invoker.pressed(); // 두 번째 클릭 - 명령 2 - Channel UP

invoker.setCommand(command3);
invoker.pressed(); // 세 번째 클릭 - 명령 3 - TV OFF
}
}

실질적인 작업을 수행하는 Receiver는 기능 1개를 담당하는 ConcreteCommand의 생성자에 넘겨진다.

ConcreteCommand1, 3의 경우에는 Receiver1의 기능을 위임을 통해 사용한다.

ConcreteCommand2의 경우에는 Receiver2의 기능을 위임을 통해 사용한다.


여기서 ChannelModuel에 해당하는 Receiver2에 channelDown()을 추가하고, 3번째 클릭 시에는 채널 다운, 4번째 클릭 시에는 전원 종료를 시키도록 요구사항이 변경된다면,


1. Receiver2에 channelDown()메서드 구현

2. ConcreteCommand4 정의

3. Client 수정(비즈니스 로직 수정)


만 하면 된다. 위에서 Invoker의 소스코드 수정은 필요가 없기 때문에, 기능 추가에 대해 기존 소스코드의 변경이 발생하지 않아 OCP를 만족하는 설계가 된다.


6. Command Pattern


실행될 구체적인 기능(ConcreteCommand)을 캡슐화(Command)함으로써 기능 호출자(Invoker) 클래스와 실제 기능을 실행하는 수신자(Receiver) 클래스 사이의 의존성을 제거한다. 따라서 실행될 기능의 변경에도 Invoker를 수정 없이 그대로 사용할 수 있다.


- UML & Seq Diagram




 - Command : 실행될 기능에 대한 인터페이스, 실행될 기능을 execute 메서드로 선언함

 - ConcreteCommand : 실제로 실행되는 기능을 구현. 즉, Command라는 인터페이스를 구현함

 - Invoker : 기능의 실행을 요청하는 호출자 클래스

 - Receiver : ConcreteCommand에서 execute 메서드를 구현할 때 필요한 클래스. 즉, ConcreteCommand의 기능을 실행하기 위해 사용하는 수신자 클래스


7. 장단점

장점 : 

단점 : 

'Design Pattern > Behavioral Pattern' 카테고리의 다른 글

Template Method Pattern (기본)  (0) 2017.09.26
Observer Pattern  (0) 2017.09.26
State Pattern과 Strategy Pattern의 공통점과 차이점  (0) 2017.09.26
State Pattern  (0) 2017.09.26
Strategy Pattern  (0) 2017.09.26
Comments