애플사이다의 iOS 개발 일지

[디자인 패턴] Command - 실행할 작업을 덩어리로 관리할 때 본문

프로그래밍 철학

[디자인 패턴] Command - 실행할 작업을 덩어리로 관리할 때

Applecider 2023. 7. 2. 22:51

GoF 디자인 패턴 중 하나인 Command 패턴을 정리했다.

- Ref : 도서 <Dive into Design Patterns>, Alexander Shvets 저


문제 상황

  • Text Editor 프로그램을 개발하는 중이며, 텍스트를 저장할 Save Button을 만들었다.
    Button을 상속받아 자식클래스로 Save Button을 구현한 뒤, save 기능을 추가했다.
  • 그런데 Save Button 뿐만 아니라 Ctrl+C 단축키로도 복사를 하고 싶고,
    텍스트와 서식을 동시에 복사할 Super Save Button을 추가하고 싶어졌다.
  • 기존 방식으로 하면 버튼/단축키에 중복 코드가 생기고, 자식클래스 종류가 너무 많아질 수 있다는 문제점이 있다.

패턴 설명

  • Command의 사전적 정의는 “명령, 명령어”이다.
    Command 패턴에서는 실행할 작업을 캡슐화하여 하나의 덩어리로 취급한다.

    즉, 실행할 명령 (ex. SaveCommand)을 별도 타입으로 정의해 두고,
    필요한 곳 (ex. SaveButton, SaveShortcut)에 해당 명령 덩어리를 주입시켜서 활용한다.
  • 관심사 분리의 원칙에 따라 UI 로직비즈니스 로직을 분리하는게 좋다.
    Command 객체는 UI 객체와 비즈니스 로직 객체 사이의 연결고리 역할을 한다.
    덕분에 UI 객체가 비즈니스 로직에게 직접 작업을 요청하지 않는다.
  • Command 패턴은 아래 5가지 역할로 구성된다.
    언뜻 복잡해보이지만 예시코드를 먼저 보면 쉽게 이해할 수 있다.

Command 패턴의 구조

  1. Invoker (발송자) : 커맨드에게 일을 시킨다. 커맨드 객체의 참조를 저장할 프로퍼티를 가진다.
  2. Command 인터페이스 : 단일 메서드 execute()를 선언한다.
  3. Concrete Command : 일을 구현한다. Concrete Command를 초기화할 때,
    Receiver 자체 (또는 Receiver의 메서드를 실행하는 데 필요한 매개변수)를 전달받아 프로퍼티로 가진다.
  4. Receiver (수신자) : 실제로 작업이 실행되는 곳이다.
  5. Client : Concrete Command를 초기화한다.

예시 코드

// Command
protocol Command {
    func execute()
}

// Concrete Command
class SaveCommand: Command {
    var textEditor: TextEditor
    
    init(textEditor: TextEditor) {
        self.textEditor = textEditor
    }
    
    func execute() {
        // 복사 기능을 구현
        let currentText = textEditor.text
        textEditor.clipboard = currentText
        print("Text Copied!")
    }
}

// Invoker - 커맨드에게 일을 시킴
class SaveButton {
    var command: Command? // 커맨드를 저장
    
    func setCommand(_ command: Command) {
        self.command = command
    }
    
    func executeCommand() {
        command?.execute()
    }
}
class SaveShortcut {
    var command: Command?

    func setCommand(_ command: Command) {
        self.command = command
    }

    func executeCommand() {
        command?.execute()
    }
}

// Receiver - 실제로 작업이 실행되는 곳
class TextEditor {
    // 가정 : 복사 기능이 실행되면 -> text를 clipboard에 저장함
    var text: String = ""
    var clipboard: String = ""
}

// Client
let textEditor = TextEditor() // receiver

let saveCommand = SaveCommand(textEditor: textEditor)

let saveButton = SaveButton() // invoker-1
saveButton.setCommand(saveCommand)
saveButton.executeCommand() // Text Copied! 출력

let saveShortcut = SaveShortcut() // invoker-2
saveShortcut.setCommand(saveCommand) // 재사용 가능
saveShortcut.executeCommand() // Text Copied! 출력

 

장점

  • 단일책임 원칙을 준수한다. Receiver (TextEditor)와 Invoker (SaveButton)의 결합도가 줄어든다.
  • 개방폐쇄 원칙을 준수한다. client 코드를 변경하지 않고도 새로운 Command를 추가할 수 있다.
  • Redo/Undo 기능, 작업 예약 기능을 쉽게 구현할 수 있다.
    • SaveCommand 외에도 PasteCommand, WriteCommand 등을 만들고,
      작업을 실행할 때마다 Command Stack에 쌓아서 히스토리를 관리하면 된다.
    • 일부 데이터의 접근이 제한된 경우 (private) Memento 패턴으로 대체 가능하다.
  • 간단한 Command를 조합해서 복잡한 Command를 만들어낼 수 있다.

단점

  • 코드 복잡도가 높아진다.

 

- Reference

 

🍎 포스트가 도움이 되었다면, 공감🤍 / 구독🍹 / 공유🔗 / 댓글✏️ 로 응원해주세요. 감사합니다.

Comments