본문 바로가기

디자인 패턴

5. 커맨드 패턴 (Command Pattern)

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

이번에 알아볼 건 커맨드 패턴이다.

 

커맨드 패턴은 지금까지 해온 패턴과는 달리 어느 정도 룰과 구성요소가 정해져 있다.

 

  • 리시버 (Receiver) : 기능을 수행할 대상
  • 커맨드 (Command) : 리시버를 제어하는 객체
  • 인보커 (Invoker) : 커맨드들을 제어하는 객체
  • 클라이언트 (Client) : 인보커를 제어하는 객체

위와 같이 총 4가지로 구성되어 커맨드 패턴을 구현하게 된다.


먼저 리시버부터 알아보자.

 

// 리시버
public class Skill
{
    public void BladeDance() { Console.WriteLine("블레이드 댄스!"); }
    public void CriticalBuff() { Console.WriteLine("크리티컬 버프!"); }
}

 

여기서는 Skill 이라는 객체를 리시버로 사용하겠다.

이 객체에는 BladeDance 라는 액티브 스킬과 CriticalBuff 라는 버프 스킬이 존재한다.

(필요에 따라서는 인터페이스나 추상 클래스로 구현에 유연성을 주는 것이 좋을 것이다.)

 

다음으로는 리시버를 제어하는 커맨드 객체들이다.

 

public interface Command
{
    void Execute();
}

public class SCBladeDance : Command
{
    private Skill skill;

    public SCBladeDance(Skill skill) => this.skill = skill;

    public void Execute() => skill.BladeDance();
}

public class SCCriticalBuff : Command
{
    private Skill skill;

    public SCCriticalBuff(Skill skill) => this.skill = skill;

    public void Execute() => skill.CriticalBuff();
}

 

Command 인터페이스를 통해 SCBladeDance, SCCriticalBuff 클래스를 구현하였다.

각 객체에서는 Skill 클래스를 생성자에서 매개 변수로 받아 멤버 변수저장하고

적절한 함수를 Execute 에서 호출하고 있다.

 

그렇다면 여기서 굳이 왜 리시버를 커맨드 객체에 다시 담는지 의문이 들 수 있다.

전략 패턴 (Strategy Pattern) 에서 처럼 리시버를 캡슐화하여 바로 사용해도 되지 않을까?

 

이 패턴의 정의를 다시 한번 살펴보면 나와있듯

객체의 확장성에 대한 유연함을 주기 위한 것이라고 할 수 있다.

 

만약 스킬이 특정한 패턴(보스나 전투 중)에 의해서 강제로 취소되는 걸 구현해야 한다고 했을 때

Command 인터페이스에 Cancel이라는 메소드를 추가하고 이에 따른 추가적인 구현만 해주면 된다.

 

물론 이 또한 리시버에 바로 넣어서 구현할 수 있지만

만약 커맨드 객체가 서로 다른 객체여러 개 가지고 있다면 이해가 좀 더 쉽게 될 것이다.

(특정 스킬에 효과를 부여하는 아이템 등을 여기서 같이 관리한다거나...)


이번에는 인보커에 대해서 알아보자.

 

// 인보커
public class SkillSlot
{
    private Command[] slots;

    public SkillSlot(int slotSize) => slots = new Command[slotSize];

    public void UseSkill(int slot) => slots[slot]?.Execute();

    public void SetSkill(int slot, Command skill) => slots[slot] = skill;
}

 

SkillSlot 에 Command 인터페이스 배열을 만들어 슬롯을 구현했다.

초기화 시 배열의 크기를 지정하고 SetSkill 메소드를 통해 슬롯별 스킬을 지정할 수 있다.

 

여기서는 단순히 각 커맨드를 지정하고 실행하는 기능을 수행한다.

 

var skillSlot = new SkillSlot(2);
var bladeDance = new SCBladeDance(new Skill());
var critBuff = new SCCriticalBuff(new Skill());

skillSlot.SetSkill(0, bladeDance);
skillSlot.SetSkill(1, critBuff);

skillSlot.UseSkill(1);
skillSlot.UseSkill(0);
skillSlot.UseSkill(0);

 

그리고 이 코드가 클라이언트 부분에 해당한다고 볼 수 있다.


패턴을 정리하여 마치도록 하겠다.

 

  • 목적 : 요청객체로 캡슐화하고 재활용성과 확장의 유연함을 준다.
  • 방법 : 리시버, 커맨드, 인보커, 클라이언트 4가지 요소를 통해 기능을 분할하여 구현한다.

 

커맨드 객체의 의의에서 보았듯이 기능을 분할하고 유연함을 주는 게 객체지향에 있어서 좋은 건 맞지만

때로는 이러한 구조가 개발 중인 환경에 따라서는 오히려 복잡하게 돼버릴 수 있다.

 

디자인 패턴은 어디까지나 어떤 문제를 해결하기 위한 검증된 해결책이지

~하지 않으면 절대 안 돼 라는 등으로 생각하는 건 잘못된 생각이지 않을까 생각한다.

 

실제 개발에서 객체지향의 원칙들을 전부 지키며 좋은 패턴을 전부 적용시키는건 불가능에 가깝다.