[Design Pattern] 커맨드 패턴

olwooz·2023년 2월 23일
0

Design Pattern

목록 보기
14/22
요청을 요청에 대한 모든 정보를 가진 독립 실행형 객체로 만드는 행동 패턴

문제

텍스트 에디터 앱 가정

에디터의 다양한 작업을 위한 많은 버튼들이 있는 툴바를 만들어야 함

  • 툴바의 버튼과 대화 상자의 일반 버튼들이 사용하는 Button 클래스를 생성했음

모든 버튼이 비슷하게 생겼지만 모두 다른 작업을 하는 상황 - 버튼의 클릭 핸들러는 어디에 위치해야 할까?

  • 가장 간단한 방법은 버튼이 사용되는 곳마다 버튼 클릭 시 실행되는 코드를 포함하는 서브클래스 생성

잘못된 접근 방식

  • 너무 많은 서브클래스를 갖게 되고, 기초 Button 클래스를 수정하면 서브클래스 코드 훼손
  • GUI 코드가 비즈니스 로직의 불안정한 코드에 의존

복사/붙여넣기와 같은 작업들은 다양한 곳에서 호출됨

  • e.g. 툴바의 복사 버튼, Ctrl+C 등등

앱이 툴바만 가지고 있었을 때에는 다양한 작업의 구현을 버튼 서브클래스에 위치시켰어도 됐음

e.g. CopyButton 내에 텍스트 복사 코드가 있어도 무관

단축키, 콘텍스트 메뉴 등 다른 것들이 구현되면 한 작업의 코드를 많은 클래스에 붙여넣거나 메뉴들이 버튼에 의존하게 만들어야 함 → 더 안 좋은 상황

해결책

좋은 소프트웨어 디자인은 관심사 분리의 원칙을 기반으로 함 → 앱을 계층 구조로 분리

  • 가장 흔한 예시: GUI 레이어와 비즈니스 로직 레이어 분리

→ GUI 객체가 인수를 전달하며 비즈니스 로직 객체의 메서드를 호출 (”요청”을 보낸다고 표현)

커맨드 패턴 - GUI 객체들이 요청을 직접 보내면 안 되고, 요청의 세부 사항을 추출해 해당 요청을 작동시키는 단일 메서드를 가지는 별도의 커맨드 클래스로 분리

커맨드 객체 - GUI와 비즈니스 로직 객체들 사이 링크 역할 수행 → GUI는 무슨 비즈니스 로직 객체가 요청을 받고 요청이 어떻게 처리될 지 알 필요 없이 커맨드를 작동시키기만 하면 됨

커맨드들은 같은 인터페이스를 구현해야 함

  • 대부분 파라미터가 없는 단일 실행 메서드로 구성
  • 커맨드의 concrete 클래스와의 결합 없이 다양한 커맨드들을 같은 요청 발신자와 사용할 수 있게 해줌
  • 발신자와 연결된 커맨드 객체를 전환해 런타임에 발신자의 행동을 바꿀 수 있음
  • 요청 파라미터가 없는 이유 - 커맨드는 미리 설정된 데이터를 가지거나, 데이터를 가져올 수 있음

위 예제 - 커맨드 패턴을 적용하면 버튼 서브클래스들이 필요 없음

  • 버튼에 커맨드 객체에 대한 참조를 저장하는 필드만 있으면 됨
  • 가능한 작업에 대한 커맨드 클래스들을 구현해 특정 버튼과 연결
  • 메뉴, 단축키 등도 같은 방법으로 구현 가능
  • 커맨드는 GUI와 비즈니스 로직 계층 사이에 있는 중간 계층으로서 결합도를 낮춤

구조

1. 발신자 (invoker) - 요청을 시작하는 역할 수행
   - 커맨드 객체에 대한 참조를 저장하는 필드 필요
   - 발신자는 요청을 수신자에게 직접 보내지 않고 해당 커맨드를 작동시킴
   - 발신자는 커맨드 객체를 생성할 책임이 없음
   - 커맨드 객체는 일반적으로 생성자를 통해 클라이언트로부터 미리 생성됨
    
2. 커맨드 인터페이스 - 대개 커맨드를 실행하는 단일 메서드만을 선언

3. concrete 커맨드 - 다양한 종류의 요청 구현
   - 작업을 직접 수행하지 않고 비즈니스 로직 객체에 호출을 전달해야지만 코드 단순화를 위해 통합될 수 있음
   - 메서드를 실행하기 위해 필요한 매개변수는 concrete 커맨드의 필드로 선언될 수 있음
   - 초기화를 생성자에게만 허용함으로써 커맨드 객체를 불변하게 만들 수 있음
    
4. 수신자 - 비즈니스 로직 보유
   - 거의 모든 객체가 수신자가 될 수 있음
   - 대부분 커맨드들은 요청이 수신자에게 전달되는 방식에 대한 세부 사항만 처리하고, 수신자가 실제 작업을 수행
    
5. 클라이언트 - concrete 커맨드 객체들을 생성하고 설정
   - 수신자 인스턴스를 포함한 모든 요청 매개변수들을 커맨드 생성자에 전달해야 함
   - 결과로 생성된 커맨드는 하나 이상의 수신자와 연결

적용

작업들로 객체를 매개변수화하려는 경우

- 커맨드 패턴은 특정 메서드 호출을 독립 실행형 객체로 만들 수 있음 
  → 커맨드를 메서드의 인수로 전달하고, 다른 객체 내부에 저장하고, 연결된 커맨드를 전환하는 등 여러 작업이 가능해짐

작업들을 대기열에 넣거나, 실행을 스케줄링하거나, 원격으로 실행하려는 경우

- 다른 객체와 마찬가지로 커맨드 또한 직렬화될 수 있음
  → string으로 변환해 손쉽게 파일/DB에 넣고 커맨드 객체로 복원할 수 있음
  - 커맨드 실행을 지연시키거나, 스케줄링하거나, 네트워크 상에서 대기열에 넣고, 로그하고, 전송할 수 있음

되돌릴 수 있는 작업을 구현하려는 경우

- undo/redo를 구현하는 많은 방법 중에서 커맨드 패턴이 가장 인기있음
- 커맨드 history(실행된 커맨드 객체들과 프로그램 상태 백업을 가지는 스택) 구현해야 함
- 두 가지 결점 보유
  1. 프로그램의 상태 일부가 private이라 저장하기 어려울 수 있음 → 메멘토 패턴으로 완화 가능
  2. 상태 백업이 RAM을 많이 소모할 수 있음 
     → 이전 상태로 복원하는 대신 커맨드가 반대 작업을 실행하는 방법도 있음 
     → 구현이 어렵거나 불가능할 수도 있음

구현방법

1. 단일 실행 메서드로 커맨드 인터페이스 선언

2. 요청들을 커맨드 인터페이스를 구현하는 concrete 커맨드 클래스들로 추출
   - 각 클래스는 요청 인수와 실제 요청 객체 참조를 저장하는 필드가 있어야 함
   - 모든 값들은 커맨드의 생성자에 의해 초기화되어야 함
    
3. 발신자 역할을 하는 클래스들을 식별해 커맨드를 저장할 필드 추가
   - 발신자는 커맨드 인터페이스를 통해서만 커맨드와 소통해야 함
   - 발신자들은 커맨드 객체들을 스스로 생성하지 않고 주로 클라이언트 코드로부터 받음
    
4. 발신자들이 수신자들에게 직접 요청을 보내는 대신 커맨드를 작동하도록 변경

5. 클라이언트는 아래 순서대로 객체 초기화:
   - 수신자 생성
   - 커맨드 생성 후 필요하다면 수신자와 연결
   - 발신자 생성 후 특정 커맨드들과 연결

장단점

장점

- SRP - 작업을 유발하는 클래스들과 작업을 수행하는 클래스들을 분리할 수 있음
- OCP - 기존 클라이언트 코드를 훼손하지 않고 새 커맨드 도입 가능
- undo/redo 구현 가능
- 작업 지연 실행 구현 가능
- 단순한 커맨드들을 모아 복잡한 커맨드로 조립 가능

단점

- 발신자와 수신자 사이에 새로운 계층이 도입돼 코드가 더 복잡해질 수 있음

다른 패턴과의 관계

- 책임 연쇄, 커맨드, 중재자, 옵저버 패턴은 요청의 발신자와 수신자를 연결하는 다양한 방법을 다룸
  - 책임 연쇄 패턴 - 요청이 처리될 때까지 잠재적 수신자들로 구성된 동적 체인을 따라 전달함
  - 커맨드 - 수신자와 발신자 단방향 커넥션 수립
  - 중재자 - 수신자와 발신자 사이의 직접적인 연결을 제거하고 중재자 객체를 통해서만 소통하게 함
  - 옵저버 - 수신자들이 동적으로 요청 수신을 구독/구독 취소할 수 있음
  
- 책임 연쇄 패턴의 핸들러들은 커맨드 패턴으로 구현 가능 
  → 다양한 작업들을 같은 컨텍스트 객체에 대해 실행할 수 있고, 해당 객체는 요청으로 표현됨
  - 요청 자체가 커맨드 객체인 경우 → 같은 연산을 체인으로 연결된 서로 다른 일련의 콘텍스트들에서 실행할 수 있음
  
- undo 구현 시 커맨드 패턴과 메멘토 패턴을 함께 사용할 수 있음
  - 커맨드 - 대상 객체에 작업을 수행하는 역할
  - 메멘토 - 커맨드가 실행되기 직전에 객체의 상태를 저장하는 역할
  
- 커맨드 패턴과 전략 패턴은 객체를 특정 작업으로 매개변수화할 수 있다는 점에서 비슷해 보이지만 다른 의도를 가짐
  - 커맨드 - 어떤 작업이든 객체로 변환할 수 있음, 작업의 매개변수는 객체의 필드가 됨, 
    해당 변환은 작업 지연 실행, 작업을 대기열에 추가, 커맨드들의 history 저장, 
    커맨드들을 원격 서비스에 전달하는 등 기능을 가능하게 함
  - 전략 - 주로 같은 일을 하는 여러 방법들을 설명해, 
    단일 컨텍스트 클래스 내부에서 해당 알고리즘들을 바꿔가며 사용할 수 있게 함
  
- 프로토타입 - 커맨드의 복제본을 history에 저장할 때 도움을 줄 수 있음

- 비지터 패턴 - 커맨드 패턴의 강력한 버전, 
  비지터 패턴 객체들은 작업들을 다양한 클래스들의 다양한 객체들에게 실행할 수 있음

TypeScript 예제

/**
 * The Command interface declares a method for executing a command.
 */
interface Command {
    execute(): void;
}

/**
 * Some commands can implement simple operations on their own.
 */
class SimpleCommand implements Command {
    private payload: string;

    constructor(payload: string) {
        this.payload = payload;
    }

    public execute(): void {
        console.log(`SimpleCommand: See, I can do simple things like printing (${this.payload})`);
    }
}

/**
 * However, some commands can delegate more complex operations to other objects,
 * called "receivers."
 */
class ComplexCommand implements Command {
    private receiver: Receiver;

    /**
     * Context data, required for launching the receiver's methods.
     */
    private a: string;

    private b: string;

    /**
     * Complex commands can accept one or several receiver objects along with
     * any context data via the constructor.
     */
    constructor(receiver: Receiver, a: string, b: string) {
        this.receiver = receiver;
        this.a = a;
        this.b = b;
    }

    /**
     * Commands can delegate to any methods of a receiver.
     */
    public execute(): void {
        console.log('ComplexCommand: Complex stuff should be done by a receiver object.');
        this.receiver.doSomething(this.a);
        this.receiver.doSomethingElse(this.b);
    }
}

/**
 * The Receiver classes contain some important business logic. They know how to
 * perform all kinds of operations, associated with carrying out a request. In
 * fact, any class may serve as a Receiver.
 */
class Receiver {
    public doSomething(a: string): void {
        console.log(`Receiver: Working on (${a}.)`);
    }

    public doSomethingElse(b: string): void {
        console.log(`Receiver: Also working on (${b}.)`);
    }
}

/**
 * The Invoker is associated with one or several commands. It sends a request to
 * the command.
 */
class Invoker {
    private onStart: Command;

    private onFinish: Command;

    /**
     * Initialize commands.
     */
    public setOnStart(command: Command): void {
        this.onStart = command;
    }

    public setOnFinish(command: Command): void {
        this.onFinish = command;
    }

    /**
     * The Invoker does not depend on concrete command or receiver classes. The
     * Invoker passes a request to a receiver indirectly, by executing a
     * command.
     */
    public doSomethingImportant(): void {
        console.log('Invoker: Does anybody want something done before I begin?');
        if (this.isCommand(this.onStart)) {
            this.onStart.execute();
        }

        console.log('Invoker: ...doing something really important...');

        console.log('Invoker: Does anybody want something done after I finish?');
        if (this.isCommand(this.onFinish)) {
            this.onFinish.execute();
        }
    }

    private isCommand(object): object is Command {
        return object.execute !== undefined;
    }
}

/**
 * The client code can parameterize an invoker with any commands.
 */
const invoker = new Invoker();
invoker.setOnStart(new SimpleCommand('Say Hi!'));
const receiver = new Receiver();
invoker.setOnFinish(new ComplexCommand(receiver, 'Send email', 'Save report'));

invoker.doSomethingImportant();
// Output.txt

Invoker: Does anybody want something done before I begin?
SimpleCommand: See, I can do simple things like printing (Say Hi!)
Invoker: ...doing something really important...
Invoker: Does anybody want something done after I finish?
ComplexCommand: Complex stuff should be done by a receiver object.
Receiver: Working on (Send email.)
Receiver: Also working on (Save report.)

참고 자료: Refactoring.guru

0개의 댓글