봄맞이 세일

커맨드 패턴

다음 이름으로도 불립니다: 액션, 트랜잭션, Command

의도

커맨드는 요청을 요청에 대한 모든 정보가 포함된 독립실행형 객체로 변환하는 행동 디자인 패턴입니다. 이 변환은 다양한 요청들이 있는 메서드들을 인수화 할 수 있도록 하며, 요청의 실행을 지연 또는 대기열에 넣을 수 있도록 하고, 또 실행 취소할 수 있는 작업을 지원할 수 있도록 합니다.

커맨드 디자인 패턴

문제

당신이 새로운 텍스트 편집기 앱을 개발하고 있다고 상상해 봅시다. 당신이 현재 하는 작업은 편집기의 다양한 작업을 위한 여러 버튼이 있는 도구 모음​(툴바)​을 만드는 것입니다. 당신은 도구 모음의 버튼들과 다양한 대화 상자들의 일반 버튼들에 사용할 수 있는 매우 깔끔한 Button​(버튼) 클래스를 만들었습니다.

커맨드 패턴으로 해결된 문제

앱의 모든 버튼은 같은 클래스에서 파생됩니다.

이 버튼들은 모두 비슷해 보이지만 각각 다른 기능들을 수행해야 합니다. 그러면 이 버튼들의 다양한 클릭 핸들러들에 대한 코드는 어디에 두겠습니까? 가장 간단한 해결책은 버튼이 사용되는 각 위치에 수많은 자식 클래스들을 만드는 것입니다. 이러한 자식 클래스들에는 버튼 클릭 시 실행되어야 하는 코드가 포함됩니다.

많은 버튼 자식 클래스들

많은 버튼 자식 클래스들이 있습니다. 무엇이 잘못될 수 있을까요?

머지않아 당신은 이 접근 방식에 심각한 결함이 있음을 깨닫게 됩니다. 일단, 당신은 이제 엄청난 수의 자식 클래스들이 있으며, 기초 Button 클래스를 수정할 때마다 이러한 자식 클래스의 코드를 깨뜨릴 위험이 있습니다. 간단히 말해서, 그래픽 사용자 인터페이스 코드는 비즈니스 로직의 불안정한 코드에 어색하게 의존하게 되었습니다.

여러 클래스가 같은 기능을 구현합니다

여러 클래스가 같은 기능을 구현합니다.

그리고 당신이 고려해야 할 최악의 사항은, 텍스트 복사/붙여넣기와 같은 일부 작업은 여러 위치에서 호출될 수 있다는 사실입니다. 예를 들어, 사용자는 무언가를 복사하기 위하여 도구 모음에서 작은 '복사' 버튼을 클릭하거나 콘텍스트 메뉴를 통해 무언가를 복사하거나 키보드에서 Ctrl+C를 누를 수 있습니다.

당신의 앱에 처음에 하나의 도구 모음만 있었을 때는 다양한 작업의 구현을 버튼의 자식 클래스들에 배치해도 괜찮았습니다. 즉, Copy­Button 자식 클래스의 내부에 텍스트를 복사하는 코드가 있어도 괜찮았습니다. 그러나 복사를 할 수 있도록 하는 콘텍스트 메뉴, 바로 가기 및 기타 항목들을 구현하면 당신은 이제 해당 작업의 코드를 많은 클래스에 복제하거나 버튼에 의존하는 메뉴들을 만들어야 하는데, 이것은 오히려 더 나쁜 옵션입니다.

해결책

올바른 소프트웨어 디자인은 종종 을 기반으로 합니다. 가장 일반적인 예로는 그래픽 사용자 인터페이스용 레이어와 비즈니스 로직용 레이어의 분리입니다. 그래픽 사용자 인터페이스용 레이어는 모든 입력을 캡처하고 화면에 아름다운 그림을 렌더링하고 사용자와 앱이 수행하는 작업의 결과를 나타내는 역할을 합니다. 그러나 달의 행성 궤도를 계산하거나 연간 보고서를 작성하는 것과 같은 중요한 작업을 수행할 때 그래픽 사용자 인터페이스 레이어는 비즈니스 논리의 배경 레이어들에 작업을 위임합니다.

위의 내용은 코드로 다음과 같이 표현될 수 있습니다. 그래픽 사용자 인터페이스 객체가 비즈니스 논리 객체의 메서드를 호출하고 일부 인수를 전달합니다. 위 프로세스는 일반적으로 한 객체가 다른 객체에 을 보내는 것이라고 불립니다.

그래픽 사용자 인터페이스 객체들은 비즈니스 논리 객체들을 직접 액세스할 수 있습니다

그래픽 사용자 인터페이스 객체들은 비즈니스 논리 객체들에 직접 접근할 수 있습니다.

커맨드 패턴은 그래픽 사용자 인터페이스 객체들이 이러한 요청을 직접 보내서는 안된다고 합니다. 대신 모든 요청 세부 정보들​(예: 호출되는 객체, 메서드 이름 및 인수 리스트)​을 요청을 작동시키는 단일 메서드를 가진 별도의 커맨드 클래스로 추출하라고 제안합니다.

커맨드 객체들은 다양한 그래픽 사용자 인터페이스 객체들과 비즈니스 논리 객체들 간의 링크 역할을 합니다. 이제부터 그래픽 사용자 인터페이스 객체는 어떤 비즈니스 논리 객체가 요청을 받을지와 이 요청이 어떻게 처리할지에 대하여 알 필요가 없습니다. 그래픽 사용자 인터페이스 객체는 커맨드를 작동시킬 뿐이며, 그렇게 작동된 커맨드는 모든 세부 사항을 처리합니다.

커맨드를 통해 비즈니스 논리 레이어를 액세스합니다.

커맨드를 통해 비즈니스 논리 레이어를 접근합니다.

이제 다음 단계는 당신의 커맨드들이 같은 인터페이스를 구현하도록 하는 것입니다. 일반적으로 커맨드는 매개 변수들을 받지 않는 단일 실행 메서드만을 가집니다. 이 인터페이스는 다양한 커맨드들을 커맨드들의 구상 클래스들과 결합하지 않고 같은 요청 발신자와 사용할 수 있게 해줍니다. 이제 당신은 발신자에 연결된 커맨드 객체들을 전환할 수 있으며, 그렇게 하여 런타임에 발신자의 행동을 변경할 수 있습니다.

당신은 요청 매개변수들이 빠져있다는 점을 눈치채셨을 것입니다. 그래픽 사용자 인터페이스 객체가 비즈니스 레이어 객체에 일부 매개변수들을 제공했을 수 있습니다. 커맨드 실행 메서드에 매개변수들이 없는데, 그러면 어떻게 요청의 세부 정보를 수신자에게 전달할 수 있을까요? 커맨드를 이러한 데이터로 미리 설정해놓거나, 이 데이터를 자체적으로 가져올 수 있도록 해야 합니다.

그래픽 사용자 인터페이스 객체들은 작업을 커맨드들에 위임합니다

그래픽 사용자 인터페이스 객체들은 작업을 커맨드들에 위임합니다.

다시 당신의 텍스트 편집기를 살펴봅시다. 커맨드 패턴을 적용한 후에는 더 이상 다양한 클릭 행동들을 구현하기 위한 여러 버튼 자식 클래스들이 필요하지 않습니다. 기초 Button 클래스에 커맨드 객체에 대한 참조를 저장하는 단일 필드를 넣은 후 이 버튼이 클릭 될 때 그 커맨드를 시행하도록 하면 됩니다.

이제 가능한 모든 작업에 대해 많은 커맨드 클래스들을 구현하고 이 클래스들을 버튼의 의도된 동작에 따라 특정 버튼들과 연결해야 합니다.

메뉴, 단축키 또는 대화 상자와 같은 다른 그래픽 사용자 인터페이스 요소들도 같은 방식으로 구현할 수 있습니다. 이들은 사용자가 그래픽 사용자 인터페이스 요소와 상호 작용할 때 실행되는 커맨드에 연결될 것입니다. 지금쯤 짐작하셨겠지만 같은 작업과 관련된 요소들은 같은 커맨드들에 연결되어 코드 중복을 방지할 것입니다.

결과적으로 커맨드들은 그래픽 사용자 인터페이스 레이어와 비즈니스 로직 레이어 간의 결합도를 줄이는 편리한 중간 레이어들이 됩니다. 그리고 이것은 커맨드 패턴이 제공할 수 있는 이점의 극히 일부에 불과합니다!

실제상황 적용

레스토랑에서 주문하기

레스토랑에서 주문하기.

당신은 도시를 한참 걷다가 멋진 레스토랑에 도착하여 창가 테이블에 앉습니다. 친절한 웨이터가 다가와 신속하게 당신의 주문을 받아 종이에 적습니다. 웨이터는 부엌으로 가서 주문을 벽에 붙입니다. 잠시 후 요리사에게 주문이 전달되고 요리사는 주문을 읽고 그에 따라 음식을 요리합니다. 요리사는 주문과 함께 식사를 트레이에 놓습니다. 웨이터는 트레이를 발견한 후 당신이 주문한 대로 식사가 요리되었는지 확인하고 완성된 주문을 당신의 테이블로 가져옵니다.

종이에 적힌 주문은 커맨드 역할을 합니다. 이 주문은 요리사가 요리할 준비가 될 때까지 대기열에 남아 있습니다. 주문에는 식사를 요리하는 데 필요한 모든 관련 정보가 포함되어 있습니다. 이를 통해 요리사는 당신에게서 주문 세부 사항을 직접 전달받는 대신 바로 요리를 시작할 수 있습니다.

구조

커맨드 디자인 패턴의 구조커맨드 디자인 패턴의 구조
  1. 발송자 클래스​(invoker라고도 함)​는 요청들을 시작하는 역할을 합니다. 이 클래스에는 커맨드 객체에 대한 참조를 저장하기 위한 필드가 있어야 합니다. 발송자는 요청을 수신자에게 직접 보내는 대신 해당 커맨드를 작동시킵니다. 참고로 발송자는 커맨드 객체를 생성할 책임이 없으며 일반적으로 생성자를 통해 클라이언트로부터 미리 생성된 커맨드를 받습니다.

  2. 커맨드 인터페이스는 일반적으로 커맨드를 실행하기 위한 단일 메서드만을 선언합니다.

  3. 구상 커맨드들은 다양한 유형의 요청을 구현합니다. 구상 커맨드는 자체적으로 작업을 수행해서는 안 되며, 대신 비즈니스 논리 객체 중 하나에 호출을 전달해야 합니다. 그러나 코드를 단순화하기 위해 이러한 클래스들은 병합될 수 있습니다.

    수신 객체에서 메서드를 실행하는 데 필요한 매개 변수들은 구상 커맨드의 필드들로 선언할 수 있습니다. 생성자를 통해서만 이러한 필드들의 초기화를 허용함으로써 커맨드 객체들을 불변으로 만들 수 있습니다.

  4. 수신자 클래스에는 일부 비즈니스 로직이 포함되어 있습니다. 거의 모든 객체는 수신자 역할을 할 수 있습니다. 대부분의 커맨드들은 요청이 수신자에게 전달되는 방법에 대한 세부 정보만 처리하는 반면 수신자 자체는 실제 작업을 수행합니다.

  5. 클라이언트는 구상 커맨드 객체들을 만들고 설정합니다. 클라이언트는 수신자 인스턴스를 포함한 모든 요청 매개변수들을 커맨드의 생성자로 전달해야 하며 그렇게 만들어진 커맨드는 하나 또는 여러 발송자와 연관될 수 있습니다.

의사코드

이 예시에서 커맨드 패턴은 실행된 작업의 기록을 추적하는 데 도움을 주며 필요한 경우 작업을 되돌릴 수 있도록 합니다.

커맨드 패턴의 구조 예시

텍스트 편집기에서 실행 취소될 수 있는 작업들.

잘라내기 및 붙여넣기 등의 편집기 상태를 변경하는 커맨드들은 커맨드와 관련된 작업을 실행하기 전에 편집기 상태의 백업 복사본을 만듭니다. 어떤 커맨드가 실행되고 나면, 그 커맨드는 해당 지점에서 편집기 상태의 백업 복사본과 함께 커맨드 기록​(커맨드 객체들의 스택)​에 배치됩니다. 나중에 사용자가 작업을 되돌려야 하는 경우 앱은 기록에서 가장 최근 커맨드를 가져와 편집기 상태의 관련 백업을 읽은 후 작업을 되돌릴 수 있습니다.

클라이언트 코드​(그래픽 사용자 인터페이스 요소들, 커맨드 기록 등)​는 커맨드 인터페이스를 통해 커맨드와 함께 작동하기 때문에 구상 커맨드 클래스들에 결합하지 않습니다. 이러한 접근 방식을 사용하면 기존 코드를 손상하지 않고 앱에 새 커맨드들을 도입할 수 있습니다.

// 기초 커맨드 클래스는 모든 구상 커맨드에 대한 공통 인터페이스를 정의합니다.
abstract class Command is
    protected field app: Application
    protected field editor: Editor
    protected field backup: text

    constructor Command(app: Application, editor: Editor) is
        this.app = app
        this.editor = editor

    // 편집기의 상태에 대한 백업을 만드세요.
    method saveBackup() is
        backup = editor.text

    // 편집기의 상태를 복원하세요.
    method undo() is
        editor.text = backup

    // 실행 메서드는 모든 구상 커맨드들이 자체 구현을 제공하도록 강제하기 위해
    // 추상으로 선언됩니다. 이 메서드는 커맨드가 편집기의 상태를 변경하는지에 따라
    // 진실 또는 거짓을 반환해야 합니다.
    abstract method execute()


// 구상 커맨드들은 여기에 배치됩니다.
class CopyCommand extends Command is
    // 복사 커맨드는 편집기의 상태를 변경하지 않으므로 기록에 저장되지 않습니다.
    method execute() is
        app.clipboard = editor.getSelection()
        return false

class CutCommand extends Command is
    // cut 커맨드는 편집기의 상태를 변경하므로 기록에 반드시 저장되어야 하며,
    // 메서드가 true를 반환하는 한 저장됩니다.
    method execute() is
        saveBackup()
        app.clipboard = editor.getSelection()
        editor.deleteSelection()
        return true

class PasteCommand extends Command is
    method execute() is
        saveBackup()
        editor.replaceSelection(app.clipboard)
        return true

// 실행취소 작업도 커맨드입니다.
class UndoCommand extends Command is
    method execute() is
        app.undo()
        return false


// 글로벌 커맨드 기록도 스택일 뿐입니다.
class CommandHistory is
    private field history: array of Command

    // 후입 …
    method push(c: Command) is
        // 커맨드를 기록 배열의 끝으로 푸시하세요.

    // … 선출
    method pop():Command is
        // 기록에서 가장 최근 명령을 가져오세요.


// 편집기 클래스에는 실제 텍스트 편집 기능이 있습니다. 이는 수신기의 역할을
// 합니다. 모든 커맨드들은 결국 편집기의 메서드들에 실행을 위임합니다.
class Editor is
    field text: string

    method getSelection() is
        // 선택된 텍스트를 반환하세요.

    method deleteSelection() is
        // 선택된 텍스트를 삭제하세요.

    method replaceSelection(text) is
        // 현재 위치에 클립보드의 내용을 삽입하세요.


// 앱 클래스는 객체 관계들을 설정하며 발신자 역할을 합니다. 이것은 무언가를
// 수행해야 할 때 커맨드 객체를 만들고 실행합니다.
class Application is
    field clipboard: string
    field editors: array of Editors
    field activeEditor: Editor
    field history: CommandHistory

    // 사용자 인터페이스 객체들에 커맨드들을 할당하는 코드는 다음과 같을 수
    // 있습니다.
    method createUI() is
        // …
        copy = function() { executeCommand(
            new CopyCommand(this, activeEditor)) }
        copyButton.setCommand(copy)
        shortcuts.onKeyPress("Ctrl+C", copy)

        cut = function() { executeCommand(
            new CutCommand(this, activeEditor)) }
        cutButton.setCommand(cut)
        shortcuts.onKeyPress("Ctrl+X", cut)

        paste = function() { executeCommand(
            new PasteCommand(this, activeEditor)) }
        pasteButton.setCommand(paste)
        shortcuts.onKeyPress("Ctrl+V", paste)

        undo = function() { executeCommand(
            new UndoCommand(this, activeEditor)) }
        undoButton.setCommand(undo)
        shortcuts.onKeyPress("Ctrl+Z", undo)

    // 커맨드를 실행하여 기록에 추가해야 하는지 확인하세요.
    method executeCommand(command) is
        if (command.execute())
            history.push(command)

    // 기록에서 가장 최근의 커맨드를 가져와서 그의 실행 취소 메서드를 실행하세요.
    // 참고로 우리는 이 커맨드의 클래스를 알지 못한다는 사실에 유념하세요.
    // 하지만 몰라도 상관없는데, 그 이유는 커맨드가 자신의 작업을 실행 취소하는
    // 법을 알기 때문입니다.
    method undo() is
        command = history.pop()
        if (command != null)
            command.undo()

적용

작업들로 객체를 매개변수화하려는 경우 커맨드 패턴을 사용하세요.

커맨드 패턴은 특정 메서드 호출을 독립실행형 객체로 전환할 수 있습니다. 이 변경을 통해 당신은 이제 커맨드들을 메서드 인수들로 전달하고, 이들을 다른 객체들의 내부에 저장하고, 런타임에 연결된 커맨드를 전환하는 등의 여러 흥미로운 작업을 진행할 수 있습니다.

예를 들어 당신은 콘텍스트 메뉴​(상황에 맞는 메뉴)​와 같은 그래픽 사용자 인터페이스 컴포넌트를 개발 중이고, 앱의 사용자들이 최종 사용자가 하나의 항목을 클릭했을 때 작업이 시작되는 메뉴 항목들을 설정할 수 있도록 만들고 싶어 합니다.

커맨드 패턴은 작업들의 실행을 예약하거나, 작업들을 대기열에 넣거나 작업들을 원격으로 실행하려는 경우에 사용하세요.

다른 여느 객체와 마찬가지로 커맨드는 직렬화될 수 있습니다. 직렬화는 파일이나 데이터베이스에 쉽게 쓸 수 있는 문자열로 변환하는 행위입니다. 나중에 이 문자열은 초기 커맨드 객체로 복원될 수 있습니다. 따라서 커맨드의 실행을 지연하고 예약할 수 있습니다. 또 같은 방식으로 네트워크를 통해 커맨드를 대기열에 추가하거나 로그​(기록) 하거나 전송할 수 있습니다.

커맨드 패턴은 되돌릴 수 있는 작업을 구현하려고 할 때 사용하세요.

실행 취소/다시 실행을 구현하는 방법에는 여러 가지가 있지만, 커맨드 패턴이 아마도 가장 많이 사용되는 패턴일 것입니다.

작업을 되돌리려면 수행한 작업의 기록을 구현해야 합니다. 커맨드 기록은 앱 상태의 관련 백업들과 함께 실행된 모든 커맨드 객체들을 포함하는 스택입니다.

이 메서드에는 두 가지 단점이 있습니다. 첫째, 앱 일부가 비공개일 수 있으므로 앱의 상태를 저장하는 것이 쉽지 않습니다. 이 문제는 메멘토 패턴으로 완화할 수 있습니다.

둘째, 상태 백업들은 상당히 많은 RAM을 소모할 수 있습니다. 따라서 때로는 대안적 구현에 의존할 수 있습니다. 예를 들어 과거 상태를 복원하는 대신 커맨드가 작업을 역으로 수행할 수 있습니다. 하지만 역으로 작업을 수행하는데도 대가가 있습니다. 구현하기 어렵거나 심지어 불가능할 수도 있습니다.

구현방법

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

  2. 요청들을 커맨드 인터페이스를 구현하는 구상 커맨드 클래스들로 추출하기 시작하세요. 각 클래스에는 실제 수신자 객체에 대한 참조와 함께 요청 인수들을 저장하기 위한 필드들의 집합이 있어야 합니다. 이러한 모든 값은 커맨드의 생성자를 통해 초기화되어야 합니다.

  3. 역할을 할 클래스들을 식별하세요. 이러한 클래스들에 커맨드들을 저장하기 위한 필드들을 추가하세요. 발송자들은 커맨드 인터페이스를 통해서만 커맨드들과 통신해야 합니다. 발송자들은 일반적으로 자체적으로 커맨드 객체들을 생성하지 않고 클라이언트 코드에서 가져옵니다.

  4. 수신자에게 직접 요청을 보내는 대신 커맨드를 실행하도록 발송자들을 변경하세요.

  5. 클라이언트는 다음 순서로 객체들을 초기화해야 합니다.

    • 수신자들을 만드세요.
    • 커맨드들을 만들고 필요한 경우 수신자들과 연관시키세요.
    • 발송자들을 만들고 특정 커맨드들과 연관시키세요.

장단점

  • . 작업을 호출하는 클래스들을 이러한 작업을 수행하는 클래스들로부터 분리할 수 있습니다.
  • / . 기존 클라이언트 코드를 손상하지 않고 앱에 새 커맨드들을 도입할 수 있습니다.
  • 실행 취소/다시 실행을 구현할 수 있습니다.
  • 작업들의 지연된 실행을 구현할 수 있습니다.
  • 간단한 커맨드들의 집합을 복잡한 커맨드로 조합할 수 있습니다.
  • 발송자와 수신자 사이에 완전히 새로운 레이어를 도입하기 때문에 코드가 더 복잡해질 수 있습니다.

다른 패턴과의 관계

  • 커맨드, 중재자, 옵서버책임 연쇄 패턴은 요청의 발신자와 수신자를 연결하는 다양한 방법을 다룹니다.

    • 패턴은 잠재적 수신자의 동적 체인을 따라 수신자 중 하나에 의해 요청이 처리될 때까지 요청을 순차적으로 전달합니다.
    • 패턴은 발신자와 수신자 간의 단방향 연결을 설립합니다.
    • 패턴은 발신자와 수신자 간의 직접 연결을 제거하여 그들이 중재자 객체를 통해 간접적으로 통신하도록 강제합니다.
    • 패턴은 수신자들이 요청들의 수신을 동적으로 구독 및 구독 취소할 수 있도록 합니다.
  • 책임 연쇄 패턴의 핸들러들은 커맨드로 구현할 수 있습니다. 그러면 당신은 많은 다양한 작업을 같은 콘텍스트 객체에 대해 실행할 수 있으며, 해당 콘텍스트 객체는 요청의 역할을 합니다. 여기에서의 요청은 처리 메서드의 매개변수를 의미합니다.

    그러나 요청 자체가 객체인 다른 접근 방식이 있습니다. 이 접근 방식을 사용하면 당신은 같은 작업을 체인에 연결된 일련의 서로 다른 콘텍스트들에서 실행할 수 있습니다.

  • 당신은 '실행 취소'를 구현할 때 커맨드메멘토 패턴을 함께 사용할 수 있습니다. 그러면 커맨드들은 대상 객체에 대해 다양한 작업을 수행하는 역할을 맡습니다. 반면, 메멘토들은 커맨드가 실행되기 직전에 해당 객체의 상태를 저장합니다.

  • 커맨드전략 패턴은 비슷해 보일 수 있습니다. 왜냐하면 둘 다 어떤 작업으로 객체를 매개변수화하는 데 사용할 수 있기 때문입니다. 그러나 이 둘의 의도는 매우 다릅니다.

    • 당신은 를 사용하여 모든 작업을 객체로 변환할 수 있습니다. 작업의 매개변수들은 해당 객체의 필드들이 됩니다. 이 변환은 작업의 실행을 연기하고, 해당 작업을 대기열에 넣고, 커맨드들의 기록을 저장한 후 해당 커맨드들을 원격 서비스에 보내는 등의 작업을 가능하게 합니다.

    • 반면에 은 일반적으로 같은 작업을 수행하는 다양한 방법을 설명하므로 단일 콘텍스트 클래스 내에서 이러한 알고리즘들을 교환할 수 있도록 합니다.

  • 프로토타입커맨드 패턴의 복사본들을 기록에 저장해야 할 때 도움이 될 수 있습니다.

  • 비지터 패턴은 커맨드 패턴의 강력한 버전으로 취급할 수 있습니다. 비지터 패턴의 객체들은 다른 클래스들의 다양한 객체에 대한 작업을 실행할 수 있습니다.

코드 예시

C#으로 작성된 커맨드 C++로 작성된 커맨드 Go로 작성된 커맨드 자바로 작성된 커맨드 PHP로 작성된 커맨드 파이썬으로 작성된 커맨드 루비로 작성된 커맨드 러스트로 작성된 커맨드 스위프트로 작성된 커맨드 타입스크립트로 작성된 커맨드