커맨드패턴은 객체의 행위(메서드)를 클래스로 만들어 캡슐화하는 패턴이다.
어떤 객체(A)에서 다른 객체(B)의 메서드를 실행하려면 객체(B)를 참조하고 있어야 하는 의존성이 발생한다. 이와 같은 상황에서 커맨드패턴을 적용하면 의존성을 제거할 수 있다. 또한 기능이 수정되거나 변경이 일어날 때 A클래스의 코드를 수정없이 기능에 대한 클래스를 정의하면 되므로 시스템이 확장성이 있으면서 유연성을 가질 수 있다.
인공지능 스피커를 예로 들어보자!
인공지능 스피커를 사용하는 사용자를 Client클래스, 인공지능스피커를 AISpeaker클래스, 조명을 Light클래스로 정의하겠다.
가장 먼저, AISpeaker는 조명을 켜기 위해서 Light객체를 참조해야한다. 이를 코드로 표현해보자!
// Light.java
public class Light {
public void lightOn() {
System.out.println("Light on");
}
}
// AISpeaker.java
public class AISpeaker {
private Light light;
public AISpeaker(Light light) {
this.light = light;
}
public void talk() {
light.lightOn();
}
}
// Client.java
public class Client {
public static void main(String args[]){
Light light = new Light();
AISpeaker aiSpeaker = new AISpeaker(light);
aiSpeaker.talk();
}
}
해당 코드를 실행하면 Light on의 문구가 출력될 것이다.
그런데 불을 켜는 기능 말고, 히터를 트는 기능을 추가하고 싶으면 어떻게 해야할까?
위와 같이 Heater클래스를 정의하고, AISpeaker클래스에서 Heater객체를 참조하도록 해야한다. 이것을 표현하면 아래의 코드로 나타낼 수 있다.
// Light.java
public class Light {
public void lightOn() {
System.out.println("Light on");
}
}
// Heater.java
public class Heater {
public void heaterOn() {
System.out.println("Heater on");
}
}
// AISpeaker.java
public class AISpeaker {
private static String[] modes = {"heater", "light"};
private Light light;
private Heater heater;
private String mode;
public AISpeaker(Light light, Heater heater) {
this.light = light;
this.heater = heater;
}
public void setMode(int index) {
this.mode = modes[index];
}
public void talk() {
switch(this.mode) {
case "heater":
this.heater.heaterOn();
break;
case "light":
this.light.lightOn();
break;
}
}
}
// Client.java
public class Client {
public static void main(String args[]){
Light light = new Light();
Heater heater = new Heater();
AISpeaker aiSpeaker = new AISpeaker(light, heater);
aiSpeaker.setMode(0); // heater
aiSpeaker.talk();
aiSpeaker.setMode(1); //light
aiSpeaker.talk();
}
}
위와 같은 방법은 기능을 추가할수록 객체 프로퍼티는 더욱 늘어날 것이고, talk메서드의 분기 또한 늘어날 것이다.
위 문제점을 해결하기 위해 커맨드패턴을 적용해보자!
가장 먼저 AISpeaker가 할 수 있는 기능을 클래스로 만들어 캡슐화한다. 그리고 AISpeaker클래스의 talk메서드에서 기능을 호출하지 않고, 캡슐화한 Command인터페이스의 메서드를 호출하도록 하자!
가장 먼저 Command인터페이스를 정의하자!
// Command.java
public interface Command {
public void run();
}
다음으로, Heater를 켜는 명령을 클래스화 하여 HeaterOnCommand클래스에 정의하고 Heater클래스는 히터를 켜는 heaterOn메서드를 정의한다.
// HeaterOnCommand.java
public class HeaterOnCommand implements Command {
private Heater heater;
public HeaterOnCommand(Heater heater) {
this.heater = heater;
}
@Override
public void run() {
heater.heaterOn();
}
}
// Heater.java
public class Heater {
public void heaterOn() {
System.out.println("Heater on");
}
}
마찬가지로, Light를 켜는 명령을 클래스화하여, LightOnCommand클래스를 정의하고 Light클래스는 그래도 불을 켜는 lightOn메서드를 정의한다.
// LightOnCommand.java
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void run() {
light.lightOn();
}
}
// Light.java
public class Light {
public void lightOn() {
System.out.println("Light on");
}
}
AISpeaker클래스의 talk메서드는 Command인터페이스의 run메서드를 호출하여 명령을 실행하도록 한다.
// AISpeaker.java
public class AISpeaker {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void talk() {
command.run();
}
}
마지막으로, AISpeaker를 사용하는 Client클래스를 정의하자!
// Client.java
public class Client {
public static void main(String args[]){
Light light = new Light();
Heater heater = new Heater();
Command heaterOnCommand = new HeaterOnCommand(heater);
Command lightOnCommnad = new LightOnCommand(light);
AISpeaker aiSpeaker = new AISpeaker();
aiSpeaker.setCommand(heaterOnCommand);
aiSpeaker.talk();
aiSpeaker.setCommand(lightOnCommnad);
aiSpeaker.talk();
}
}
여기에 새로운 기능을 추가할 경우 해당 클래스를 추가하면 되기 때문에 OCP에 위배되지 않으면서 기능을 추가할 수 있다.
커맨드패턴 학습 시 참고 블로그: https://victorydntmd.tistory.com/295?category=719467