Java/Design Pattern

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

민돌v 2023. 3. 29. 19:21
728x90
(인프런) 코딩으로 학습하는 GoF의 디자인 패턴 - 백기선, 강의를 보고 정리한 글입니다.
코드는 GitHub 에 있습니다

 

#1. 객체 생성 관련 패턴

#2. 구조 관련 패턴

 

#3. 행동 관련 패턴

 

 

 


✔️ 커맨드 패턴이란 (Command Patterns)

요청을 캡슐화 하여 호출자(Invoker) 와 수신자(receiver)를 분리하는 패턴
  •  커맨드 패턴은, 객체의 행위(메서드)를 클래스로 만들어 캡슐화하여 호출자와 수신자의 결합도(의존성)를 분리하는 패턴입니다.
  • 호출자(invoke) 객체가 어떠한 행위를 가지는 응답자(receiver) 객체의 기능을 직접적으로 호출한다면, 호출자 객체의 응답자의 메소드를 호출하는 부분의 코드가 수정되거나 || 응답자 객체의 코드가 수정되면 한쪽이 수정되더라도 양쪽 모두 수정이 되는 강한 결합상태를 가집니다.
  • 커맨드 패턴은 이러한 2 객체간의 의존성을 제거하기위한 패턴입니다.

커맨드 패턴 구조

 

 


✔️ 커맨드 패턴 사용 예시

먼저 커맨드 패턴을 적용하기 전에 예시를 들어보겠습니다

특정한 버튼을 누르면 "불이 켜진다" 라는 기능을 가지는 객체가 있다고 생각해봅시다.

  • Button 이라는 객체가 Invoker(호출자)가 되어 Light 객체 (Receiver) 의 "불을 켜다" 라는 기능을 수행하는 메소드를 호출하고 있습니다.
  • → 여기서 버튼을 눌렀을 때 "불을 끄고싶다." 로 요구사항이 변경되면 어떻게 될까요?
//호출자 (Invoker)
@AllArgsConstructor
public class Button {

    private Light lights;

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

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

//응답자 (Receiver)
public class Light {

    private boolean isOn;

    public void on() {
        System.out.println("불을 켭니다.");
        this.isOn = true;
    }
}

 

  • 먼저 Light 객체의 "불을 크다" 라는 기능을 수행하는 메소드가 추가되고, Button 객체의 receiver 를 호출하는 부분이 수정되야겠죠
  • 즉, 지금의 구조는 2 객체 모두가 수정되어야하는 강한 결합상태 입니다.
@AllArgsConstructor
public class Button {

    private Light lights;

    public void press() {
        //요청사항이 바뀌면 코드가 수정되어야함
        lights.off()
    }

    //생략
}


public class Light {

    private boolean isOn;

    public void on() {
        System.out.println("불을 켭니다.");
        this.isOn = true;
    }

    public void off() {
        System.out.println("불을 끕니다.");
        this.isOn = false;
    }
}

 

 

👏🏻 이 두 객체, 즉 호출자와 응답자 객체의 의존성을 제거하기위해 "커맨드 패턴" 을 사용합니다.

  • 지금은 버튼을 누를 때 "불을 키다", "불을 끄다" 이지만 → 버튼을 누를 때 게임을 시작한다 | 불꽃놀이가 터진다 | 주식이 폭등한다... 등등 다양한 요구사항이 생기고, 그럴 때 마다 중복에 가까운 객체가 마구마구 생성된거죠
  • 이런 커플링(강한 결합도)를 해결하기 위해 버튼을 누를때의 실행되는 행위를 캡슐화 합니다.

 

Command 객체

  • 버튼을 누를때 어떤 행위를 할지는 이 Command 인터페이스를 통해 호출합니다.
  • 이렇게 행위를 추상화하고, 내부 기능을 캡슐화함으로써 호출자(Invoker) 는 응답자(Receiver) 의 구체적인 사항을 몰라도 됩니다. (의존성 분리)
// JAVA Runnable 유사

public interface Command {

    void execute();
}

 

CommandImpl 객체

  • 그럼 이 기능을 구현하는 캡슐화 객체들이 존재하겠죠 간단하게 "불을 킨다" , "불을 끈다 이 2가지로 기능을 구현해 보겠습니다.
  • 여기서 주의해야할 점은 CommandImpl 객체가 어떤 작업을 수행하기 위해서는 필요한 모든 요소들을 알고 있어야 합니다.
//커멘드에는 어떤 작업을 하기위해 필요한 모든 요소들이 들어와야한다.

@AllArgsConstructor
public class LightOnCommand implements Command{

    private Light light;

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

@AllArgsConstructor
public class LightOffCommand implements Command{

    private Light light;

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

 

Button 호출자 객체 (Invoker)

  • 호출자는 Command 의 "실행" 메소드만을 바라보고 구체적인 기능은 내부로 캡슐화 되어있습니다.
  • 그렇기 때문에, 기능이 변경되더라도 응답자의 구현체가 추가되거나, 수정되어집니다.
  • 호출자(Button) 객체의 command 를 수행하는 부분은 수정이 일어나지 않는 구조가 되어집니다.
public class Button {

    private final Command command;

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

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

    public static void main(String[] args) {

        /**
         * Command 가 바뀌거나, 요구사항이 추가되더라도 invoker 는 수정이 일어나지 않아도 된다.
         * 수정의 범위가 축소, Command 재사용성의 증가, 책임의 분리
         */
	Button button_1 = new Button(new LightOffCommand(new Light()));
        Button button_2 = new Button(new LightOffCommand(new Light()));
        
	button_1.press();
        button_2.press();
    }
}

 

+ 게임을 시작하다! 라는 기능을 추가해도 기능을 정의한 구현체만 추가되지 Button의 코드는 수정되지 않습니다.

@AllArgsConstructor
public class GameStartCommand implements Command{

    private Game game;

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

public class Button {

    private final Command command;

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

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

    public static void main(String[] args) {

        /**
         * Command 가 바뀌거나, 요구사항이 추가되더라도 invoker 는 수정이 일어나지 않아도 된다.
         * 수정의 범위가 축소, Command 재사용성의 증가, 책임의 분리
         */

        Button button_1 = new Button(new LightOffCommand(new Light()));
        Button button_2 = new Button(new LightOffCommand(new Light()));
        Button button_3 = new Button(new GameStartCommand(new Game()));

        button_1.press();
        button_2.press();
        button_3.press();
    }
}

 

 

 

 

✔️ 커맨드 패턴 장,단점

장점

  1. 기존 코드를 변경하지 않고 새로운 커맨드를 만들 수 있다.
  2. 수신자의 코드가 변경되어도 호출자의 코드는 변경되지 않는다. → 수정의 범위가 축소된다.
  3. 커맨드 객체를 로깅, DB에 저장, 네트워크로 전송 하는 등 다양한 방법으로 활용할 수도 있다.

단점

  • 클래스가 많아진다.

 

 


이렇게 기능을 캡슐화하여 호출하는 쪽과 기능을 제공하는 쪽의 결합도를 분리하고 싶을 때

커맨드 패턴을 사용하면 두 객체간의 의존성을 제거하는데 좋을거같다는 생각이 들었습니다.. 끝!

 

 

 

 

 

반응형