Swift/Design_Pattern

[Swift 디자인 패턴] Command Pattern (커맨드) - 디자인 패턴 공부 15

Dev_Pingu 2021. 5. 25. 00:11
반응형

안녕하세요 Pingu입니다.🐧

 

지난 글에서는 행동 패턴 중 하나인 Chain of Responsibility Pattern(책임 연쇄 패턴)에 대해 알아봤었는데요, 이번 글에서는 또 다른 행동 패턴인 Command Pattern(커맨드)에 대해 알아보도록 하겠습니다.

 

커맨드 패턴이란?

Command Pattern은 요청을 하나의 객체로 캡슐화해서 이를 큐나 log로 처리하는 방법입니다. 이렇게 캡슐화한 작업은 실행 취소를 할 수도 있게 만들 수 있어요. OS에서 스케줄러가 작업들을 관리하거나, iOS에서 Operating Queue에 Operation 객체로 작업을 추가하는 예 들이 떠오르는 패턴입니다.

  • Command
    • 작업을 실행하기 위한 인터페이스를 정의합니다.
    • Command를 실행하기 위한 하나의 메서드로 이루어진 경우가 많습니다.
  • Concrete Command
    • Receiver 객체와 작업 사이 바인딩을 정의합니다.
    • Receiver에서 해당 작업을 호출하여 실행합니다.
    • 직접 작업을 수행하는 것이 아닌 Receiver에게 전달하기 위한 목적을 갖고 있습니다.
    • 코드를 단순화 하기 위해 클래스를 합칠 수도 있습니다.
  • Client
    • Concrete Command 객체를 만들고 Receiver를 설정합니다.
    • Receiver의 인스턴스를 포함한 작업에 필요한 모든 매개변수를 Command의 생성자에 전달하여 작업을 처리합니다.
  • Invoker
    • Invoker 클래스에는 명령 객체에 대한 참조를 저장하기 위한 필드가 있어야 합니다.
    • 클라이언트가 생성자를 통해 Invoker 객체를 만들 때 Command 객체를 받게 됩니다.
    • 요청을 Receiver에게 직접 보내는 것이 아닌 해당 요청의 시작을 담당합니다.
  • Receiver
    • 웬만한 클래스는 Receiver 역할을 할 수 있습니다.
    • 요청된 작업을 수행합니다.

커맨드 패턴은 언제 쓰나요?

커맨드 패턴은 작업을 캡슐화해서 Receiver에게 보내서 처리하고 싶을 때 사용하면 좋습니다.

 

예를 들어 컴퓨터를 할 때 자주 사용하는 복사, 붙여 넣기를 생각해볼게요. 어떤 메모장 앱에 복사, 붙여넣기 기능을 만들고 싶어서 버튼을 만들었습니다. 버튼 클래스를 만들고 버튼에 복사, 붙여넣기을 수행하는 메서드를 만들었죠. 하지만 사람들은 Ctrl + C, Ctrl + V와 같은 단축키로도 해당 기능을 쓰고 싶고, 다른 버튼에도 복사, 붙여 넣기 기능이 있었으면 좋겠다고 문의가 들어옵니다.

 

지금은 복사, 붙여넣기 버튼이 존재하는데 이럴 경우 동일한 기능을 하는 버튼이나 단축키를 만들기 위해서는 또 새로운 버튼을 만들어야 합니다. 이건 비효율적이므로 여기서 커맨드 패턴을 사용하면 됩니다. 

 

복사, 붙여 넣기를 처리해주는 Command 객체를 만드는 거죠. 버튼은 그냥 눌러지면 해당 명령을 실행하게만 합니다. 이렇게 하면 여러개의 복사, 붙여넣기 기능을 하는 버튼을 만들어도 Command 객체는 하나만 존재하게 되고, 이는 코드의 중복을 막아줍니다.

 

또한 다른 기능을 추가하고 싶다면 Command 객체만 더 만들어내면 됩니다. 그런 뒤 버튼을 눌렀을 때 해당 Command 객체만 전달하고 실행되도록 만들기만 하면 됩니다.

커맨드 패턴의 결과

장점

  • Single Responsibility Principle (단일 책임 원칙)을 만족합니다. Command 객체를 통해 작업을 수행하는 객체와 작업을 호출하는 객체를 나눌 수 있습니다.
  • Open / Closed Principle (개방 / 폐쇄 원칙)을 만족합니다. 클라이언트의 코드를 수정하지 않고도 새로운 Command를 추가할 수 있죠
  • 실행 취소, 다시 실행을 구현할 수 있습니다.
  • 작업의 시작을 지연시킬 수 있습니다.
  • 여러 개의 단순한 명령을 조합해서 복잡한 명령을 만들 수 있습니다.

단점

  • Receiver, Invoker 사이에 관계를 도입해야 하므로 코드가 복잡해질 수 있습니다.

예제

그럼 이제 Command Pattern을 Swift로 구현해보겠습니다.

아까 본 복사, 붙여 넣기 예제를 구현해볼게요.

아까 언급했던 대로 커맨드 패턴은 실행 취소, 다시 실행 등도 만들 수 있다고 했는데요, 그런 것 역시 만들어보도록 하겠습니다.

 

가장 먼저 Command 프로토콜을 정의합니다.

// Command
protocol Command {
    var receiver: TextEditor { get set }
    var backup: String { get set }
    
    func execute()
    func undo()
}

저는 Command를 실행도 하고 취소도 할 수 있게 만들기 위해 메서드를 2개 만들어줬습니다.

그리고 Command 객체는 receiver를 참조해야 한다고 했으니, 그런 필드도 하나 만들어줍니다.

 

다음으로 Invoker를 만들어보겠습니다.

// Invoker
class Invoker {
    var history: [Command] = []
    
    private func push(command: Command) {
        self.history.append(command)
    }
    
    private func pop() -> Command? {
        return history.popLast()
    }
    
    func executeCommand(command: Command) {
        command.execute()
        self.push(command: command)
    }
    
    func undoCommand() {
        let command = self.pop()
        if command == nil {
            print("되돌릴 작업이 없습니다.")
        } else {
            command?.undo()
        }
    }
}

Invoker는 Command를 수행하고, 되돌리는 걸 시작하는 역할만 합니다.

또한 실행 취소를 구현하기 위해 실행된 Command들을 순서대로 저장할 Stack을 하나 만들어줍니다.

저는 history라고 이름을 만들었습니다.

 

Receiver는 Command가 실제로 수행되는 곳으로 아래와 같이 만들어줬습니다.

// Receiver
class TextEditor {
    var text: String = ""
    var clipboard: String = ""
}

이제 Command 들만 구현하면 됩니다.

저는 복사, 붙여 넣기, 쓰기만 구현했습니다.

// Concrete Command
class CopyCommand: Command {
    var receiver: TextEditor
    var backup: String = ""
    
    init(receiver: TextEditor) {
        self.receiver = receiver
    }
    
    func undo() {
        receiver.clipboard = self.backup
        self.backup = ""
    }

    func execute() {
        self.backup = receiver.clipboard
        receiver.clipboard = receiver.text
    }
}
// Concrete Command
class PasteCommand: Command {
    var receiver: TextEditor
    var backup: String
    
    init(receiver: TextEditor) {
        self.receiver = receiver
        self.backup = self.receiver.clipboard
    }
    
    func undo() {
        let startIndex = receiver.text.startIndex
        let lastIndex = receiver.text.index(startIndex, offsetBy: receiver.text.count - backup.count)
        receiver.text = String(receiver.text[startIndex..<lastIndex])
    }
    
    func execute() {
        self.receiver.text += backup
    }
}
// Concrete Command
class WriteCommand: Command {
    var receiver: TextEditor
    var backup: String
    
    init(receiver: TextEditor, backup: String) {
        self.receiver = receiver
        self.backup = backup
    }
   
    func undo() {
        let startIndex = receiver.text.startIndex
        let lastIndex = receiver.text.index(startIndex, offsetBy: receiver.text.count - backup.count)
        receiver.text = String(receiver.text[startIndex..<lastIndex])
    }
    
    func execute() {
        receiver.text += backup
    }
}

필요한 건 모두 만들었으니 이제 실행해보겠습니다~

 

위의 결과를 보면 실행과 취소가 잘 수행되는 것을 볼 수 있습니다.

Command가 실행되면 receiver에서 실제로 실행되고, invoker에서 undo를 하면 가장 최근에 한 작업이 취소되는 것을 볼 수 있습니다.

 

 

이렇게 Command Pattern을 공부하고 실제로 구현도 해봤습니다.

책에 나온 개념을 최대한 활용하여 구현했는데, Invoker의 역할, Receiver의 역할, Command들의 생성 위치 등이 많이 고민됐습니다.

 

혹시라도 틀린 부분이 있다면 알려주시면 감사하겠습니다.

 

전체 코드는 여기에서 볼 수 있습니다.

 

감사합니다.

 

 

반응형