메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

게임 프로그래밍 패턴

디자인 패턴 다시 보기: 명령 패턴(1/2)

한빛미디어

|

2016-06-01

|

by 로버트 나이스트롬

25,122

『GoF의 디자인 패턴』이 나온 지 20년이 넘었다.  사람으로 치자면 술을 마셔도 될 만큼 나이를 먹었다. 빠르게 변하는 소프트웨어 산업에서 20년은 엄청난 세월이다. 그런데도 이 책이 계속 인기 있다는 것은 여러 프레임워크나 방법론에 비해 세월이 지나도 변치 않는 설계의 가치가 얼마나 대단한지를 보여준다.

『GoF의 디자인 패턴』은 여전히 유용하지만, 우리도 지난 20년 동안 많이 배웠다. 『GoF의 디자인 패턴』에 수록된 여러 패턴을 되짚어보면서 패턴별로 유용하거나 재미있는 것을 살펴보려 한다.

 

명령command 패턴은 내가 아주 좋아하는 패턴 중 하나다. 꼭 게임이 아니어도 웬만큼 규모가 있는 프로그램에는 언제나 명령 패턴을 써먹었다. 잘 쓰기만 하면 굉장히 지저분한 코드도 깔끔하게 정리할 수 있다. 그래서인지 몰라도, GoF는 명령 패턴을 위와 같이 난해하게 표현했다.

저 문장이 형편없다는 데에는 다들 동의할 것이다. 비유부터가 와 닿지 않는다. 단어에 아무 뜻이나 갖다 붙일 수 있는 소프트웨어 세계가 아닌 현실 세계에서 '사용자'는 우리가 같이 일을 하는 사람을 뜻한다. 방금 다시 찾아봤지만 사람을 '매개변수화'할 수는 없다.

나머지 문장은 명령 패턴을 적용할 만한 항목을 나열해놨을 뿐이다. 우리가 하려는 업무가 이 중에 있지 않는 한 이해하는 데 별 도움이 안 된다. 나는 명령 패턴을 다음과 같이 매우 간결하게 요약한다.

 

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

물론 '매우 간결하다'는 게 '무슨 말인지 모를 정도로 간단하다'와 같은 말일 때가 종종 있다. 내가 써놓은 요약도 GoF의 그것과 별반 차이 없어 보일지 모르겠다. 좀 더 풀어보겠다. '실체화'는 '실제하는 것으로 만든다'는 뜻이다. 프로그래밍 분야에서는 무엇인가를 '일급first-class'으로 만든다는 뜻으로도 통한다.

'실체화'니 '일급'이니 하는 말은 어떤 개념을 변수에 저장하거나 함수에 전달할 수 있도록 데이터, 즉 객체로 바꿀 수 있다는 걸 의미한다. 여기에서 명령 패턴을 '메서드 호출을 실체화한 것'이라고 한 것은 함수 호출을 객체로 감쌌다는 의미다.

이는 프로그래밍 언어 배경에 따라 '콜백', '일급 함수', '함수 포인터', '클로저', '부분 적용 함수'와 비슷하게 들릴 테고, 실제로 그렇다. GoF는 책의 뒤에서는 '명령 패턴은 콜백을 객체지향적으로 표현한 것'이라고 정의한다.

처음부터 그렇게 요약했더라면 훨씬 나았을 것이다. 그렇다고 해도 둘 다 추상적이고 애매하기는 매한가지다. 나는 패턴을 설명할 때 뭔가 구체적인 것에서부터 시작하는 편인데 이번에는 그러질 못했다. 지금부터는 명령 패턴을 잘 써먹을 수 있는 예제를 살펴보겠다.

 

 

3.1. 입력키 변경

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

1.png

▲ 게임 행동과 연결된 버튼들

(JUMP: 점프, FIRE_GUN: 발사, LURCH: 비틀거리기, SWAP_WEAPON: 무기 교체)

 

정말 간단하게 구현해보자. 

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

}

 

일반적으로 이런 함수는 [[게임 루프]]에서 매 프레임 호출된다. 코드는 쉽게 이해할 수 있을 것이다. 입력 키 변경을 불가능하게 만들겠다면 이래도 되겠지만, 많은 게임이 키를 바꿀 수 있게 해준다.

키 변경을 지원하려면 jump()나 fireGun() 같은 함수를 직접 호출하지 말고 교체 가능한 무엇인가로 바꿔야 한다. '교체'라는 단어를 들으니 왠지 어떤 게임 행동을 나타내는 객체가 있어서 이를 변수에 할당해야 할 거 같지 않은가? 이제 명령 패턴이 등장할 때다.

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

class Command {

public:

  virtual ~Command() {}

  virtual void execute() = 0;

};

 

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

class JumpCommand : public Command {

public:

  virtual void execute() { jump(); }

class FireCommand : public Command {

public:

  virtual void execute() { fireGun(); }

};

// 뭘 하려는 건지 알 것이다...

 

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

class InputHandler {

public:

  void handleInput();

  // 명령을 바인드(bind)할 메서드들...

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

}

 

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

2.png

할당 가능한 명령에 연결된 버튼들

(BUTTON_X: X 버튼, BUTTON_Y: Y 버튼, BUTTON_B: B 버튼, BUTTON_A: A 버튼

JUMP COMMAND: 점프 명령, FIRE..: 공격 명령, LURCH...: 비틀거리기 명령, SWAP...: 무기 교체 명령)

 

여기까지가 명령 패턴의 핵심이다. 이미 명령 패턴의 장점을 이해했다면, 나머지는 덤이라고 생각하고 읽어보자.

 

 

3.2. 액터에게 지시하기

방금 정의한 Command 클래스는 이번 예제만 놓고 보면 잘 동작하지만 한계가 있다. jump()나 fireGun() 같은 전역 함수가 플레이어 캐릭터 객체를 암시적으로 찾아서 꼭두각시 인형처럼 움직이게 할 수 있다는 가정이 깔려 있다는 점에서 상당히 제한적이다.

이렇게 커플링이 가정에 깔려 있다 보니 Command 클래스의 유용성이 떨어진다. 현재 JumpCommand 클래스는 오직 플레이어 캐릭터만 점프하게 만들 수 있다. 좀 더 유연하게 만들기 위해 제어하려는 객체를 함수에서 직접 찾게 하지 말고 밖에서 전달해주자.

class Command {

public:

  virtual ~Command() {}

  virtual void execute(GameActor& actor) = 0;

};

 

여기서 GameActor는 게임 월드를 돌아다니는 캐릭터를 대표하는 '게임 객체' 클래스다. Command를 상속받은 클래스는 execute()가 호출될 때 GameActor 객체를 인수로 받기 때문에 원하는 액터의 메서드를 호출할 수 있다.

class JumpCommand : public Command {

public:

  virtual void execute(GameActor& actor) {

    actor.jump();

  }

};

 

이제 JumpCommand 클래스 하나로 게임에 등장하는 어떤 캐릭터라도 폴짝거리게 할 수 있다. 남은 것은 입력 핸들러에서 입력을 받아 적당한 객체의 메서드를 호출하는 명령 객체를 연결하는 코드뿐이다. 먼저 handleInput()에서 명령 객체를 반환하도록 변경한다.

Command* 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;

}

 

어떤 액터를 매개변수로 넘겨줘야 할지 모르기 때문에 handleInput()에서는 명령을 실행할 수 없다. 여기에서는 명령이 실체화된 함수 호출이라는 점을 활용해서, 함수 호출 시점을 지연한다.

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

Command* command = inputHandler.handleInput();

if (command) {

  command->execute(actor);

}

 

 

액터(actor)가 플레이어 캐릭터라면 유저 입력에 따라 동작하기 때문에 처음 예제와 기능상 다를 게 없다. 하지만 명령과 액터 사이에 추상 계층을 한 단계 더 둔 덕분에, 소소한 기능이 하나 추가되었다. 명령을 실행할 때 액터만 바꾸면 플레이어가 게임에 있는 어떤 액터라도 제어할 수 있게 되었다.

사실 플레이어가 다른 액터를 제어하는 기능은 일반적이지 않다. 하지만 비슷하면서도 자주 사용되는 기능이 있다. 생각해보자. 지금까지는 플레이어가 제어하는 캐릭터만 생각했지만, 게임에는 다른 캐릭터가 많다. 이런 캐릭터는 AI가 제어하는데, 같은 명령 패턴을 AI 엔진과 액터 사이에 인터페이스용으로 사용할 수 있다. 즉, AI 코드에서 원하는 Command 객체를 이용하는 식이다.

Command 객체를 선택하는 AI와 이를 실행하는 액터를 디커플링함으로써 코드가 훨씬 유연해졌다. 액터마다 AI 모듈을 다르게 적용할 수 있고, 기존 AI를 짜 맞춰서 새로운 성향을 만들 수도 된다. 적을 좀 더 공격적으로 만들고 싶은가? 좀 더 공격적인 AI를 적용해 공격 명령을 찍어내게 하면 된다. 더 나아가서는 플레이어 캐릭터에 AI를 연결해서 자동으로 실행되는 데모 모드를 만들 수도 있다.

액터를 제어하는 Command를 일급 객체로 만든 덕분에, 메서드를 직접 호출하는 형태의 강한 커플링을 제거할 수 있었다. 이제는 명령을 큐queue나 스트림stream으로 만드는 것도 생각해보자. 

 

3.png

▲ 비유를 대강 그림으로 그려보았다.

(AI : AI, COMMAND STREAM: 명령 스트림, ACTOR: 액터)

 

입력 핸들러나 AI 같은 코드에서는 명령 객체를 만들어 스트림에 밀어 넣는다.  디스패처dispatcher나 액터에서는 명령 객체를 받아서 호출한다. 큐를 둘 사이에 끼워 넣음으로써, 생산자producer와 소비자consumer를 디커플링할 수 있게 되었다.

댓글 입력
자료실

최근 본 상품0