행동 관련 : 커맨드 패턴
- 디자인 패턴
- 2023. 12. 2.
들어가기 전
이 글은 인프런 백기선님의 GOF 강의를 복습하며 작성한 글입니다.
커맨드 패턴
- GOF : 요청을 캡슐화 하여 호출자(Invoker)와 수신자(Receiver)를 분리하는 패턴
- 요청을 처리하는 방법이 바뀌거나 추가되더라도 호출자의 코드는 변경되지 않는다.
- 자주 변경되지 않는 부분 : 호출자
- 자주 변경되는 부분 : 요청 처리 부분
- Invoker(호출자)와 Receiver(수신자)를 디커플링 시키는 패턴이다. 디커플링의 수단으로 Command 객체를 활용한다.
- Invoker는 Command Interface에 의존.
- Command Interface는 Receiver를 호출.
- 새로운 Receiver나 기능이 추가될 때 마다 Concrete Command가 추가됨.
- Component
- Invoker : 요청자. Command 인터페이스를 받고 execute()만 실행함.
- Comamnd : Invoker가 사용할 수 있도록 인터페이스를 제공함.
- ConcreteCommand : 구체적인 Receiver의 메서드를 호출함. Receiver가 누구인지, Operation이 무엇인지 등을 결정함.
- Receiver : 실제로 요청을 처리하는 클래스
패턴이 필요한 코드
public class Button {
private Light light;
public Button(Light light) { this.light = light; }
public void press() {
light.off();
// light.on()
}
public static void main(String[] args) {
Button button = new Button(new Light());
button.press();
button.press();
button.press();
button.press();
}
}
버튼 클래스를 이용해 Light 클래스의 불을 끄고 키는 기능이 있다고 가정해보자. 이 때 이런 문제점이 존재한다.
- 현재 press() 메서드만 존재하는데, Light에서 불을 키고 끄기 위해 press() 메서드를 수정해야 함.
- 혹은 Light의 On, Off를 위해 Button 클래스에 on(), off() 메서드를 추가해야함.
둘다 문제가 있다. 첫번째는 쓸 데 없이 계속 코드를 수정해야하는 점이다. 두번째는 Light - Button이 강하게 결합되어 있어서 Light에서 제공하는 인터페이스에 대응되는 메서드를 Button 클래스에서 생성해야 한다는 점이다.
만약 두번째처럼 구현하기로 결정했을 때, Light에 On / Off 기능보다 더 세분화 되어 1~5 단계로 밝기를 조절하는 기능이 추가되는 것을 상상해보자. 그렇다면 그에 대응되는 메서드가 Button에 추가되어야 할 것이다. Light에 새로운 기능이 추가될 때 마다 Button 클래스도 영향을 받기 때문에 코드 확장에 문제가 발생할 것이다.
디자인 패턴 적용하기
Button은 Invoker(호출자), Light는 Receiver(수신 및 처리자)로 볼 수 있다. 이 때, Command 클래스를 중간에 추가해서 Button - Receiver의 결합을 줄일 수 있다.
- Command 인터페이스를 제공한다.
- Command 인터페이스를 구현한 LightOffCommand, LightOnCommand를 구현한다.
- Button은 Command의 execute()만 실행하도록 한다.
- Command 구현체는 Receiver에 대한 정보를 모두 가진다. (어떤 Receiver 호출해야할지, 무슨 메서드 호출해야 할지)
public interface Command {
void execute();
void undo();
}
Command 인터페이스를 제공한다.
public class LightOffCommand implements Command {
private final Light light;
public LightOffCommand(Light light) { this.light = light; }
@Override
public void execute() { this.light.off(); }
@Override
public void undo() { new LightOnCommand(this.light).execute(); }
}
ConcreteCommand 클래스는 다음 기능을 추가한다.
- 어떤 Receiver를 호출해야할지
- Receiver의 어떤 인터페이스를 호출해야할지.
- Receiver를 호출할 때 어떤 파라메터를 전달해야할지
등을 전달해주면 된다.
public class Button {
private Stack<Command> commands = new Stack<>();
public void press(Command command) {
this.commands.add(command);
command.execute();
}
public void undo() {
Command command = this.commands.pop();
command.undo();
}
public static void main(String[] args) {
Button button = new Button();
button.press(new LightOnCommand(new Light()));
button.undo();
button.press(new GameStartCommand(new Game()));
button.undo();
}
}
Button 클래스는 다음과 같이 작성할 수 있다. Invoker는 단순히 Command를 Execute 하기만 하면 된다.
- Command 인터페이스에만 의존한다.
- Command 인터페이스의 execute() 메서드만 실행한다.
이 두 가지 덕분에 Button(Invoker) - Light(Receiver)는 디커플링된다.
조삼모사가 아닌가? 라고 생각할 수 있다. 왜냐하면 이런 관점이 있기 때문이다.
- Button 코드가 바뀌면 Command쪽 코드가 바뀌는 것 아닌가?
그것은 맞는 말인데, 기본적으로 Command 패턴에서 Invoker는 최대한 변화가 없는 역할로 본다. 주로 Receiver에 새로운 기능이 많이 추가되는 것을 가정했을 때 사용하는 디자인 패턴이다. 이런 경우라면 Receiver에 새로운 기능이 추가되더라도 ConcreteCommand만 추가되고 Invoker는 영향을 받지 않는다는 것에 주목해야 한다.
Command 패턴의 장점 / 단점
- 장점
- 기존 코드를 변경하지 않고 새로운 커맨드를 만들 수 있음. (OCP)
- Receiver(수신자)의 코드가 변경되어도 Invoker(호출자)의 코드는 변경되지 않음.
- Command 객체를 로깅, DB에 저장, 네트워크로 전송하는 등 다양한 방법으로 활용할 수도 있음.
- 단점
- 코드가 복잡하고 클래스가 많아진다.
예를 들어 커맨드에 undo() 기능을 추가해 볼 수도 있다. 예를 들어 GameEndCommand의 Undo는 Game을 시작하는 거니 new GameStart(this).execute() 같은 식으로도 처리해볼 수 있는데, 이를 통해 Command 객체를 다양한 형태로 확장해 볼 수 있다는 것이다.
'디자인 패턴' 카테고리의 다른 글
행동 관련 : 이터레이터 패턴 (Iterator Pattern) (0) | 2023.12.02 |
---|---|
행동 관련 : 중재자 패턴 (Mediator Pattern) (0) | 2023.12.02 |
구조 관련 : Composite 패턴 (0) | 2023.11.30 |
행동 관련 : Strategy(전략) 패턴 (0) | 2023.11.30 |
행동 관련 : State 패턴 (0) | 2023.11.30 |