[디자인패턴] 커맨드 패턴 (Command Pattern)

koline·2023년 9월 7일
0

디자인패턴

목록 보기
15/24

커맨드 패턴


실행될 기능을 캡슐화함으로써 주어진 여러 기능을 실행할 수 있는 재사용성이 높은 클래스를 설계하는 패턴으로 하나의 추상 클래스에 메서드를 만들어 각 명령이 들어오면 그에 맞는 서브 클래스가 선택되는 특징을 갖는 패턴이다.

커맨드 패턴을 사용하면 어떤 객체(A)에서 다른 객체(B)의 메서드를 실행하려고 할 때 의존성을 제거할 수 있다. 또한 기능이 수정되거나 변경이 일어날 때 A클래스의 코드를 수정없이 기능에 대한 클래스를 정의하면 되므로 시스템이 확장성이 있으면서 유연성을 가질 수 있다.



구조


  1. Command: 실행될 기능에 대한 인터페이스
    실행될 기능을 execute 메서드로 선언
  2. ConcreteCommand: 실제로 실행되는 기능을 구현
    Command 인터페이스를 구현
  3. Invoker: 기능의 실행을 요청하는 호출자 클래스
    Command 객체를 캡슐화하며 요청을 처리하기 위해 커맨드 객체에 요청을 전달한다.
  4. Receiver: ConcreteCommand에서 execute 메서드를 구현할 때 필요한 클래스
    ConcreteCommand의 기능을 실행하기 위해 사용하는 수신자 클래스



구현


아래 예제에서 AISpeaker는 IoT 기능이 탑재된 스마트홈 제품이다.

커맨드 패턴 적용 전

// AISpeaker.java
public class AISpeaker {
    private Light light;

    public AISpeaker(Light light) {
        this.light = light;
    }

    public void talk() {
        light.turnLightOn();
    }
}

// Light.java
public class Light {
    public void turnLightOn() {
        System.out.println("[Light] turning light on...");
    }
}

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

        AISpeaker speaker = new AISpeaker(new Light());
        speaker.talk();
    }
}

// 실행 결과
[Light] turning light on...

AISpeaker는 Light를 속성으로 가지며 Light 객체의 turnLightOn() 메소드를 사용해 집의 전등을 켜는 동작을 한다. 만약 AISpeaker에 에어컨을 켜는 기능을 추가하려면 어떻게 해야 할까?

// AirConditioner.java
public class AirConditioner {
    public void turnAirConditionerOn() {
        System.out.println("[AirConditioner] turning air conditioner on...");
    }
}

// AISpeaker.java
public class AISpeaker {
    private Light light;
    private AirConditioner airConditioner;
    private String mode = "";

    public AISpeaker(Light light, AirConditioner airConditioner) {
        this.light = light;
        this.airConditioner = airConditioner;
    }

    public void setMode(String mode) {
        this.mode = mode;
    }

    public void talk() {
        switch(mode) {
            case "light" :
                light.turnLightOn();
                break;
            case "airConditioner" :
                airConditioner.turnAirConditionerOn();
                break;
            case "" :
                System.out.println("[AISpeaker] Please set mode before operating...");
                break;
            default :
                System.out.println("[AISpeaker] Please set mode properly...");
                break;
        }
    }
}

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

        AISpeaker speaker = new AISpeaker(new Light(), new AirConditioner());
        speaker.talk();

        speaker.setMode("test");
        speaker.talk();

        speaker.setMode("airConditioner");
        speaker.talk();
    }
}

// 실행 결과
[AISpeaker] Please set mode before operating...
[AISpeaker] Please set mode properly...
[AirConditioner] turning air conditioner on...

우선 AirConditioner 클래스를 생성하고, AISpeaker 클래스에 AirConditioner 속성을 추가해준 다음, 여러개의 동작 중 하나를 선택하기 위해 mode 속성을 추가해주고 talk() 메소드에 switch-case문을 사용해 모드에 맞는 동작을 하게끔 수정했다.

이와 같은 방식은 기능이 추가될 때마다 번거로운 수정 작업을 거쳐야 하고, 기능이 많아질 수록 talk메서드의 분기 또한 늘어날 것이다.

커맨드 패턴 적용 후

// Command.java (Command)
public interface Command {
    public void run();
}

// AirConditionerOnCommand.java (ConcreteCommand)
public class AirConditionerOnCommand implements Command {
    private AirConditioner airConditioner;

    public AirConditionerOnCommand(AirConditioner airConditioner) {
        this.airConditioner = airConditioner;
    }

    @Override
    public void run() {
        airConditioner.turnAirConditionerOn();
    }
}

// LightOnCommand.java (ConcreteCommand)
public class LightOnCommand implements Command {
    private Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }
    
    @Override
    public void run() {
        light.turnLightOn();
    }
}

// AirConditioner.java (Receiver)
public class AirConditioner {
    public void turnAirConditionerOn() {
        System.out.println("[AirConditioner] turning air conditioner on...");
    }
}

// Light.java (Receiver)
public class Light {
    public void turnLightOn() {
        System.out.println("[Light] turning light on...");
    }
}

// AISpeaker.java (Invoker)
public class AISpeaker {
    private Command command;

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

    public void talk() {
        command.run();
    }
}

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

        AISpeaker speaker = new AISpeaker();

        speaker.setCommand(new LightOnCommand(new Light()));
        speaker.talk();

        speaker.setCommand(new AirConditionerOnCommand(new  AirConditioner()));
        speaker.talk();
    }
}

// 실행 결과
[Light] turning light on...
[AirConditioner] turning air conditioner on...

이렇게 구현할 경우 기능이 추가되면 추가된 기능을 Command를 상속하는 클래스로 만들고 장치가 늘어날 경우 장치 클래스를 만들고 해당 장치의 기능을 실행하는 Command 클래스를 만드는 식으로 기존 코드의 수정없이 확장을 할수 있다. (객체지향 설계 원칙 - OCP)



목적

  1. 이벤트가 발생했을때 실행될 기능이 다양하면서도 변경이 필요한 경우
  2. 커맨드 발생 시점을 사용자가 커스터마이징해야 할 때
  3. 여러 커맨드를 조합하여 하나의 커맨드처럼 사용할 필요가 있을 때
  4. 커맨드 실행 취소, 재실행 등의 기능을 구현해야 할 때

장점

  1. 작업을 수행하는 객체와 작업을 요청하는 객체를 분리하기 때문에 SRP 원칙을 잘 지킨다
  2. 기존 코드 수정 없이 새로운 리시버와 새로운 커맨드 추가가 가능하기 때문에 OCP 원칙을 잘 지킨다
  3. 커맨드 단위의 별도의 액션 등이 가능하고 커맨드 상속 및 조합을 통해 더 정교한 커맨드를 구현할 수 있다

단점

  1. 전체적으로 이해가 필요하고 복잡한 설계구조를 가진다



참고


[디자인패턴] 디자인패턴이란? - 생성패턴, 구조패턴, 행위패턴

[디자인패턴] 커맨드패턴(Command Pattern)

Command 패턴

profile
개발공부를해보자

0개의 댓글