본문 바로가기
Computer Sience/Desgin Pattern

[Design Pattern] 커맨드(Command) 패턴

by 제우제우 2024. 10. 31.

커맨드(Command) 패턴

요청을 객체로 캡슐화하여 호출자(invoker)와 수신자(receiver)를 분리하는 패턴

요청을 처리하는 방법이 바뀌더라도, 호출자의 코드는 변경되지 않는다. 

 

Command 인터페이스

실행할 명령을 추상적으로 정의한다. 

 

ConcreteCommand 클래스 

실제 명령을 구현한 클래스 Command 인터페이스를 구현한다.

수행할 구체적인 행동과 수신자(receiver)를 포함한다. 

 

Receiver (수신자)

실제 명령의 실행 방법을 알고 있는 객체로, ConcreteCommand 클래스에서 수신자 객체를 통해 요청을 수행

 

Invoker (호출자)

명령 객체(Command)를 받아 execute()를 호출하는 역할을 한다.

호출자는 명령의 구체적인 실행 방법에 대해 알 필요 없이, 명령 객체를 호출하여 요청을 전달할 수 있다.

 

Client(클라이언트)

구체적인 명령 객체를 생성하여 호출자에게 전달하는 역할을 수행

커맨드(Command) 패턴 before

구글홈이라고 "OK Google 히터 틀어줘"라고 하면, 히터를 틀어주는 실제 구글 서비스가 있다. 

구글홈을 사용하는 사용자를 Client 클래스 

구글홈을 OKGoogle 클래스 

히터를 Heater 클래스로 정의하겠다. 

 

Heater

public class Heater {
    public void powerOn(){
        System.out.println("Heater On");
    }
    public void powerOff(){
        System.out.println("Heater Off");
    }
}

 

OKGoogle

public class OKGoogle {
    public static enum CONTROL{
        ON, OFF
    }
    private Heater heater;
    public OKGoogle(Heater heater){
        this.heater = heater;
    }
    public void call(CONTROL control){
        if(control.equals(CONTROL.ON)){
            heater.powerOn();
        }
        else heater.powerOff();
    }
}

 

Client

public class Client {
    public static void main(String[] args) {
        Heater heater = new Heater();
        OKGoogle okGoogle = new OKGoogle(heater);

        okGoogle.call(OKGoogle.CONTROL.ON);
        okGoogle.call(OKGoogle.CONTROL.OFF);
    }
}

// 출력
Heater On
Heater Off

 

OKGoogle에서 히터를 기능 말고, 불을 켜는 기능을 추가하고 싶다면 어떻게 해야 할까?

Lamp 클래스를 정의하고, OKGoogle 클래스에서 Lamp 객체를 참조하도록 해야 한다. 

 

Lamp 클래스 

public class Lamp {
    public void turnOn(){
        System.out.println("Lamp On");
    }
    public void turnOff(){
        System.out.println("Lamp Off");
    }
}

 

OKGoogle 클래스 

public class OKGoogle {
    public static enum MODE {
        HEATER, LAMP
    }
    public static enum CONTROL{
        ON, OFF
    }
    private MODE mode;
    private Heater heater;
    private Lamp lamp;
    public OKGoogle(Heater heater, Lamp lamp){
        this.heater = heater;
        this.lamp = lamp;
    }
    public void setMode(MODE mode){
        this.mode = mode;
    }
    public void call(CONTROL control){
        if(Objects.isNull(mode)){
            throw new IllegalStateException("mode is not defined");
        }
        switch (mode){
            case LAMP -> {
                if(CONTROL.ON.equals(control)){
                    lamp.turnOn();
                }
                else lamp.turnOff();
            }
            case HEATER -> {
                if(CONTROL.ON.equals(control)){
                    heater.powerOn();
                }
                else heater.powerOff();
            }
        }
    }
}

 

Client

public class Client {
    public static void main(String[] args) {
        Heater heater = new Heater();
        Lamp lamp = new Lamp();
        OKGoogle okGoogle = new OKGoogle(heater, lamp);

        okGoogle.setMode(OKGoogle.MODE.HEATER);
        okGoogle.call(OKGoogle.CONTROL.ON);
        okGoogle.call(OKGoogle.CONTROL.OFF);

        okGoogle.setMode(OKGoogle.MODE.LAMP);
        okGoogle.call(OKGoogle.CONTROL.ON);
        okGoogle.call(OKGoogle.CONTROL.OFF);
    }
}

// 출력
Heater On
Heater Off
Lamp On
Lamp Off

 

현재 코드의 문제점은 OKGoogle은 Heater, Lamp 객체를 참조로 가진다. 

만약 기능이 많아지만 가지는 객체의 참조는 계속 늘어난다.

또한 enum 타입 또한 늘어나고, call() 메소드에서 분기가 계속 늘어난다. 

이런 경우 OCP에 위배된다. 

커맨드(Command) 패턴 after

Command 인터페이스

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

undo() 메소드 

제일 최근에 했던 command의 동작과 반대되는 행위를 한다. 

ex) heater on → heater off

 

ConcreteCommand - 4가지 Command 구현체 

public class HeaterOnCommand implements Command{
    private Heater heater;
    public HeaterOnCommand(Heater heater) {
        this.heater = heater;
    }
    @Override
    public void execute() {
        heater.powerOn();
    }
    @Override
    public void undo() {
        new HeaterOffCommand(this.heater).execute();
    }
}

public class HeaterOffCommand implements Command{
    private Heater heater;
    public HeaterOffCommand(Heater heater) {
        this.heater = heater;
    }
    @Override
    public void execute() {
        heater.powerOff();
    }
    @Override
    public void undo() {
        new HeaterOnCommand(this.heater).execute();
    }
}

public class LampOnCommand implements Command{
    private Lamp lamp;
    public LampOnCommand(Lamp lamp) {
        this.lamp = lamp;
    }
    @Override
    public void execute() {
       lamp.turnOn();
    }
    @Override
    public void undo() {
        new LampOffCommand(this.lamp).execute();
    }
}

public class LampOffCommand implements Command{
    private Lamp lamp;
    public LampOffCommand(Lamp lamp) {
        this.lamp = lamp;
    }
    @Override
    public void execute() {
        lamp.turnOff();
    }
    @Override
    public void undo() {
        new LampOnCommand(this.lamp).execute();
    }
}

 

undo() 메소드의 로직에서 왜 참조하고 있는 객체의 메소드를 바로 호출하지 않는가?

백기선님 디자인 패턴 인프런 질문

 

학습 페이지

 

www.inflearn.com

내가 생각하는 Reciver의 메소드를 직접 호출하는 게 아닌 반대 성격의 command에게 위임했을 때의 장점은 변경이 최소화된다는 것이었다.

 

만약 undo에서 직접 호출한다고 하면?

Reciver의 메소드가 변경이 된다고 하면 해당 메소드를 execute()에서 호출하고 있는 command와 undo에서 호출하고 있는 command 2곳에서 변경이 일어난다.

 

하지만 반대 성격의 command에게 위임한다면?

execute()에서 호출하고 있는 command만 변경하면 된다.

 

Client

public class Client {
    public static void main(String[] args) {
        OKGoogle okGoogle = new OKGoogle();
        okGoogle.call(new HeaterOnCommand(new Heater()));
        okGoogle.call(new LampOnCommand(new Lamp()));
        okGoogle.undo();
        okGoogle.undo();
    }
}

// 출력
Heater On
Lamp On
Lamp Off
Heater Off

 

이제 OKGoogle(Invoker)은 직접적으로 Lamp / Heater (Receiver)에게 의존하지 않게 되었다. 

원하는 동작을 가지는 Command만 생성해서 넣어주면 된다. 

또한 이런 Command들은 생성하고 재사용이 가능하다. 

+ undo 기능도 사용 가능 

커맨드(Command) 패턴 정리

장점

(Invoker)기존 코드를 변경하지 않고 새로운 커맨드를 만들 수 있다

(Receiver)수신자의 코드가 변경되어도 호출자의 코드는 변경되지 않는다

커맨드 객체를 로깅, DB에 저장, 네트워크로 전송 하는 등 다양한 방법으로 활용할 수도 있다.

 

단점

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

커맨드(Command) 패턴 적용 사례

자바 

public class CommandInJava {
    public static void main(String[] args) {
        Heater heater = new Heater();
        Lamp lamp = new Lamp();
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        executorService.submit(heater::powerOn);
        executorService.submit(heater::powerOff);
        executorService.submit(lamp::turnOn);
        executorService.submit(lamp::turnOff);
    }
}

Runnable / 람다 / 메소드 래퍼런스 → 커맨드 패턴 

 

스프링

public class CommandInSpring {
    private DataSource dataSource;
    public CommandInSpring(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    public void add(Command command){
        SimpleJdbcInsert insert = new SimpleJdbcInsert(dataSource)
                .withTableName("command")
                .usingGeneratedKeyColumns("id");
        Map<String, Object> data = new HashMap<>();
        data.put("name", command.getClass().getSimpleName());
        data.put("when", LocalDateTime.now());
        insert.execute(data);
    }
}

SimpleJdbcInsert: ConcreteCommand

참고자료

백기선님 디자인 패턴 강의

 

코딩으로 학습하는 GoF의 디자인 패턴 강의 | 백기선 - 인프런

백기선 | 디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를

www.inflearn.com

커맨드 패턴 예제 참고 티스토리

 

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

커맨드 패턴( Command Pattern )커맨드 패턴은 객체의 행위( 메서드 )를 클래스로 만들어 캡슐화 하는 패턴입니다. 즉, 어떤 객체(A)에서 다른 객체(B)의 메서드를 실행하려면 그 객체(B)를 참조하고 있

victorydntmd.tistory.com