본문 바로가기
디자인 패턴/명령(Command)

Design pattern(1-1) - 명령 패턴(command) 원리

by SL123 2024. 7. 30.

명령 패턴이란? 

 

쉽게 설명한다면 '메서드 호출을 실체화 한것'입니다.

실체화는 실체하는 것(인스턴스)을 만든다는 의미이며, 변수에 저장하거나 함수에

전달할 수 있도록 데이터, 즉 객체로 바꿀 수 있다는 걸 의미합니다.

 

명령 패턴은 '메서드 호출을 객체로 감싸는 것'을 의미합니다. 이는 함수 호출을

객체로 캡슐화하여, 나중에 그 작업을 실행할 수 있도록 합니다. 이 패턴은

콜백과 유사하지만, 콜백은 주로 비동기 상황에서 특정 이벤트를 처리할 때

사용합니다.

 

명령 패턴을 요약하자면 함수가 하는 명령(커맨드)를 인스턴스로 실체화하여

객체에게 함수가 하는 일을 맡기는 것입니다.

 

이제 명령 패턴이  어떻게 쓰이는지 알아봅시다.

 

입력키 변경

 

콘솔의 입력을 읽는 코드가 있고 그 코드를 호출한다고 가정해봅시다. 

https://www.mermaidchart.com/

 

간단하게 구현한다면

void InputHandler::handleInput() {

  if (isPressed(BUTTON_X)) jump();
  else if (isPressed(BUTTON_Y)) fireGun();
  else if (isPressed(BUTTON_A)) swapWeapon();
  else if (isPressed(BUTTON_B)) lurchIneffectively();
}

 

이런식으로 매 프레임 호출해 상태를 바꿔줄 수 있습니다.

코드는 쉽게 이해할 수 있지만, 입력 키 변경이 불가능합니다.

만약 입력 키를 안바꾼다면 상관없지만 수많은 게임들은 키를 바꿀 수 있습니다.

 

대표적으로 카트라이더 악마 아이템이 있다.

 

키 변경을 지원한다고 하면 고정된 함수보다 교체 가능한 무엇인가로 바꿔야합니다.

교체 가능한 무언가 -> 이동이 가능한 함수 -> 함수를 실체화해서 변경 -> 명령 패턴

이러한 이유로 명령 패턴이 사용됩니다.

 

게임에서 할 수 있는 행동을 실행할 수 있는 상위 클래스(Command)를 정의합니다.

그리고 하위 클래스에서 함수를 재정의하여 사용합니다.

// 상위 클래스를 추상 클래스로 설정
class Command {
public:
    virtual ~Command() {};
    virtual void excute() = 0;
};

// 자식 클래스에서 재정의하여 사용
class SwapWeaponCommand : public Command {
public:
    virtual void excute() { jump(); }
};

class FireCommand : public Command { 
public:
    virtual void execute() { fireGun(); }

};

 

입력 핸들러 코드는 각 버튼별로 Command 클래스 포인터를 저장합니다.

(포인터로 저장해 입력 핸들링을 자유자재로 하기 위함)

(메모리 누수는 신경 안쓰고 기본적인 디자인 패턴 설계도)

class InputHandler {
public:
    void handleInput();
    // (생략) 명령 바인드할 메서드들
    
private:
    Command* buttonX_;
    Command* buttonY_;
    Command* buttonA_;
    Command* buttonB_;
}

 

그리고 나서 다음 코드로 변경됩니다. 

void InputHandler::handleInput() {
  if (isPressed(BUTTON_X)) buttonX_->execute();
  else if (isPressed(BUTTON_Y)) buttonY_->execute();
  else if (isPressed(BUTTON_A)) buttonA_->execute();
  else if (isPressed(BUTTON_B)) buttonB_->execute();
}

 

 

이렇게 코드를 작성하면 직접 함수를 통해 고정된 값으로 전달되는 것이 아닌

함수로 코드를 우회해 더 유연하게 작성할 수 있도록 만들었습니다. 

 

 

커플링 방지

기본적으로 만든 Command 클래스는 플레이어에서 Jump() 등의 함수를 호출하기

때문에 커플링이 되어있고 상당히 제한적인 느낌을 받습니다.

 

이렇다보니 Command의 유용성은 매우 떨어지며, 현재 JumpCommand와

SwapweaponCommand는 플레이어만이 사용할 수 있는 클래스가 되어버렸습니다.

이런 제약을 해결하기 위해 함수 인자에 Jump()를 할 수 있는 클래스를 전달해줍니다.

 

class Command {
public:
    virtual ~Command() {}
    virtual void execute(Actor& actor) = 0;
};

 

이로써 Command를 상속받은 클래스는 execute()를 호출할 때 Actor의 객체를

인수로 받기 때문에 원하는 Actor의 메서드를 호출할 수 있습니다.

class LurchCommand : public Command {
public:
    virtual void execute(Actor& actor) {
    	actor.Lurch();
    }

};

 

이렇게 작성하면 어떤 Actor든지 Lurch를 할 수 있게 만든 것입니다.

이제 HandleInput()을 다시 디커플링 상태로 바꿔보죠.

Command* InputHandler::handleInput() {
  if (isPressed(BUTTON_X)) return buttonX_;
  else if (isPressed(BUTTON_Y)) return buttonY_;
  else if (isPressed(BUTTON_A)) return buttonA_;
  else if (isPressed(BUTTON_B)) return buttonB_;
  
  // 아무것도 누르지 않음(명령을 입력하지 않음)
  return nullptr;
}

 

다음으로 명령 객체를 받아서 Actor에 코드를 적용시켜보죠.

Command* command = inputHandler.handleInput();
if(command) {
    command->execute(actor);
}

 

일단 이렇게 구현했지만 플레이어 캐릭터 커맨드라면 처음과 달라질 것이 없습니다.

다만 명령 <-- 추상 계승 --> 액터를 반영했기 때문에 게임에 어떤 Actor라도

추상 계층을 타고 넘아가 Command를 입력받을 수 있는 상태가 되는 것입니다.

 

일반적으로 플레이어가 모든 Actor들을 직접 조종하는 것은 아닙니다.

하지만, AI가 돌(Actor)를 굴려서 떨어뜨린다거나 AI가 AI를 돌격시키는 등의

다양한 인터페이스들을 구성할 수 있습니다. 즉, AI코드에서

원하는 Command 객체를 이용할 수 있습니다.

(상태 패턴과 연계가 된다. 추후에 상태 패턴도 알아보겠습니다.)

 

Actor를 제어하는 Command를 객체로 만들었기 때문에 메서드를 직접 호출하는

강력한 커플링을 제거해 유용하게 사용할 수 있습니다.

 

Event queue pattern을 이용해 추상 계층을 하나 더 만들어

명령 패턴을 훨씬 더 다방면화 시킬 수 있습니다.

(추후에 Event queue pattern 도 알아보겠습니다.)

 

다음 포스팅에서는 명령 패턴의 undo 와 redo를 알아보겠습니다.

'디자인 패턴 > 명령(Command)' 카테고리의 다른 글

Design pattern(1-2) - 명령 패턴(undo, redo)  (0) 2024.08.01