클린 소프트웨어 - 커맨드와 액티브 오브젝트 패턴

커맨드와 액티브 오브젝트 패턴

커맨드command 패턴은 가장 단순하면서도 세련된 패턴이다.

protocol Command {
func do()
}

대부분의 클래스는 프로퍼티와 메소드의 집합으로 이루어져 있는데, 커맨드 패턴은 오히려 함수를 캡슐화하여 프로퍼티에서 자유롭게 한다.

단순한 커맨드 패턴 적용

class RelayOnCommand: Command {
func do() { ... }
}

class MotorOnCommand: Command {
func do() { ... }
}

class ClutchOnCommand: Command {
func do() { ... }
}

...

위와 같은 구조를 잘 만들어 낸다면, Command 객체를 시스템에 넘겨주는 것으로 작업을 수행할 수 있다. Command 객체가 무엇을 표현하는지 알 필요가 없이 do() 메소드를 실행할 수 있다.

위의 구체 Command 클래스를 사용하는 클라이언트는 자신이 하는 일을 모른다. 그저 Commanddo() 메소드를 호출할 뿐이다. 즉 클라이언트는 구체적인 것에 대해 알 필요가 없다.

커맨드 패턴은 명령의 개념을 캡슐화하여 시스템의 논리적인 상호 연결을 분리해낼 수 있게 한다.

트랜잭션

커맨드 패턴은 트랜잭션의 생성과 실행에 대해 유용하게 사용될 수 있다. 예를 들어 직원 데이터베이스를 관리하는 시스템의 사용자들은 이 데이터베이스를 사용하여 새 직원을 추가하고, 기존 직원을 삭제하고, 직원의 속성을 변경할 수 있을 것이다.

protocol Transaction {
func validate()
func execute()
}

class AddEmployeeTransaction: Transaction {
private var name: String
private var address: String

func validate() { ... }
func execute() { ... }
}

Transaction 인터페이스를 작성하여 커맨드 패턴을 실현한다.

  • validate() 커맨드는 모든 데이터를 살펴보고 유효한지 확인한다.
  • execute() 커맨드는 검증된 데이터를 사용하여 데이터베이스를 갱신한다.

물리적, 시간적 분리

위의 방식을 사용하여, 사용자에게서 데이터를 받는 코드와, 그 데이터를 검증하고 그것으로 작업을 하는 코드와, 업무 객체 그 자체를 분리할 수 있다.

예를 들어 GUI를 통해 데이터를 받을 때, 이 데이터가 유효한지 확인하고 데이터베이스 갱신 작업을 실행하는 코드가 GUI에 위치한다면, GUI와 로직이 결합되어 다른 인터페이스가 해당 로직을 사용할 수 없게 만들 것이다.

시간적 분리

커맨드를 입력하고, 그 시점에 커맨드가 유효한지 검증한 후, 나중에 작업을 수행할 수 있도록 할 수 있다.

되돌리기

protocol Command {
func do()
func undo()
}

Command 인터페이스의 파생 클래스가 do() 메소드가 수행하는 동작을 기억할 수 있다면, undo() 메소드가 그 동작을 되돌리도록 구현할 수 있을 것이다.

명령을 취소하는 방법을 아는 코드는 항상 그 명령을 수행하는 방법을 아는 코드와 함께 있어야 한다.

액티브 오브젝트 패턴

액티브 오브젝트 패턴은 커맨드 패턴의 응용 중 하나이며, 다중 제어 스레드 구현을 위한 기법이다.

class ActiveObjectEngine {
var commands = [Command]()

func add(_ command: Command) {
commands.append(command)
}

func run() throws {
while !commands.isEmpty {
let command = commands.removeFirst()
try command.execute()
}
}
}
protocol Command {
func execute() throws
}

ActiveObjectEngine은 액티브 오브젝트 패턴을 구현하기 위한 클래스다. 내부에 커맨드 배열을 저장하고, 커맨드를 추가하거나 저장한 커맨드를 계속 소비하는 메소드를 포함한다.

commands 배열에 있는 요소 중 하나가 자신을 복제하여 그 복제본을 배열에 다시 넣는다면, run() 메소드는 절대 끝나지 않을 것이다.

class SleepCommand: Command {
private var wakeUpCommand: Command!
private var engine: ActiveObjectEngine!
private var sleepTime = 0.0
private var startTime = 0.0
private var isStarted = false

init(milliSeconds: Double, engine: ActiveObjectEngine, wakeupCommand: Command) {
sleepTime = milliseconds
self.engine = engine
self.wakeupCommand = wakeupCommand
}

func execute() throws {
let currentTime = CFAbsoluteTimeGetCurrent()
if !isStarted {
isStarted = true
startTime = currentTime
engine.add(self)
} else if currentTime - startTime < sleepTime {
engine.add(self)
} else {
engine.add(wakeupCommand)
}
}
}

Command 인터페이스를 구현하는 SleepCommand는 다음과 같이 동작한다.

  • 실행이 되면, 자신이 이전에 실행된 적이 있는지 확인한다. 실행된 적이 없다면 시작 시간을 기록한다.
  • 지연 시간이 지나지 않았다면 자신을 다시 ActiveObjectEngine에 넣는다.
  • 지연 시간이 지났다면 ActiveObjectEnginewakeupCommand 명령을 넣는다.

이러한 기법을 다양하게 변형하여 멀티스레드 시스템을 구축할 수 있다. 각 Command 인스턴스가 다음 Command 인스턴스의 실행이 가능해지기 전에 완료되기 때문에, RTCrun-to-completion 태스크라는 이름으로 알려져 있다. 이는 Command 인스턴스가 블록을 하지 않는다는 의미를 내포한다.

각 RTC 스레드에 대해 별도의 런타임 스택을 정의하거나 할당할 필요가 없어, 많은 스레드가 실행되고 메모리가 제한된 시스템에서 강력한 이점이 될 수 있다.

결론

커맨드 패턴은 데이터베이스 트랜잭션, 장치 제어, 멀티스레드 시스템의 핵심, GUI에서의 실행 및 실행 취소 관리 등 매우 다양한 용도에서 사용될 수 있다.

커맨드 패턴은 클래스보다 함수를 강조하여 객체 지향 패러다임을 망가뜨린다고 이야기되어 왔으나, 현장에서는 커맨드 패턴이 매우 유용하게 사용될 수 있다.


추가

커맨드 패턴이란 요청을 객체의 형태로 캡슐화하여 사용자가 보낸 요청을 나중에 이용할 수 있도록 메소드 이름, 매개변수 등 요청에 필요한 정보를 저장 또는 로깅, 취소할 수 있게 하는 패턴이다.

커맨드 패턴에는 명령command, 수신자receiver, 발동자invoker, 클라이언트client의 네 개의 용어가 항상 따른다.

  • 명령 객체는 수신자 객체를 가지고 있고, 수신자의 메소드를 호출한다.
  • 수신자 객체는 명령 객체의 호출에 따라 자신에게 정의된 메소드를 수행한다.
  • 발동자 객체는 명령 객체를 발동하게 한다. 명령 발동에 대한 기록을 남길 수 있다. 하나의 발동자 객체에 여러 개의 명령 객체가 전달될 수 있다.
  • 클라이언트 객체는 발동자 객체와 하나 이상의 명령 객체를 보유한다. 어느 시점에서 어떤 명령을 수행할지 결정한다.
  • 명령을 수행하기 위해 클라이언트 객체는 발동자 객체로 명령 객체를 전달한다.
// Invoker
class Switch {
private let flipUpCommand: Command
private let flipDownCommand: Command

init(flipUpCommand: Command, flipDownCommand: Command) {
self.flipUpCommand = flipUpCommand
self.flipDownCommand = flipDownCommand
}

func flipUp() {
flipUpCommand.execute()
}

func flipDown() {
flipDownCommand.execute()
}
}

// Receiver
class Light {
func turnOn() {
print("The light is on")
}

func turnOff() {
print("The light is off")
}
}

// Command
protocol Command {
func execute()
}

// Concrete Command : turn on
class TurnOnLightCommand: Command {
private let light: Light

init(light: Light) {
self.light = light
}

func execute() {
light.turnOn()
}
}

// Concrete Command : turn off
class TurnOffLightCommand: Command {
private let light: Light

init(light: Light) {
self.light = light
}

func execute() {
light.turnOff()
}
}

// Client
let light = Light()
let switchUpCommand = TurnOnLightCommand(light: light)
let switchDownCommand = TurnOffLightCommand(light: light)
let switch = Switch(flipUpCommand: switchUpCommand, flipDownCommand: switchDownCommand)

switch.flipUp()
switch.flipDown()