개요
행동 패턴에는 다음 패턴들이 포함되어 있다. (취소선은 다루지 않음)
전략(Strategy)- 상태(State)
- 탬플릿 메서드(Template Method)
- 명령(Command)
- 책임 연쇄(Chain of Responsibility)
- 관찰자 (Observer)
- 메멘토 (Memento)
- 중재자 (Mediator)
- 방문자 (Visitor)
반복자 (Iterator)인터프리터 (Interpreter)
전략패턴, 반복자패턴은 이미 익숙한 개념이므로 패스, Interpreter는 특정 상황(ex. 문자열 명령어 파싱)에 한정적으로 쓰여서 패스.
상태패턴
2D 횡스크롤 게임이 있다고 가정하다. 캐릭터는 점프, 앉기, 달리기... 등의 여러 동작이 가능하다. 이런 동작들을 구현할 때 많은 예외에 부딪힐 것이다. 예를 들어, 점프 중에는 점프하거나 엎드릴 수 없다. 이런 경우 FSM을 사용해서 캐릭터의 동작을 처리할 수 있다.
FSM은 다음의 특징을 갖는다.
- 가질 수 있는 상태는 제한되어 있다.
- 한 번에 한 가지 상태만 될 수 있다.
- 각 상태에는 전달된 입력이나 이벤트에 따라 다른 상태로 바뀌는 전이(Transition)가 있다.
FSM을 열거형의 상태와 Switch 문을 통해서 많이 구현하는데 만약 각 상태별로 데이터가 있다면 상태를 오브젝트로 분리하는 편이 나을 것이다.
상태 패턴은 이런 식으로 State 클래스를 만들어 그때그때 Input을 Handle()하여 처리한다.
class CharacterState
{
public :
virtual void handleInput(Character& character, Input input) = 0;
virtual void update(Character& character, float deltaTime) = 0;
};
class DuckingState : public CharacterState
{
public:
DuckingState() : chargeTime_(0) {}
void handleInput(Character& character, Input input) override
{
//엎드린 상태에서의 Input을 처리한다.
}
void update(Character& character, float deltaTime) override
{
chargeTime_ += deltaTime;
//캐릭터 상태를 업데이트 한다.
}
private:
float chargeTime_;
};
class Character
{
public:
void handleInput(Input input)
{
state_->handleInput(*this, input);
}
void update(float deltaTime)
{
state_->update(*this, deltaTime);
}
private:
CharacterState * state_;
};
만약 상태 객체에 필드가 없다면 각 상태별로 인스턴스는 하나만 있으면 된다. 따라서 상위 상태 클래스에 정적 인스턴스로 둘 수 있다.
class CharacterState
{
public:
static StandingState standing;
static RunningState running;
static DuckingState ducking;
static JumpingState jumping;
};
이런 정적 상태 객체들은 매번 상태 객체를 할당하기 위한 메모리, CPU 오버헤드가 없으므로 인스턴스가 있는 상태 객체보다 좀 더 이상적이다.
FSM의 장점이자 단점은 제한된 구조를 갖는다는 것이다. 예를 들어, 위의 Character에서 무기를 든 상태/무기를 들지 않은 상태를 나눠야 한다면? 무기를 든 점프/무기를 들지 않은 점프 이렇게 n*m개의 상태를 모두 구현하는 대신 FSM을 두개 갖는 Character를 만들어주면 된다. (무장 상태 State, 캐릭터 상태 State)
또한 상태 객체끼리 상속을 할 수도 있다. 예를 들어 [점프하기], [날기] 등은 모두 [공중] 상태를 상속받을 수 있다. 이렇게 구현한다면 [공중] 상태일때 점프키를 입력 받아도 상위 클래스에서 미리 처리가 가능하다.
푸시 다운 오토마타처럼 스택에 상태를 넣어 이전 상태를 기억해 둘 수도 있다. 예를 들어, 캐릭터가 총을 쏘는 경우 이전 상태가 달리기냐 걷기냐를 Stack에 저장해 두었다가 총을 쏘고 난 뒤 다시 불러올 수 있다.
FSM은 다음 경우에 사용하면 좋다.
- 내부 상태에 따라 객체 동작이 바뀔 때.
- 이런 상태가 많지 않은 선택지로 분명하게 구분될 수 있을 때.
- 객체가 입력이나 이벤트에 따라 반응할 때.
탬플릿 메서드
Abstract Class에서 동작의 기본적인 뼈대를 정의하고 Concrete에서 특정 단계만 선택적으로 재정의 하는 것.
예를 들어, 라면을 끓이는 동작이 있다면 TemplateMethod()에서 물 끓이기, 재료 넣기 등을 수행해 줄 수 있으며 이를 상속하는 치즈라면 끓이기에서 재료 넣기만 재정의하여 치즈를 추가해줄 수 있다.
명령
명령 패턴은 메서드 호출을 객체로 감싼 것이다.
예를 들어, 게임 패드에서 버튼 입력을 받아 캐릭터를 동작하는 기능이 있다고 하자.
void InputHandler::handleInput()
{
if (isPressed(BUTTON_X)) jump();
else if (isPressed(BUTTON_Y)) attack();
//... 계속 한다.
}
그런데 대부분의 게임은 키 변경을 지원한다. 만약 유저가 키를 변경했다면? 위 코드에서는 교체가 불가능하니 교체가 가능한 형태로 바꿔줘야 한다.
따라서 jump, attack같은 명령들을 클래스로 객체로 만들어주자.
class Command
{
public:
virtual ~Command(){}
virtual void execute(Actor& actor) = 0;
};
class JumpCommand : public Command
{
public:
void execute(Actor& actor) override
{
actor.jump();
}
};
그러면 Input은 다음과 같이 처리할 수 있다.
class InputHandler
{
public:
Command* handleInput();
private:
Command * buttonX_;
Command * buttonY_;
Command * buttonA_;
Command * buttonB_;
};
Command* InputHandler::handleInput()
{
if (isPressed(BUTTON_X)) return buttonX_;
else if (isPressed(BUTTON_Y)) return buttonY_;
//... 계속 한다.
}
Command* command = inputHandler.handleInput();
if (command)
command->execute(actor);
이렇게 되면 플레이어 뿐 아니라 AI를 제어할 수도 있고 모바일 게임에서 많이 쓰이는 자동전투를 구현하기도 쉽다. 또한 액터에 명령 큐나 스트림을 덧붙여서 커플링을 느슨하게 할 수도 있다.
또한 이러한 설계는 undo 기능을 개발하기도 편하다. 단순히 Command 클래스에 undo라는 순수 가상 함수를 추가해준 뒤 하위 클래스에서 이를 구현해주면 된다.
class Command
{
public:
virtual ~Command(){}
virtual void execute(Actor& actor) = 0;
virtual void undo(Actor& actor) = 0; //새로 추가된 undo 함수
};
사실 명령 패턴은 클로저를 제대로 지원하는 언어에서 일급 함수로 얼마든지 대체가 가능하다. 명령 패턴의 유용함은 함수형 프로그래밍의 유용함을 보여주는 예이기도 하다.
책임 연쇄
책임 연쇄 패턴은 명령 객체와 처리 객체(Handler)로 구성되어 있다. 처리 객체는 명령 객체를 받아 처리를 하고 다음 처리 객체에 명령을 넘겨줄 지 결정한다.
이때 처리 객체들은 일종의 Linked List나 Tree 구조로 이뤄지며 다음으로 명령을 넘길 처리 객체를 가리킬 수 있다.
처리 객체들끼리 서로 알 필요가 없기 때문에 커플링이 느슨해진다.
데코레이터 패턴과 유사한데, 데코레이터 패턴은 모든 감싼 객체들이 수행되어지는 반면 책임 연쇄 패턴은 처리 객체 중 일부만 수행되며 각각의 처리 객체가 다음 처리 객체를 선택할 수 있다는 점이 다르다.
관찰자
굉장히 많이 쓰이는 패턴이다.
게임에서는 특히 업적등을 관리할 때 유용하다. 대상(Subject)는 관찰자(Observer)와 상호작용 하지만 서로 커플링 되어있지 않다는 것이 장점이다.
관찰자 패턴이 동기적이라는 점을 주의해야 한다. 대상이 관찰자 메서드를 직접 호출하기 때문에 모든 관찰자의 알림 메서드를 반환하기 전에 다음 코드로 넘어갈 수 없다. 오래 걸리는 작업이 있다면 다른 스레드에 넘기거나 작업 큐를 활용하자.
관찰자를 멀티 쓰레드 환경에서 사용할 때는 조심해야 한다. 관찰자가 어떤 대상의 락을 물고있다면 프로그램 전체가 교착상태에 빠질 수 있다. 이런 환경에서는 이벤트 큐를 이용하여 비동기적으로 상호작용 하는게 좋다.
또한 등록 취소를 잘 신경써줘야 한다. 객체가 삭제될 때 스스로 등록 취소하도록 하는게 좋다. 그러나 이 방법의 경우 관찰자가 대상들의 목록을 관리해야 하므로 상호 참조가 생겨 복잡성이 늘어난다.
업적 <-> 물리엔진 같은 상관없는 분야에서의 상호작용에서 관찰자 패턴은 유용하지만 긴밀히 연관된 코드 덩어리 안에서는 오히려 명시적으로 연결하는 것이 코드를 이해하거나 양쪽 코드의 상호작용을 살펴보는데 더 유용할 수 있다.
관찰자 객체를 만드는 것 보다 함수 레퍼런스를 관찰자로 만드는 것이 더 일반적이다.
관찰자 패턴을 비동기적으로 쓰려면 이벤트 큐 패턴 참고
메멘토
메멘토 패턴은 캡슐화를 훼손하지 않고 객체의 내부 상태를 캡처하여 저장해두어서 변경 사항을 되돌릴 수 있게 해준다.
public class SomeMemento
{
private readonly System.DateTime CreateTime;
public readonly int SomeFieldA;
public readonly float SomeFieldB;
public SomeMemento(int A, float B)
{
CreateTime = System.DateTime.Now;
SomeFieldA = A;
SomeFieldB = B;
}
}
public class SomeClass
{
private int _someFieldA;
private float _someFieldB;
public SomeMemento CreateMemento()
{
return new SomeMemento(_someFieldA, _someFieldB);
}
public void Restore(SomeMemento memento)
{
_someFieldA = memento.SomeFieldA;
_someFieldB = memento.SomeFieldB;
}
}
중재자
중재자 패턴은 오브젝트간의 강한 커플링을 없애줄 수 있다.
예를 들어, IOT 시스템이 있다고 가정하자. 여기에는 <스마트 시계>, <스마트 전등>, <스마트 스피커>가 있다.
<스마트 시계>의 알람이 울리면 <스마트 전등>에서 불을 켜고 <스마트 스피커>에서 음악을 틀어준다고 하자. 그렇다면 <스마트 시계> 객체에서 <스마트 전등>, <스마트 스피커>에 직접 접근을 해야 할까? 만약 <스마트 전등>만 있고 <스마트 스피커>는 없는 사용자라면? 강한 커플링덕에 여러모로 문제가 생긴다. 이때 이런 IOT기능들을 일괄적으로 처리해주는 스마트기기 시스템을 만들어 관리해줄 수 있다.
위 클래스 다이어그램에서 Colleague에 속하는 스마트 기기는 다음과 같이 정의할 수 있다.
class IoT
{
public:
~IoT() = default;
void setMediator(Mediator * mediator)
{
mediator_ = mediator;
}
protected:
Mediator * mediator_;
};
class Clock : public IoT
{
public:
void Alarm()
{
mediator_->notify("Alarm");
}
};
그렇다면 Mediator인 IoT시스템은 다음과 같이 정의할 수 있다.
class Mediator
{
public:
~Mediator() = default;
virtual void notify(string signal) = 0;
};
class IoTSystem : public Mediator
{
public:
void notify(string signal) override
{
//스마트 기기사이의 상호작용을 처리
}
private:
Clock * clock_;
Light * light_;
Alarm * alarm_;
};
방문자(Visitor)
오브젝트와 오브젝트를 다루는 알고리즘(Visitor)를 나눈다. 이렇게 되면 오브젝트를 수정하지 않고 새로운 기능을 추가할 수 있다. 따라서 OCP를 준수하는 방법 중 하나이기도 하다.
위 UML 다이어그램을 보면 Object Structure (Element의 컬렉션을 포함한 오브젝트)에서 개별 Element에 Visitor를 넘겨주어 기능을 수행한다.
예를 들어, 컴파일러 등에서 Expression을 처리하는 데에 사용될 수 있다.
Expression을 출력하는 PrintVisitor나 값을 계산하는 Visitor를 만들어준 뒤, Root Node에서 해당 Visitor를 Accept 하는 식으로 쓸 수 있다.
오브젝트 자체를 함수에 매개변수로 넘겨주거나, 함수 오브젝트를 오브젝트에 넘겨주는 식으로 Visitor 패턴을 사용하지 않고도 오브젝트 수정 없이 기능 추가가 가능하다.
참고 : 로버트 나이스트롬, 게임 프로그래밍 패턴, 한빛미디어
'이론 > 디자인패턴' 카테고리의 다른 글
서비스 중개자 (0) | 2022.01.16 |
---|---|
이벤트 큐 패턴 (1) | 2022.01.03 |
컴포넌트 패턴 (1) | 2021.12.25 |
디자인 패턴 - 구조 (1) | 2021.12.03 |
디자인 패턴 - 생성 패턴 (0) | 2021.11.27 |