이론/디자인패턴

디자인 패턴 - 행동패턴

tsyang 2021. 12. 12. 15:04

개요

행동 패턴에는 다음 패턴들이 포함되어 있다. (취소선은 다루지 않음)

 

  • 전략(Strategy)
  • 상태(State)
  • 탬플릿 메서드(Template Method)
  • 명령(Command)
  • 책임 연쇄(Chain of Responsibility)
  • 관찰자 (Observer)
  • 메멘토 (Memento)
  • 중재자 (Mediator)
  • 방문자 (Visitor)
  • 반복자 (Iterator)
  • 인터프리터 (Interpreter)

전략패턴, 반복자패턴은 이미 익숙한 개념이므로 패스, Interpreter는 특정 상황(ex. 문자열 명령어 파싱)에 한정적으로 쓰여서 패스.

 

 

상태패턴


 

https://en.wikipedia.org/wiki/State_pattern

 

 

2D 횡스크롤 게임이 있다고 가정하다. 캐릭터는 점프, 앉기, 달리기... 등의 여러 동작이 가능하다. 이런 동작들을 구현할 때 많은 예외에 부딪힐 것이다. 예를 들어, 점프 중에는 점프하거나 엎드릴 수 없다. 이런 경우 FSM을 사용해서 캐릭터의 동작을 처리할 수 있다.

 

FSM은 다음의 특징을 갖는다.

  1. 가질 수 있는 상태는 제한되어 있다.
  2. 한 번에 한 가지 상태만 될 수 있다.
  3. 각 상태에는 전달된 입력이나 이벤트에 따라 다른 상태로 바뀌는 전이(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 함수
};

 

사실 명령 패턴은 클로저를 제대로 지원하는 언어에서 일급 함수로 얼마든지 대체가 가능하다. 명령 패턴의 유용함은 함수형 프로그래밍의 유용함을 보여주는 예이기도 하다.

 

책임 연쇄


http://w3sdesign.com/?gr=b01&ugr=struct

책임 연쇄 패턴은 명령 객체와 처리 객체(Handler)로 구성되어 있다. 처리 객체는 명령 객체를 받아 처리를 하고 다음 처리 객체에 명령을 넘겨줄 지 결정한다.

 

이때 처리 객체들은 일종의 Linked List나 Tree 구조로 이뤄지며 다음으로 명령을 넘길 처리 객체를 가리킬 수 있다.

 

처리 객체들끼리 서로 알 필요가 없기 때문에 커플링이 느슨해진다.

 

데코레이터 패턴과 유사한데, 데코레이터 패턴은 모든 감싼 객체들이 수행되어지는 반면 책임 연쇄 패턴은 처리 객체 중 일부만 수행되며 각각의 처리 객체가 다음 처리 객체를 선택할 수 있다는 점이 다르다.

 


관찰자


https://en.wikipedia.org/wiki/Observer_pattern

굉장히 많이 쓰이는 패턴이다. 

 

게임에서는 특히 업적등을 관리할 때 유용하다. 대상(Subject)는 관찰자(Observer)와 상호작용 하지만 서로 커플링 되어있지 않다는 것이 장점이다.

 

관찰자 패턴이 동기적이라는 점을 주의해야 한다. 대상이 관찰자 메서드를 직접 호출하기 때문에 모든 관찰자의 알림 메서드를 반환하기 전에 다음 코드로 넘어갈 수 없다. 오래 걸리는 작업이 있다면 다른 스레드에 넘기거나 작업 큐를 활용하자.

 

관찰자를 멀티 쓰레드 환경에서 사용할 때는 조심해야 한다. 관찰자가 어떤 대상의 락을 물고있다면 프로그램 전체가 교착상태에 빠질 수 있다. 이런 환경에서는 이벤트 큐를 이용하여 비동기적으로 상호작용 하는게 좋다.

 

또한 등록 취소를 잘 신경써줘야 한다. 객체가 삭제될 때 스스로 등록 취소하도록 하는게 좋다. 그러나 이 방법의 경우 관찰자가 대상들의 목록을 관리해야 하므로 상호 참조가 생겨 복잡성이 늘어난다.

 

업적 <-> 물리엔진 같은 상관없는 분야에서의 상호작용에서 관찰자 패턴은 유용하지만 긴밀히 연관된 코드 덩어리 안에서는 오히려 명시적으로 연결하는 것이 코드를 이해하거나 양쪽 코드의 상호작용을 살펴보는데 더 유용할 수 있다.

 

관찰자 객체를 만드는 것 보다 함수 레퍼런스를 관찰자로 만드는 것이 더 일반적이다.

 

관찰자 패턴을 비동기적으로 쓰려면 이벤트 큐 패턴 참고

https://tsyang.tistory.com/88

 

이벤트 큐 패턴

이게머임? 이벤트 큐 패턴은 메시지나 이벤트를 보내고 처리하는 시점을 분리하는 패턴이다. (시점의 디커플링) 또한 관찰자 패턴을 멀티 쓰레딩 환경에서 사용할 때 클래스끼리 서로 비동기적

tsyang.tistory.com

 

 


메멘토


메멘토 패턴은 캡슐화를 훼손하지 않고 객체의 내부 상태를 캡처하여 저장해두어서 변경 사항을 되돌릴 수 있게 해준다.

 

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;
    }
}

 


 

중재자


http://w3sdesign.com/?gr=b05&amp;ugr=struct

 

중재자 패턴은 오브젝트간의 강한 커플링을 없애줄 수 있다

 

예를 들어, 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를 준수하는 방법 중 하나이기도 하다.

 

https://en.wikipedia.org/wiki/Visitor_pattern

 

위 UML 다이어그램을 보면 Object Structure (Element의 컬렉션을 포함한 오브젝트)에서 개별 Element에 Visitor를 넘겨주어 기능을 수행한다.

 

예를 들어, 컴파일러 등에서 Expression을 처리하는 데에 사용될 수 있다.

 

(1+2)+3

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