[디자인 패턴 16편] 행동 패턴, 커맨드 (Command)
1. 개념
커맨드 패턴은 특정 객체에 대한 커맨드를 객체화하여
이 커맨드 객체를 필요에 따라 처리하는 패턴이다.
가장 주의 깊게 볼 사항은 커맨드 자체를 객체화한다는 점이다.
보통 OOP 에서는 주체 객체와 대상 객체가 존재하고, 대상 객체에 대한 액션은 주체 객체에서의 메쏘드로 처리하는데,
이 액션까지 객체로 만든 뒤에 처리하겠다는 말이다.
처음엔 이렇게만 봐서 이해가 절대 안 간다. 자세한 내용은 차차 설명하겠다.
1.1. 첫 번째 예시
아무래도 커맨드 패턴은 조금 복잡해서 처음에 적절한 예시가 필요한 거 같다.
가장 많이 사용되는 '손님 - 웨이터 - 주방장' 예시를 들어보려고 한다.
다음과 같이 가게에 손님이 음식을 요청하는 상황을 생각해보자.
어떤 식당에는 주방장과 웨이터가 있다.
이 식당에 어떤 손님이 들어와, 주문서에 스파게티를 작성하고 이를 웨이터에게 전달했다.
웨이터는 이 주문서를 읽고 주방장에게 스파게티 요리를 요청했다.
상황을 단계별로 하나하나 구체적으로 적어보았다.
먼저 상황에 참여하는 주체를 살펴보면 다음과 같다. 이는 모두 객체로 추상화시킬 수 있는 개념들이다.
- 손님 (Client)
- 어떤 음식을 요청한다.
- 웨이터 (Invoker)
- 음식 요청을 주방장에게 전달한다.
- 주방장 (Recevier)
- 요청받은 것을 수행한다.
주체는 아니지만, 객체로 추상화 시킬 수 있는 개념이 하나 더 있다.
- 주문 (Command)
- 어떤 주문을 할 것인지에 대한 정보가 있다.
이제 이를 코드로 하나씩 구현해보자.
먼저, 식당에는 주방장과 웨이터가 있다고 했다.
주방장과 웨이터를 추상화하면 다음과 같다.
class Chef:
"""요리에 대한 액션을 하는 주체"""
def __init__(self, name):
self.name = name
def cook_spaghetti(self):
print(f"{self.name}가 스파게티를 요리합니다.")
pass
def cook_pizza(self):
print(f"{self.name}가 피자를 요리합니다.")
pass
class Waiter:
"""주문을 받거나 주문을 실행하는 주체"""
def __init__(self):
self.orders = []
def add_order(self, order: Order) -> None:
"""고객의 주문을 주문서에 적는다."""
self.orders.append(order)
def execute_orders(self):
"""주문서에 적힌 주문을 실행한다."""
for order in self.orders:
order.execute()
self.orders = []
그다음으로, 주문을 추상화해보자.
구체적인 주문에는 어떤 "셰프" 가 어떤 "음식" 을 해야 하는지 적혀있다고 하자.
다음과 같이 추상 클래스를 정의하고, 구체적인 클래스를 정의할 수 있다.
class Order(metaclass=ABCMeta):
@abstractmethod
def execute(self):
pass
class SpaghettiOrder(Order):
def __init__(self, chef: Chef) -> None:
self.chef = chef
def execute(self):
self.chef.cook_spaghetti()
class PizzaOrder(Order):
def __init__(self, chef: Chef) -> None:
self.chef = chef
def execute(self):
self.chef.cook_pizza()
주문서 안에는 execute(...)
라는 주문에 대한 액션도 포함한다.
또 스파게티에 대한 주문인지, 피자에 대한 주문인지 명확하게 분리해놓았다.
각 주문이 실행될 때 실행되는 로직도 조금 다르다.
왜 이렇게 짰는지 의문을 갖지 말고 일단은 이 플로우를 따라가 보자.
이제 손님이 들어와서 요리를 주문하는 상황을 코드로 구현해보자.
# 처음에 가게에 있는 요리사와 웨이터
chef = Chef("흠시")
waiter = Waiter()
# 고객(클라이언트)는 주문서를 작성하고, 웨이터에게 건낸다.
order = SpaghettiOrder(chef=chef) # 흠시 주방장에게 스파게티를 요청하는 주문서
waiter.add_order(order)
# 주문을 다 받은 웨이터는 주문을 실행한다.
waiter.execute_orders()
# 이후 더 배가 고파진 고객은 주문서를 또 작성하고, 웨이터에게 건낸다.
order = PizzaOrder(chef=chef) # 흠시 주방장에게 피자를 요청하는 주문서
waiter.add_order(order)
# 주문을 다 받은 웨이터는 주문을 실행한다.
waiter.execute_orders()
실행 결과는 다음과 같다.
흠시가 스파게티를 요리합니다.
흠시가 피자를 요리합니다.
이제, 각 객체의 역할을 정리하면 다음과 같다.
- 요청을 하는 사람 (Client) = 손님
- 그 요청을 받아서 요청을 수행시키는 사람 (Invoker) = 웨이터
- 요청을 수행하는 사람 (Receiver) = 주방장
- 요청 그 자체 (Command) = 주문서
이와 같이 역할을 나누어 설계한 패턴을 커맨드 패턴이라고 한다.
특히 요청 그 자체 (Command) 를 주체 객체 마냥 설계해놓았기 때문에, 좀 더 고도화된 캡슐화라고 할 수 있겠다.
1.2. 구조
이번엔 전반적인 구조를 통해 다시 한번 살펴보자.
- Client
- 말 그대로 명령을 내릴 사용자다.
- 사용자는 Recevier 와 ConcreteCommand 만 알고 있으면 된다.
- 즉, 누가 무엇을 할지에 대해서만 알고 있으면 된다.
- 이때 '누가' 는 Recevier, '무엇을 할지' 는 Command 가 된다.
- Recevier
- 실제로 Client 의 원하는 요청을 수행하는 객체다.
- 누가 언제 요청해왔는지 몰라도 된다. 액션에 대해서만 관심(구현)을 가지면 된다.
- 위 Client 에서 '누가' 에 해당하는 객체다.
- Invoker
- Client 의 요청을 받아 실제로 Receiver 의 액션을 호출하는 객체다.
- 흐름 상 Client 의 요청을 Receiver 에게 전달해준다.
- Client 는 Invoker 를 거쳐 Receiver 에게 요청하기 때문에,
이 Invoker 에서 Receiver 에 도달하기 전, 요청에 대한 이런저런 설정들을 할 수가 있다.
- Command
- ConcreteCommand 의 추상 클래스
execute()
를 공통 인터페이스로 둔다.
- ConcreteCommand
- 실제 구체적인 요청 내용을 담는 클래스
- 누가(Receiver) 가 무엇을 해야하는지(
execute()
내 로직) 를 담는다.
1.3. 두 번째 예시
이 패턴을 왜 쓸까?
위에는 간단한 예시라, 이 패턴을 이해하기는 좋았지만 왜 쓰는지에 대한 유용성은 잘 못 느껴졌을 거 같다.
그래서 예시 하나를 더 들어보려고 한다.
goodGid님 블로그를 참고했다.
내 방 책상 위에는 전등과 음악 플레이어가 있다.
전등과 음악 플레이어 각각의 ON / OFF 명령을 하나의 리모콘에 담고 싶다.
1~9 까지의 버튼을 가지는 리모콘에 이러한 기능을 설계해보자
실제로 ON, OFF 를 수행하는 주체는 전등과 음악 플레이어다. 이들은 리시버의 역할을 하게 된다.
이러한 ON, OFF 명령어를 입력받아 전달하는 주체는 리모콘이다. 인보커의 역할을 하게 된다.
각 주체에 대한 ON, OFF 명령은 그 자체로 객체가 된다. 커맨드의 역할을 하게 된다.
이제 이를 코드로 구현해보자.
먼저, 전등과 음악 플레이어를 객체화하면 다음과 같다.
class Light:
def __init__(self, location: str):
self.location = location
def on(self):
print(f"{self.location} 전등 켜짐")
def off(self):
print(f"{self.location} 전등 꺼짐")
class MusicPlayer:
def __init__(self, location: str):
self.location = location
def on(self):
print(f"{self.location} 음악 플레이어 켜짐")
def off(self):
print(f"{self.location} 음악 플레이어 꺼짐")
이번엔 각 리시버들에게 전달되는 명령을 객체화해보자.
class Command(metaclass=ABCMeta):
@abstractmethod
def execute(self):
pass
class LightOnCommand(Command):
def __init__(self, light: Light):
self.light = light
def execute(self):
self.light.on()
def undo(self):
self.music_player.off()
class LightOffCommand(Command):
def __init__(self, light: Light):
self.light = light
def execute(self):
self.light.off()
def undo(self):
self.music_player.off()
class MusicPlayerOnCommand(Command):
def __init__(self, music_player: MusicPlayer):
self.music_player = music_player
def execute(self):
self.music_player.on()
def undo(self):
self.music_player.off()
class MusicPlayerOffCommand(Command):
def __init__(self, music_player: MusicPlayer):
self.music_player = music_player
def execute(self):
self.music_player.off()
def undo(self):
self.music_player.on()
이번에는 execute()
뿐만 아니라, 실행 취소의 기능인 undo()
도 추가하였다.
이제 인보커 역할을 하는 리모콘을 객체화해보자.
class RemoteControl:
def __init__(self):
self.buttons = [None] * 10
self.last_command = None
def setCommand(self, index: int, command: Command):
self.buttons[index] = command
def pressButton(self, index):
self.buttons[index].execute()
self.last_command = self.buttons[index]
def pressUndoButton(self):
self.last_command.undo()
이제 클라이언트는 다음과 같이 사용할 수 있다.
# 책상 위에 전등과 음악 플레이어가 있다.
light = Light("책상 위")
music_player = MusicPlayer("책상 위")
# 리모콘에 명령어 세팅을 한다.
remote_control = RemoteControl()
remote_control.setCommand(0, LightOnCommand(light))
remote_control.setCommand(1, LightOffCommand(light))
remote_control.setCommand(2, MusicPlayerOnCommand(music_player))
remote_control.setCommand(3, MusicPlayerOffCommand(music_player))
# 사용자가 각 버튼을 순서대로 누른다.
remote_control.pressButton(0) # 책상 위 전등 켜짐
remote_control.pressButton(2) # 책상 위 음악 플레이어 켜짐
remote_control.pressButton(1) # 책상 위 전등 꺼짐
remote_control.pressButton(3) # 책상 위 음악 플레이어 꺼짐
# 마지막 버튼 실행 취소를 한다.
remote_control.pressUndoButton() # 책상 위 음악 플레이어 켜짐
이번 예시를 통해 커맨드 패턴을 좀 더 잘 이해했을 거라고 생각한다.
당장 느껴지는 유용함이라면
- 명령어를 인보커에 언제든지 등록할 수 있고, 원하는 언제든지 인보커로 간단하게 실행시킬 수 있다는 것이다.
- 또한, 추후 명령이나 리시버가 더 추가될 경우 기존 코드에 수정 없이, 두 객체를 만들고
setCommand()
로 추가해주면 된다. 즉 OCP 원칙을 잘 지켜낸다.
1.4. 장점
- 작업을 수행하는 객체(리시버)와 작업을 요청하는 객체를 분리하기 때문에 SRP 원칙을 잘 지켜낸다.
- 기존 코드 수정 없이 새로운 리시버, 명령어 추가가 가능하기 때문에 OCP 원칙을 잘 지켜낸다.
- 커맨드 단위의 별도의 액션(undo, redo) 등이 가능하고, 커맨드 상속 및 조합을 통해 더 정교한 커맨드를 구현할 수 있다.
1.5. 단점
- 전체적으로 이해가 필요하고 복잡한 설계구조를 가진다.
1.6. 활용 상황
- 커스텀 리모콘의 예시처럼, 커맨드 발생 시점을 사용자가 커스터마이징 해야 하는 경우
- 여러 커맨드를 조합하여 하나의 커맨드처럼 사용할 필요가 있는 경우
- 커맨드 실행 취소, 재 실행 등의 기능을 구현해야 하는 경우
2. 구현
클라이언트는 다음과 같이 사용할 수 있다.
recevier = Receiver()
recevier_operation_a = RecevierOperationA(recevier)
recevier_operation_b = RecevierOperationB(recevier)
invoker = Invoker()
invoker.setCommand(recevier_operation_a)
invoker.doCommand()
invoker.setCommand(recevier_operation_b)
invoker.doCommand()
Receiver 는 다음과 같이 구현할 수 있다.
class Recevier:
def operation_A(self):
pass
def operation_B(self):
pass
Command 는 다음과 같이 구현할 수 있다.
class Command(metaclass=ABCMeta):
@abstractmethod
def execute(self):
pass
class RecevierOperationA(Command):
def __init__(self, recevier: Recevier):
self.recevier = recevier
def execute(self):
self.recevier.operation_A()
class RecevierOperationB(Command):
def __init__(self, recevier: Recevier):
self.recevier = recevier
def execute(self):
self.recevier.operation_B()
Invoker 는 다음과 같이 구현할 수 있다.
class Invoker:
def __init__(self):
self.commands = []
def setCommand(self, command:Command):
self.commands.append(command)
def doCommand(self, index: int = -1):
self.commands[index].execute()