커맨드 (Command) 패턴

weekbelt·2022년 12월 16일
0

1. 패턴 소개

요구 사항을 객체로 캡슐화 할 수 있으며, 매개변수를 써서 여러 가지 다른 요구 사항을 집어넣을 수도 있습니다. 또한 요청 내역을 큐에 저장하거나 로그로 기록할 수도 있으며, 작업취소 기능도 지원합니다.

위 그림으로 커맨드 패턴의 구성요소를 살펴보면 Invoker가 Command를 참조하고 주로 execute를 호출합니다. 경우에 따라서 undo를 지원하는 Command라면 undo를 실행할 수도 있습니다. Command인터페이스는 여러 구체적인 명령들을 추상화 시킨 인터페이스 혹은 추상클래스 입니다. 그리고 구체적인 타입인 ConcreteCommand들은 실질적으로 어떠한 Recevier를 사용할지 Recever의 operation을 직접 실행할때 필요로한 파라미터들이 담깁니다. Recevier는 요청에대한 작업을 수행하는 클래스입니다.

먼저 코드를 보면서 커맨드 패턴이 필요해지는 상황을 살펴보겠습니다. 아래에 간단한 Button클래스가 있습니다. Button클래스가 Light를 이용해서 버튼이 눌렸을때 불을 켜는 press메서드가 있습니다. 만약에 불을 꺼야한다면 press메서드의 light.on()을 light.off()로 수정해야합니다. 또 다른 문제는 Button이 의존하는 Light가 불을 끄는 메서드명이 on -> turnOff로 바뀐다면 Button클래스까지 수정을 해야합니다.

public class Button {

    private Light light;

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

    public void press() {
        light.on();
    }

    public static void main(String[] args) {
        Button button = new Button(new Light());
        button.press();
        button.press();
        button.press();
        button.press();
    }
}

또 다른 문제는 Button클래스에서 불을 키고 끄는것이 아닌 Game을 시작하거나 종료하고 싶은경우 Button클래스의 코드를 변경해야 합니다.

public class Button {

    private Game game;

    public Button(Game game) {
        this.game = game;
    }

    public void press() {
        game.start();
    }

}

이렇게되면 코드의 변경이 자주 일어납니다. 이런 이유는 호출을 하는쪽인 Invoker와 요청을 받는 Recevier의 관계가 상당이 타이트하다는 뜻입니다. 커맨드 패턴을 사용해서 요청 자체를 캡슐화해서 요청을 받는 Receiver가 누구이고, 그 Receiver의 어떤 operation을 호출해야하고, 호출할때 필요한 파라미터가 무엇인지 명령을 수행하기위한 모든 작업들을 Command라는 인터페이스 안으로 캡슐화 합니다. 그리고 명령을 호출하는 Invoker는 매우 추상화 되어 있는 Command라는 인터페이스의 excute메서드만 호출하면 됩니다.

2. 패턴 적용하기

커맨드 패턴을 적용하여 위에 언급했던 문제들을 해결해 보겠습니다. 기존의 문제점은 명령을 호출하는 Button과 명령을 실행하는 Light간의 관계가 타이트하다는 점이었습니다. 그래서 Button과 Light사이에 관계를 느슨하게 하기 위해서 Command를 사용해서 Receiver와 Invoker간의 관계를 분리시켜 보겠습니다.
먼저 Command라는 인터페이스를 선언하고 execute추상 메서드를 선언합니다.

public interface Command {

    void execute();
}

기존의 Button클래스가 Command를 사용하도록 Button클래스를 수정합니다. Command를 주입받아서 command의 execute메서드를 실행하는 것이 전부입니다.

public class Button {

    private Command command
    
    public Button(Command command) {
    	this.command = command;
    }

    public void press(Command command) {
        command.execute();
    }
}

그리고 어떤 인스턴스의 메서드를 호출할지 정의하기위해 ConcreteCommand들을 생성하겠습니다. 어떤 Receiver를 사용해서 어떤 메서드를 호출할지는 ConcreteCommand인 클래스에서만 알면 됩니다.

불을 키는 Command

public class LightOnCommand implements Command {

    private Light light;

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

    @Override
    public void execute() {
        light.on();
    }
}

불을 끄는 Command

public class LightOffCommand implements Command {

    private Light light;

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

    @Override
    public void execute() {
        light.off();
    }
}

게임을 시작하는 Command

public class GameStartCommand implements Command {

    private Game game;

    public GameStartCommand(Game game) {
        this.game = game;
    }

    @Override
    public void execute() {
        game.start();
    }
}

게임을 종료하는 Command

public class GameEndCommand implements Command {

    private Game game;

    public GameEndCommand(Game game) {
        this.game = game;
    }

    @Override
    public void execute() {
        game.end();
    }
}

이렇게 선언한 Command들을 Button을 통해서 사용해 보겠습니다. Button인스턴스를 생성해서 버튼를 press할때 Command들을 파라미터로 전달하면 됩니다. 불을 키고 싶다면 LightOnCommand를 게임을 시작명령을 하고싶다면 GameStartCommand를 전달합니다. 이렇게되면 Invoker인 Button을 코드를 변경할 필요가 없습니다.

public class Button {

    private Command command;
    
    public Button(Command command) {
    	this.command = command;
    }

    public void press(Command command) {
        command.execute();
    }

    public static void main(String[] args) {
        Button button = new Button();
        button.press(new GameStartCommand(new Game()));
        button.press(new LightOnCommand(new Light()));
    }

}

만약 Light나 Game인 Receiver의 코드가 바뀐다고 해도 Button은 추상화된 Command의 execute만을 호출하면 되기때문에 코드가 변경되지 않습니다. 하지만 Receiver를 사용하는 ConcreteCommand의 코드를 변경해야합니다. 결국 코드는 바뀌기 때문에 조삼모사가 아닌지 의문이 들수도 있습니다. 만약 Command를 의존하지않고 직접 Receiver를 의존해서 여러군데에서 쓰게 된다면 Reciver의 코드 변경이 일어나면 그것을 의존하는 모든 코드를 변경해야합니다. 하지만 Command를 의존하고 있다면 여러군데의 코드가 전혀 바뀔 필요가 없이 Receiver의 코드만 변경해주면 되기때문에 코드가 변경되는 부분이 현저히 줄어들게 됩니다.

3. 결론

커맨드(Command) 패턴 역시 기존코드를 변경하지 않고 새로운 커맨드를 생성할 수 있습니다. Receiver의 코드가 변경되어도 명령을 호출하는 Invoker쪽의 코드는 변경되지 않습니다. 하지만 Receiver를 사용하는 Command클래스들이 많이 생성된다는 단점이 있습니다.

참고

profile
백엔드 개발자 입니다

1개의 댓글

comment-user-thumbnail
2024년 9월 15일

To her surprise, Nina soon experienced a series of fortunate outcomes. Her bets on specific numbers and color combinations started paying off, and she began to accumulate winnings. The game’s dynamic nature and the excitement of each https://uniquecasino-france1.com/ spin kept her engaged, and she found herself refining her betting strategy on the fly. By the end of her gaming session, Nina had achieved a notable profit, demonstrating that even a casual approach to roulette could lead to impressive results. Her story is a testament to how experimentation and a bit of luck can turn a simple game into a rewarding experience.

답글 달기