소년코딩

명령 패턴, 커맨드 패턴, Command Pattern

요청 자체를 캡슐화 하는 것입니다. 이를 통해 서로 다른 사용자(client)를 매개변수로 만들고, 요청을 대기시키거나 로깅하며, 되돌릴 수 있는 연산을 지원합니다. (GoF의 디자인 패턴 311p)

간결하게 요약하면 다음과 같다.

명령 패턴은 메서드 호출을 실체화 한 것이다.

실체화는 '실제하는 것으로 만든다'라는 뜻으로, 프로그래밍에서는 무엇인가를 '일급(first-class)'으로 만든다는 뜻으로 통한다.

즉, 어떤 개념을 변수에 저장하거나 함수에 전달할 수 있도록 데이터, 즉 객체로 바꿀 수 있다는 걸 의미한다.

다시 요약한다면 명령 패턴은 함수 호출을 객체로 감싼다는 것이므로 콜백을 객체지향적으로 표현한 것이다.


입력 키 변경

모든 게임에는 버튼이나 키보드등 유저 입력을 읽는 코드가 있고, 이런 코드는 입력을 받아서 의미있는 행동으로 전환한다.

간단하게 구현하면 다음과 같다.

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)) kneel();
}

일반적으로 위와같은 함수는 게임 루프에서 매 프레임 호출된다. 코드는 쉽게 이해할 수 있다.

그러나 만약 입력 키 변경을 지원하려면 jump()나 fireGun() 같은 함수를 직접 호출하지 말고 교체가능한 무엇인가로 바꿔야 한다. '교체'라는 단어를 들으니 왠지 어떤 게임 행동을 나타내는 객체가 있어야 될꺼같다. (이제 명령 패턴이 등장한다.)

우선 게임에서 할 수 있는 행동을 실행할 수 있는 공통 상위 클래스부터 정의한다.

class Command {
public:
    virtual ~Command() { }
    virtual void excute() = 0;
};
// 인터페이스에 반환 값이 없는 메서드 하나밖에 없다면 명령 패턴일 가능성이 높다.

이제 각 행동별로 하위 클래스를 만든다.

class JumpCommand : public Command {
public:
    virtual void execute() { jump(); }
};

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

// ... more

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

class InputHandler {
public:
    void handleInput();
    // 명령을 바인드(bind)할 메서드들...

private:
    Command* buttonX_;
    Command* buttonY_;
    Command* buttonA_;
    Command* buttonB_;
};

이제 입력 처리는 다음 코드로 위임된다.

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

직접 함수를 호출하던 코드 대신에, 위와 같이 한 겹 우회하는 계층이 생겼다.


액터에게 지시하기

방금 정의한 Command 클래스는 잘 동작하지만 jump()나 fireGun() 같은 전역 함수가 암시적으로 플레이어 캐릭터 객체만을 움직이게 할 수 있다는 가정이 깔려있다는 한계가 있다.

이렇게 커플링이 가정에 깔려 있다 보니 Command 클래스의 유용성이 떨어진다. (현재 JumpCommand 클래스는 오직 플레이어 캐릭터만 점프하게 만들 수 있다.)

이런 제약을 유연하게 만들기 위해 제어하려는 객체를 함수에서 직접 찾게하지 말고 밖에서 전달해주자.

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

위에서 GameActor 클래스는 게임 월드를 돌아다니는 캐릭터를 대표하는 클래스다.

Command를 상속받은 클래스는 excute()가 호출될 때 GameActor 객체를 인수로 받기 때문에 원하는 액터의 메서드를 호출할 수 있다.

class JumpCommand : public Command {
public:
    virtual void execute(GameActor& actor) { 
        actor.jump();
    }
};

이제 JumpCommand 클래스 하나로 게임에 등장하는 어떤 캐릭터도 점프시킬 수 있다.

남은 것은 입력 핸들러에서 입력을 받아 적당한 객체의 메서드를 호출하는 명령 객체를 연결하는 코드뿐이다.

void InputHandler::handleInput()
{
    if (isPressed(BUTTON_X)) return buttonX_;
    if(isPressed(BUTTON_Y)) return buttonY_;
    if(isPressed(BUTTON_A)) return buttonA_;
    if(isPressed(BUTTON_B)) return buttonB_;

    // 아무것도 누르지 않았다면, 아무것도 하지 않는다.
    return NULL;
}

다음으로 명령 객체를 받아서 플레이어를 대표하는 GameActor 객체에 적용하는 코드가 필요하다.

Command* command = inputHandler.handleInput();
if(command)
    command->excute(actor);

위와같이 명령과 액터 사이에 추상 계층을 한 단계 더 두었다. 덕분에 명령을 실행할 때 액터만 바꾸면 플레이어가 게임에 있는 어떤 액터라도 제어할 수 있게 되었다.

결론적으로 액터를 제어하는 Command를 일급 객체로 만든 덕분에, 메서드를 직접 호출하는 형태의 강한 커플링을 제거할 수 있었다.


실행취소와 재실행

명령 객체가 어떤 작업을 실행할 수 있따면, 이를 실행취소(undo)할 수 있게 만드는 것도 어렵지 않다. 그냥 실행취소 기능을 구현하려면 어렵지만, 명령 패턴을 이용하면 쉽게 만들 수 있다.

이미 명령 객체를 이용해서 입력 처리를 추상화해둔 덕분에, 플레이어 이동도 명령에 캡슐화되어 있다. 어떤 유닛을 옮기는 명령을 생각해보자.

class MoveUnitCommand : public Command {
public:
    MoveUnitCommand(Unit* unit, int x, int y)
    : unit_(unit), x_(x), y_(y) { }

    virtual void excute() {
        unit_->moveTo(x_, y_);
    }

private:
    Unit* unit_;
    int x_, y_;
};

MoveUnitCommand 클래스는 이전 예제와 다르게 이동하려는 유닛과 위치 값을 생성자에서 받아서 명령과 명시적으로 바인드했다. 이는 MoveUnitCommand 명령 인스턴스가 '무엇인가를 움직이는' 보편적인 작업이 아니라 게임에서의 구체적인 실제 이동을 담고 있는걸 뜻한다.

이것은 명령 패턴 구현을 어떻게 변형할 수 있는지 잘 보여준다. 처음 예제 같은 경우, 어떤 일을 하는지를 정의한 명령 객체 하나가 매번 재사용 되었고, 이번에 만든 명령 클래스는 특정 시점에서 발생될 일을 표현한다는 점에서 좀 더 구체적이다.

이를테면, 입력 핸들러 코드는 플레이어가 이동을 선택할 때마다 명령 인스턴스를 생성해야 한다.

Command* handleInput() 
{
    Unit* unit = getSelectedUnit();

    if(isPressed(BUTTON_UP)) {
        // 유닛을 한 칸 위로 이동한다.
        int destY = unit->getY() - 1;
        return new MoveUnitCommand(unit, unit->getX(), destY);
    }
    // 다른 이동들...

    return NULL;
}

Command 클래스에 명령의 취소할 수 있도록 순수 가상함수 undo()를 정의한다.

class Command {
public:
    virtual ~Command() { }
    virtual void excute() = 0;
    virtual void undo() = 0;
};

undo() 에서는 excute() 에서 변경하는 게임 상태를 바꿔주면 된다.

class MoveUnitCommand : public Command {
public:
    MoveUnitCommand(Unit* unit, int x, int y)
    : unit_(unit), x_(x), y_(y), xBefore_(0), yBefore_(0) { }

    virtual void excute() {
        // 나중에 이동을 취소할 수 있도록 원래 유닛 위치를 저장한다.
        xBefore_ = unit_->getX();
        yBefore_ = unit_->getY();
        unit_->moveTo(x_, y_);
    }

    virtual void undo() {
        unit_->moveTo(xBefore_, yBefore_);
    }

private:
    Unit* unit_;
    int x_, y_;
    int xBefore_, yBefore_;
};

플레이어가 이동을 취소할 수 있게 하려면 이전에 실행했던 명령을 저장해야 한다.

또한 여러 단계의 실행취소를 지원하는 것도 그다지 어렵지 않다. 가장 최근 명령만 기억하는 대신, 명령 목록을 유지하고 '현재' 명령이 무엇인지만 알고 있으면 된다.

(오래된 명령) 명령 명령 명령 명령 (최근 명령)
실행취소 현재 재실행
  • 실행취소: 현재 명령을 실행취소하고 핸재 명령을 가리키는 포인터를 뒤로 이동한다.
  • 재실행: 포인터를 다음으로 이동시킨 후에 해당 포인터를 재 실행한다.

유저가 몇 번 '실행취소'한 뒤에 새로운 명령을 실행한다면, 현재 명령 뒤에 새로운 명령을 추가하고 그 다음에 붙어 있는 명령들은 버린다.


클래스 VS 함수형

지금까지의 예제에서는 전부 클래스만 사용했다.

C++이 일급 함수를 제대로 지원하지 않기 때문이다. 함수 포인터에는 상태를 저장할 수 없고, 함수 객체는 여전히 클래스르 정의해야 한다. 또한 람다는 메모리를 직접 관리해야 하기 때문에 쓰기가 까다롭다.

만약 자바스크립트로 게임을 만든다면 유닛 이동명령을 다음과 같이 만들 수 있다.

function makeMoveUnitCommand(unit, x, y) {
    // 아래 function이 명령 객체에 해당한다.
    return function() {
        unit.moveTo(x, y);
    };
}

클로저를 여러개 이용하면 실행취소도 지원할 수 있다.

function makeMoveUnitCommand(unit, x, y) {
    var xBefore, yBefore;

    return  {
        excute: function() {
            xBefore = unit.getX();
            yBefore = unit.getY();
            unit.moveTo(x, y);
        },
        undo: function() {
            unit.moveTo(xBefore, yBefore);
        }
    };
}

More

  • 명령 패턴을 쓰다보면 수많은 Command 클래스를 만들어야 할 수 있다. 이럴 때에는 상위 클래스에 여려 가지 편의를 제공하는 상위 레벨 메서드를 만들어 놓은 뒤에 필요하면 하위 클래스에서 원하는 작동을 재정의할 수 있게 하면 좋다. 이러면 명령 클래스의 excute 메서드가 하위 클래스 샌드박스 패턴으로 발전하게 된다.

  • 예제에서는 어떤 액터가 명령을 처리할지 명시적으로 지정했지만 계층 구조 객체 모델에서처럼 누가 명령을 처리할지 명시적이지 않을 수도 있다. 객체가 명령에 반응할 수도 잇고 종속 객체에 명령처리를 떠넘길 수도 있다면 GoF의 책임 연쇄 패턴이다.

  • 어떤 명령은 처음 예제에서 등장한 JumpCommand 클래스처럼 상태 없이 순수하게 행위만 정의되어 있을 수 있다. 이런 클래스는 모든 인스턴스가 같기 때문에 인스턴스를 여러 개 만들어봐야 메모리만 낭비한다. 이 문제는 경량패턴으로 해결할 수 있다.


by 소년코딩

추천은 글쓴이에게 큰 도움이 됩니다.

악플보다 무서운 무플, 댓글은 블로그 운영에 큰 힘이됩니다.

신고
댓글 로드 중…

트랙백을 확인할 수 있습니다

URL을 배껴둬서 트랙백을 보낼 수 있습니다