Design Pattern, Command

2022. 2. 6. 17:29ETC/Design Patterns

 

Object Behavioral Pattern

Command Pattern

 

-----------------    INDEX     -----------------

 

Command Pattern ?

Structure

Sample Code: Java

Applicability

관련 패턴

 

----------------------------------------------

 

 

Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

- GoF Design Patterns

 

 

커맨드 패턴은 요청을 객체의 형태로 캡슐화하고, 그로 인해 다양한 요청·대기열·로그 요청을 하는 클라이언트를 매개 변수화하고 실행 취소 작업을 지원할 수 있습니다.

 

커맨드 패턴이 이해갈듯 말듯하다가 시간이 조금 지체되었습니다. 개념을 이해한다 싶으면 구조가 헷갈리고 그러더라구요 .. 

그래서 설명을 조금 더 자세하게 다룰까 합니다. 미래의 제가 분명히 잊을테니까요,,ㅎㅎ,ㅎ,ㅎㅎ,,ㅎ,,

 

 

Intro

커맨드 패턴은 위의 소개 그대로 요청을 캡슐화합니다. 캡슐화하는 이유를 살펴보고, 그에 대한 이점도 같이 보겠습니다.

 

누구나 'words', '한/글', 'google docs'로 문서를 편집해본 적 있을텐데요.

이런 문서 편집 어플리케이션에서는 다양한 기능을 제공합니다. 

 

기본적으로 문서를 열고(open), 닫는(close) 기능부터

글자나 문장을 오려두거나(cut), 복사(copy)하고 붙여넣는(paste) 기능 등을 제공합니다.

이런 기능을 직접 구현한다고 생각하면 Command 패턴을 이해하는데 더 가까워집니다.

 

 

Toolbar의 Buttons을 통해 해당 기능들을 구현한다고 할 때 어떻게 구현할까요?

Button 인터페이스를 받는 OpenButton, CloseButton, CutButton, CopyButton, PasteButton을 생성해서 내부 메서드로 작성하는 방법이 있어요.

 

여기서 몇 가지의 문제점을 제안해보겠습니다.

먼저, 단축키를 이용해 동일한 작업을 하고 싶을 때는 어떻게 해야할까요?

혹은 CutButton을 구현할 때 'Copy -> Delete'의 동작을 CopyButton과는 따로 동일한 로직을 구현해야할까요?

 

여러분은 어떤 방식으로 해결할 것 같으신가요?

위의 상황에서는 Button이라는 외부 인터페이스(e.g. GUI Button)와 그에 해당하는 명령(Command)을 분리하여 구현할 수 있습니다.

명령을 따로 캡슐화하여 외부 인터페이스 구현체에서 불러오는 거죠.

 

이것이 바로 Command Pattern입니다. 

 

 

더 깊게, 명령과 실제 로직을 분리합니다.

예를 들어 오려두기 명령을 구현할 때에는 Copy 로직과 Delete 로직을 따로 불러와 명령을 실행하면 됩니다.

(코드, 파일의 간결함을 위해 종종 합치기도 합니다.)

만약 실제 로직을 직접 불러오다 보면, 객체지향에서 자연스럽게 로직들을 캡슐화하는 명령 객체를 몇 개 구현할 수 밖에 없겠죠?

 

아래에서 소개할 개체들로 나타내면,

이렇게 로직들의 모임들을 구현한 객체가 (Concrete)Command

실제 로직들을 구현하는 객체가 Receiver(명령을 받는 최종 수신자)입니다.

또, 버튼이나 단축키는 요청을 발생시키는 Invoker(Sender)의 역할을 합니다.

 

 

 

Interface

Command 패턴의 핵심은 Operation을 실행하는 동작을 통일화하는 인터페이스입니다.

 

매우 간단하게 코드로 확인해볼게요.

 

public interface Command {
    public void execute();
}

 

정말 간단히 위와 같이 표현됩니다.

필요에 따라 abstract class로 구현할 수도 있겠죠.

 

이를 받는 명령들은 execute을 필수적(강제적)으로 구현해야합니다.

예를들어, 간단한 Copy 명령을 작성해보도록 할게요.

 

abstract class Command {
    protected Editor editor;

    public abstract void execute();
}

class CopyCommand extends Command {
    @Override
    public void execute() {
        editor.copy();
    }
}

class CutCommand extends Command {
    @Override
    public void execute() {
        editor.copy();
        editor.delete();
    }
}
class Editor {
    private Application app;
    private String selection;

    void copy() {
        app.clipboard = this.selection;
    }

    void delete() {
        this.selection = "";
    }
    
    // ...
}

 

 

위와 같이 execute() 메서드를 필수로 만들어 어떤 Command 객체를 실행해도 같은 동작이 되게끔 만들어줍니다.

 

 

이제 어느정도 이해했으니 전체적인 구조를 자세히 살펴보도록 할게요.

 

 

Structure

 

[GoF Design Patterns] - command Diagram

 

Command

✔️ Execute() 작업을 실행하기 위한 인터페이스를 선언

 

 

ConcreteCommand

✔️ Receiver 객체와 action 간의 바인딩을 정의
✔️ Receiver의 operation을 호출하고 실행하는 동작을 구현

 

ConcreteCommand은 다양한 종류의 요청들을 구현합니다.

ConcreteCommand은 스스로 작업을 수행하는 것이 아니라 비즈니스 논리 객체 중 하나(Receiver)에 호출을 전달합니다.

하지만 코드를 단순화하기 위해 이 클래스들이 병합될 수 있습니다.

 

 

Receiver

✔️ 수신기 클래스에는 비즈니스 논리들이 포함되어 있으며,

✔️ 거의 모든 개체가 수신기 역할을 할 수 있음

 

대부분의 Commands는 Receiver가 실제 작업을 수행하는 동안,

요청이 Receiver로 전달되는 방법에 대한 세부 사항만 처리합니다.

 

 

Invoker

✔️ 명령을 통해 요청을 수행하도록 요청

 

Invoker는 Sender로도 불리며, 요청을 시작하는 역할을 합니다.

이 클래스에는 Command 개체에 대한 참조를 저장하는 필드가 있어야 합니다. Receiver에게 직접 요청을 보내는 대신 Sender가 해당 명령을 트리거합니다.

Sender는 Command 객체를 직접 생성하지 않고, 일반적으로는 생성자를 통해 미리 생성된 Command를 받습니다.

 

 

Client

✔️ ConcreteCommand 개체를 만들고 해당 Receiver를 설정

 

 

 

Sample Code: Java

Command

public abstract class Command {
    protected Editor editor;

    public Command(Editor editor) {
        this.editor = editor;
    }

    public abstract void execute();

    public void undo() {
        // undo logic
    }
}

 

 

ConcreteCommand

public class CopyCommand extends Command {
    public CopyCommand(Editor editor) {
        super(editor);
    }
    @Override
    public void execute() {
        editor.copy();
    }
}

public class CutCommand extends Command {
    public CutCommand(Editor editor) {
        super(editor);
    }
    @Override
    public void execute() {
        editor.copy();
        editor.delete();
    }
}

public class PasteCommand extends Command {
    public PasteCommand(Editor editor) {
        super(editor);
    }
    @Override
    public void execute() {
        editor.paste();
    }
}

 

 

Receiver

public class Editor {
    private Application app;
    public String selection;

    public Editor(Application app) {
        this.app = app;
    }

    public void copy() {
        System.out.println("copy " + this.selection);
        app.clipboard = this.selection;
    }

    public void delete() {
        System.out.println("delete " + this.selection);
        this.selection = "";
    }

    public void paste() {
        this.selection = app.clipboard;
        System.out.println("paste " + this.selection);
    }
}

 

 

Invoker

public class Button {
    Command cmd;

    public void setCommand(Command command) {
        this.cmd = command;
    }

    public void click() {
        this.cmd.execute();
    }
}

 

 

Client

class Application {
    String clipboard;
    CommandHistory history;


    public static void main(String[] args) {
        Application docs = new Application();

        Editor editor = new Editor(docs);
        editor.selection = "design pattern : command!";

        Command copy = new CopyCommand(editor);

        Button copyButton = new Button();
        copyButton.setCommand(copy);
        copyButton.click();

        Command paste = new PasteCommand(editor);

        Button pasteButton = new Button();
        pasteButton.setCommand(paste);
        pasteButton.click();

        Command cut = new CutCommand(editor);

        Button cutButton = new Button();
        cutButton.setCommand(cut);
        cutButton.click();
    }
}

 

 

코드는 Refactoring Guru에서 참고하여 구현했습니다.

 

 

 

 

Applicability

콜백 함수

Command 객체가 수행할 작업을 매개 변수화합니다.

이러한 매개 변수화는 절차적 언어에서의 콜백 함수(나중에 호출할 수 있는 어딘가에 등록된 함수)와 같이 표현할 수 있습니다.

객체 지향에서 콜백을 Command로 대체할 수 있습니다.

 

 

시점 제어

요청서로 다른 시간에 지정하거나 대기열로 처리하거나 실행할 수 있습니다.

위임 받은 객체를 순차적으로 실행하는 것이 아니라, 실행 시점을 미리 설정한 후 실행할 수 있습니다.

명령 패턴은 객체의 원래 처리 요청 시점과 다른 생명주기를 가지며, 명령 패턴을 이용하면 동작 실행의 예약처리같은 작업도 가능합니다.

 

 

복구 기능

동작 명령과 반대되는 명령으로 취소 처리를 추가하거나, 큐와 같은 저장소 리스트를 역으로 탐지해 기존의 동작을 취소할 수도 있습니다.

이 때, Command 인터페이스에는 이전 호출를 되돌리는 실행 취소 작업(e.g. undo method)이 추가되어야 합니다.

 

 

저장 기능

시스템 충돌 시 다시 적용할 수 있도록 로깅 변경 사항을 지원합니다.

로드 및 저장 작업으로 명령 인터페이스를 늘리면 변경 사항에 대한 로그를 영구적으로 유지할 수 있습니다.

충돌 복구 시에는 디스크에서 기록된 명령을 다시 로드하고 실행 작업을 통해 해당 명령을 다시 실행합니다.

 

 

 

관련 패턴

C- Creational Patterns  |  S - Structural Patterns  |  B - Behavioral Patterns

 

 

C: Prototype

Command 패턴은 명령 객체의 상태를 저장할 때 객체를 복사하고,

Prototype은 객체를 생성하지 않고 생성된 객체를 복제하여 저장합니다.

 

 

S: Composite

여러 개의 명령 객체를 관리하기 위해 복합체composite 패턴을 사용할 수 있습다.

복합체 패턴은 복합구조의 객체를 응용하여 여러 개의 노드를 갖습니다.

명령 객체의 Invoker가 여러 개의 명령 객체를 갖고 있는 것이 복합체 패턴과 유사합니다.

 

 

B: Memento

실행되는 명령의 이력을 저장할 대 메멘토 패턴을 함께 응용합니다.

메멘토는 상태를 관리하는 패턴으로,

메멘토를 이용해 객체의 상태를 저장하며, 저장된 상태값을 이용해 undo 기능을 구현할 수 있습다.

 

 

 

 

그럼 지금까지 Command Pattern에 대해 알아보았습니다.

오타나 잘못된 내용이 있다면 댓글로 남겨주세요!

감사합니다 ☺️ 

 

 

 

모든 Design Patterns 모아보기

 

Design Patterns

안녕하세요. GoF 디자인 패턴을 정리하고자 합니다. 디자인 패턴은 일주일 전부터 공부를 시작했는데, 스스로 설명하듯 적는게 익히는데 도움이 클 것같아 정말 오랜만에 시리즈로 포스팅하려

gngsn.tistory.com

 

 

'ETC > Design Patterns' 카테고리의 다른 글

Design Pattern, Visitor  (0) 2022.02.13
Design Pattern, Iterator  (0) 2022.02.10
Design Pattern, Decorator  (0) 2022.02.03
Design Pattern, Composite  (0) 2022.02.01
Design Pattern, Singleton  (0) 2022.01.26