커맨드 패턴이란

  • 실행될 기능을 캡슐화함으로써 주어진 여러 기능을 실행할 수 있는 재사용성이 높은 클래스를 설계하는 패턴이다.
    • 즉, 이벤트가 발생했을 때 실행될 기능이 다양하면서도 변경이 필요한 경우에 이벤트를 발생시키는 클래스를 변경하지 않고 재사용하고자 할 때 유용하다.
    • ‘행위(Behavioral) 패턴’ 중 하나이다.
  • 실행될 기능을 캡슐화함으로써 기능의 실행을 요구하는 호출자(위 그림에서 Invoker) 클래스와 실제 기능을 실행하는 수신자(Receiver) 클래스 사이의 의존성을 제거한다.
  • 따라서 실행될 기능의 변경에도 호출자 클래스의 수정 없이 그대로 사용할 수 있도록 해준다.
  • 위 그림에서 각 역할이 수행하는 작업
    • Command
      • 실행될 기능에 대한 인터페이스
      • 실행될 기능을 execute 메소드로 선언한다.
    • ConcreteCommand
      • 실제로 실행되는 기능을 구현한다.
      • 즉, Command 인터페이스를 구현한다.
    • Invoker
      • 기능의 실행을 요청하는 호출자 클래스
    • Receiver
      • ConcreteCommand에서 execute 메소드를 구현할 때 필요한 클래스
      • 즉, ConcreteCommand의 기능을 실행하기 위해 사용하는 수신자 클래스

예시

개념을 어느 정도 봤으니 예시를 살펴보자. 이번에는 만능 버튼을 만들어 볼 예정이다. 버튼이 눌리면 불이 켜지는 프로그램이다.

Lamp, Button 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Lamp 클래스.
public class Lamp {
public void turnOn(){
System.out.println("Turn On!");
}
}
// Button 클래스.
public class Button {
private Lamp lamp;

public Button(Lamp lamp){
this.lamp=lamp;
}

public void buttonPress(){
lamp.turnOn();
}
}

Client 클래스

1
2
3
4
5
6
7
public class CommandClient {
public static void main(String[] args) {
Lamp lamp = new Lamp();
Button button = new Button(lamp);
button.buttonPress();
}
}
  • Lamp 클래스로부터 Lamp 객체를 생성한다.
  • Button 클래스의 생성자를 이용해 불을 켤 Lamp 객체를 전달한다.
  • Button 클래스의 buttonPress()가 호출되면 생성자를 통해 전달받은 Lamp 객체의 turnOn()를 호출해 불을 켠다.

문제점

  1. 버튼을 눌렀을 때 다른 기능을 실행하는 경우
    • 버튼을 눌렀을 때 알람이 시작되게 하려면? 다음과 같이 수정하면 된다.

Alarm, Button 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Alarm 클래스.
public class Alarm {
public void startAlarm(){
System.out.println("Start Alarm ~~~~!!");
}
}
// 수정된 Button 클래스.
public class Button {
private Alarm alarm;

public Button(Alarm alarm) {
this.alarm = alarm;
}

public void buttonPress() {
alarm.startAlarm();
}
}

Clien 클래스

1
2
3
4
5
6
7
public class CommandClient {
public static void main(String[] args) {
Alarm alarm = new Alarm();
Button button = new Button(alarm);
button.buttonPress();
}
}
  • 새로운 기능으로 변경하려고 기존 코드인 Button 클래스의 내용을 수정했다. 이로 인해 OCP 원칙에 위배된다.
  • Button 클래스의 buttonPress() 전체를 변경해야 한다.
  1. 버튼을 누르는 동작에 따라 다른 기능을 실행하는 경우
    • 버튼을 처음 눌렀을 때는 램프를 켜고, 두 번째 눌렀을 때는 알람을 동작하게 하려면?

수정된 Button 클래스

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
public class Button {
private Alarm alarm;
private Lamp lamp;
private Mode mode;

// 생성자에서 버튼을 눌렀을 필요한 기능을 인자로 받는다.
public Button(Alarm alarm, Lamp lamp) {
this.alarm = alarm;
this.lamp = lamp;
}

// 램프 모드 또는 알람 모드를 설정.
public void setMode(Mode mode) {
this.mode = mode;
}

// 설정된 모드에 따라서 램프를 켜거나 알람을 시작한다.
public void buttonPress() {
switch (mode) {
case LAMP:
lamp.turnOn();
break;
case ALARM:
alarm.startAlarm();
break;
}
}
}

필요한 기능을 새로 추가할 때마다 Button 클래스의 코드를 수정해야 한다. 따라서 재사용하기 굉장히 어렵다.

해결책

문제를 해결하기 위해서는 구체적인 기능을 직접 구현하는 대신 실행될 기능을 캡슐화해야 한다.

  • 즉, Button 클래스의 buttonPress() 메소드에서 구체적인 기능(램프 켜기, 알람 동작 등)을 직접 구현하는 대신 버튼을 눌렀을 때 실행될 기능을 Button 클래스 외부에서 제공받아 캡슐화해 buttonPress() 메소드에서 호출한다.
  • 이를 통해서 Button 클래스 코드를 수정하지 않고도 그대로 사용할 수 있다.
  • Button 클래스는 미리 약속된 Command 인터페이스의 execute 메소드를 호출한다.
    • 램프를 켜는 경우에는 theLamp.turnOn() 메소드를 호출하고 알람이 동작하는 경우에는 theAlarm.start() 메소드를 호출하도록 buttonPress() 메소드를 수정한다.
  • LampOnCommand 클래스에서는 Command 인터페이스의 execute 메소드를 구현해 Lamp 클래스의 turnOn() 메소드를 호출한다.(램프 켜는 기능)
  • 마찬가지로 AlarmStartCommand 클래스에서는 Command 인터페이스의 execute 메소드를 구현해 Alarm 클래스의 start() 메소드를 호출한다.(알람이 울리는 기능)

Command 인터페이스

1
2
3
public interface Command {
void execute();
}

Button 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Button {
private Command command;

// 생성자에서 버튼을 눌렀을 때 필요한 기능을 인자로 받는다.
Button(Command command) {
setCommand(command);
}

public void setCommand(Command command) {
this.command = command;
}

// 버튼이 눌리면 주어진 Command 의 execute() 메소드를 호출한다.
public void buttonPress() {
command.execute();
}
}

Lamp, LampOnCommand 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Lamp {
public void turnOn(){
System.out.println("Turn On!");
}
}

// 램프를 켜는 명령을 수행하는 LampOnCommand 클래스.
public class LampOnCommand implements Command {
private Lamp theLamp;

public LampOnCommand(Lamp lamp) {
this.theLamp = lamp;
}

// Command 인터페이스의 execute() 메소드 실행.
@Override
public void execute() {
theLamp.turnOn();
}
}

Alarm, AlarmStartCommand 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Alarm {
public void startAlarm(){
System.out.println("Start Alarm ~~~~!!");
}
}

// 알람을 울리는 명령을 수행하는 AlarmStartCommand 클래스.
public class AlarmStartCommand implements Command {
private Alarm theAlarm;

public AlarmStartCommand(Alarm alarm) {
this.theAlarm = alarm;
}

@Override
public void execute() {
theAlarm.startAlarm();
}
}

Client에서 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CommandClient {
public static void main(String[] args) {
Lamp lamp = new Lamp();
Alarm alarm = new Alarm();

Command lampOnCommand = new LampOnCommand(lamp);
Command alarmStartCommand = new AlarmStartCommand(alarm);

// Lamp 를 켜는 Command 를 설정.
Button button = new Button(lampOnCommand);
button.buttonPress(); // 램프를 켜는 기능 수행.

System.out.println();
// 알람을 울리는 Command 를 설정.
Button button1 = new Button(alarmStartCommand);
button1.buttonPress(); // 알람 울리는 기능 수행.
button1.setCommand(lampOnCommand); // 다시 램프를 켜는 Command 로 설정.
button1.buttonPress(); // 램프를 켜는 기능 수행.
}
}
  • Command 인터페이스를 구현하는 LampOnCommand와 AlarmStartCommand 객체를 Button 객체에 설정한다.(setCommand() 메소드를 통해서)
  • Button 클래스의 buttonPress() 메소드에서 Command 인터페이스의 execute() 메소드를 호출한다.
  • 즉, 버튼을 눌렀을 때 필요한 임의의 기능은 Command 인터페이스를 구현한 클래스의 객체를 Button 객체에 설정해서 실행할 수 있다.
  • 이렇게 Command 패턴을 이용하면 Button 클래스의 코드를 변경하지 않으면서 다양한 동작을 구현할 수 있게 된다.

정리

커맨드 패턴을 적용하지 않은 경우, Button에 많은 기능이 추가될수록 Button 클래스가 가지고 있는 객체 프로퍼티는 더욱 늘어날 것이고 기존의 buttonPress() 메소드에서 분기가 더 늘어날 것이다. 이는 결국 OCP에 위배된다.

커맨드 패턴을 적용하면, Button이 할 수 있는 기능들(램프를 킨다, 알람을 시작한다.)을 클래스로 만든다. 즉, 기능을 수행하도록 명령을 내리는 클래스(LampOnCommand, AlarmStartCommand)로 만들어서 각 기능들을 캡슐화 한다.

그리고 Button 클래스의 buttonPress() 메소드에서 lamp.turnOn(), alarm.start()와 같이 기능들을 직접 호출하지 않고, 캡슐화한 Command 인터페이스의 execute() 메소드를 호출하도록 한다.

만약 버튼에 TV를 틀어주는 기능이 추가된다면 TvOnCommand 클래스와 TV 클래스를 추가하면 되므로, 기존의 코드를 수정할 필요가 없다. 따라서 OCP에 위배되지 않으면서 기능을 추가할 수 있다.

참고