디자인 패턴/GoF

[디자인 패턴] 행동 패턴 (4) 상태 State

로파이 2021. 2. 3. 15:13

상태 (State) 패턴

 

1. 의도

  • 객체 상태에 따라 행동이 다르고 행동은 다른 상태 전이를 발생시킬때 사용하는 패턴.

2. 활용

상태 패턴 예시

- TCP 연결

  • 네트워크 연결을 추상화한 TCPConnection 클래스를 생각해보면 TCPConnection 클래스는 여러가지 상태를 가질 수 있다.
  • 연결 성공(Established), 대기(Listening), 연결 종료(Closed) 상태를 가지는데 TCP-Connection 객체는 자신의 상태에 따라 사용자 요청에 대한 처리 결과가 다르다.
  • ex) Open 요청은 종료 상태에서만 의미가 있을 수 있고 Acknowledge() 요청은 전송된 패킷을 성공적으로 수신했다는 패킷을 보내는 것이므로 대기나 연결 성공 상태에서 의미가 있는 연산이다.

- TCPState 추상 클래스

  • TCP 추상 클래스는 네트워크 연결에서 나타나는 모든 상태를 표현하고 가능한 요청에 대한 인터페이스를 제공한다.
  • TCPState의 서브 클래스는 각 자신의 상태에 따른 요청 방식을 구체적으로 구현하도록 한다.

- TCPConnection 클래스

  • TCPConnection 클래스는 사용자의 인터페이스에 가까운 클래스로 TCPState 인스턴스를 가짐으로 자신의 상태를 표현하고 TCPState의 인터페이스와 같기 때문에 실제 구현은 TCPState 인스턴스의 연산을 이용한다.
  • 행동 결과에 따라 TCPState 서브 클래스 중 하나의 인스턴스로 변경한다.

 

다음 상황에서 상태 패턴을 사용한다.

  • 객체의 행동이 상태에 따라 달라지고 상태에 따라서 런타임에 행동이 바뀌어야한다.
  • 상태에 따라 행동을 선택해야하는데 이를 위한 분기 조건 (if-else, switch)이 많이 사용될 때

 

3. 참여자

행동 패턴 - 상태 패턴

  • Context: 사용자가 관심있는 인터페이스르 정의
  • State: Context의 각 상태별로 필요한 행동을 캡슐화하여 인터페이스로 정의
  • ConcreteState 서브클래스: 각 서브 클래스들은 Context의 상태에 따라 처리되어야할 실제 행동을 구현

4. 협력방법

  • Context가 요청을 받으면 현재의 ConcreteState 객체에 요청을 전달한다.
  • Context클래스는 실제 연산을 처리할 State 객체에 자신을 매개변수로 전달한다. (자신에 대한 정보 접근을 위해)
  • Context 클래스는 사용자가 사용할 수 있는 기본 인터페이스를 제공한다.
  • Context 클래스 또는 CocnreteState 서브클래스는 다음 자기상태가 무엇이고, 어떤 환경에서 다음 상태로 가는 지 결정 가능하다.

5. 결과

 

1) 상태에 따른 행동을 제한하고 서로 다른 상태에 대한 행동을 별도의 객체로 관리한다.

  • 새로운 상태와 전이 규칙이 있다면 새로운 서브클래스만 정의하면 된다.
  • 상태 종류가 많아지면 서브 클래스가 많아지지만 상태 클래스를 두지 않는다면 조건문 검사도 많이 필요하게 된다.

2) 상태 전이를 명확하게 만든다.

  • 상태를 내부 일반 데이터로 표현하지 않고 객체로 관리하며 행동 구현이 되어 있기 때문에 상태 전이를 명확하게 할 수 있다.

3) 상태 객체는 공유될 수 있다.

  • 모든 Context에 공통적으로 적용될 수 있는 상태이기 때문에 같이 공유할 수 있다.

6. 구현

 

1) 누가 상태 전이를 정할 것인가

  • State의 서브 클래스들이 다음 상태를 알고 있으므로 State가 전이를 담당한다. 이 경우 State의 상태 전이 연산에 Context를 매개 변수로 전달하여 Context의 상태를 변경할 수 있도록 한다.

2) 테이블 기반

  • 상태와 전이된 상태를 관리한다. 조건식보다는 괜찮지만 여전히 테이블 탐색을 해야하고 전이된 상태를 바로 알기 힘들기 때문에 상태 전이에 따른 처리 동작을 추가하기 힘들다.

3) 상태 객체의 생성과 소멸

  • 상태 객체를 필요할 때만 생성하고 필요 없게 되면 없애거나 필요하기 전에 미리 만들어두고 없애지 않고 계속 두는 방법.
  • State 객체가 정보가 많다면 사용하는 객체만 생성한다. 반대로 상태 전이가 빈번하다면 생성-삭제에 오버헤드가 있기 때문에 미리 만들어두고 공유하는 방법이 좋다.

4) 동적 상속

  • 클래스를 아에 변경해서 상태를 변경한다. 거의 사용되지 않는다.

예제 코드

 

TCP 프로토콜을 사용한 네트워크 연결 수립 과정 중 나타나는 TCPState와 서브클래스를 구현해보고 TCPConnection 클래스는 사용자 인터페이스르 제공한다.

 

연관 내용: TCP의 3-way handshaking 연결 수립과정  

2021/01/06 - [Computer Network] - [네트워크] 전송 계층 (2) TCP 프로토콜 / 혼잡 제어

 

TCPConnection.h

  • TCPConnection 클래스는 사용자 인터페이스를 제공하며 Open(), Close(), Send()와 같은 연산과 Acknowldeg(), Synchronize()와 같이 ACK, SYN를 위한 더 작은 단위의 연산이 있다.
  • ProcessOctect()은 연결되어 있는 소켓을 통해 패킷을 전송한다.
  • TCPState를 friend 클래스로 지정하여 TCPState 서브 클래스가 자신의 ChangeState 메서드를 사용하도록 허락하여 자신의 TCPState 상태를 변경하도록 한다.
class TCPOctetStream;
class TCPState;

class TCPConnection {
public:
	TCPConnection();

	// 상태 인스턴스의 연산을 이용
	void ActiveOpen()		{ _state->ActiveOpen(this); }
	void PassiveOpen()		{ _state->PassiveOpen(this); }
	void Close()			{ _state->Close(this); }
	void Send()				{ _state->Send(this); }
	void Acknowledge()		{ _state->Acknowledge(this); }
	void Synchronize()		{ _state->Synchronize(this); }

	// 소켓 전송을 위한 데이터 처리
	void ProcessOctet(TCPOctetStream*);
private:
	// TCPState 인스턴스가 자신의 ChangeState를 접근할 수 있게함
	friend class TCPState;
	// 상태 변경
	void ChangeState(TCPState* s) { _state = s; }
private:
	TCPState* _state;
};

 

TCPState.h

  • 모든 연산의 기본 구현은 비워두고 상태에 따른 행동을 자신의 서브 클래스가 구현하도록 한다.
  • TCPConnection과 같은 인터페이스를 제공하며 모든 상태를 구현할 필요는 없기 때문에 순수 가상 함수로 정의하지 않는다.
  • ChangeState()는 TCPConnection 인스턴스와 다음 상태를 매개변수로 받아 상태를 변경한다.
class TCPState
{
public:
	// 기본 구현은 없음 (상태마다 제공하는 연산이 다름)
	virtual void Transmit(TCPConnection*, TCPOctetStream*){}
	virtual void ActiveOpen(TCPConnection*){}
	virtual void PassiveOpen(TCPConnection*){}
	virtual void Close(TCPConnection*){}
	virtual void Synchronize(TCPConnection*){}
	virtual void Acknowledge(TCPConnection*){}
	virtual void Send(TCPConnection*){}
protected:
	void ChangeState(TCPConnection* t, TCPState* s) { t->ChangeState(s); }
};

 

TCPState의 서브 클래스

 

TCPState 상태 전이 테이블

 

기본 상태 전이는 위와 같이 표현할 수 있다.

이에 따른 서브 클래스의 각 상태 전이를 동반하는 함수를 구현하도록 한다.

TCPState는 어떤 TCPConnection 인스턴스가 사용하던지 상관 없기 때문에 단일체로 구현하여 공유할 수 있다.

 

- TCPClosed.h

기본 상태는 닫힘 상태에서 시작한다.

  • PassiveOpen()을 통해 TCPListen 상태로 전이 가능하다.
  • ActiveOpen()은 중간 SYN와 SYNACK를 받는 과정을 포함하고 바로 연결 수립 상태로 전이가능하다.
class TCPClosed : public TCPState
{
public:
	static TCPClosed* Instance();

	virtual void ActiveOpen(TCPConnection*);
	virtual void PassiveOpen(TCPConnection*);
	// ...
private:
	TCPClosed();
	static TCPClosed* _instance;
};

TCPClosed* TCPClosed::_instance = 0;

void TCPClosed::ActiveOpen(TCPConnection* t)
{
	// send SYN, receive SYNACK
	ChangeState(t, TCPEstablished::Instance());
}

void TCPClosed::PassiveOpen(TCPConnection* t)
{
	// socket listens to connect
	ChangeState(t, TCPListen::Instance());
}

 

- TCP Listen.h

  • TCP 소켓이 연결 수립되기 전 SYN 메세지를 보내는 중간 단계이다.
  • Send()를 통해 SYN를 전송, SYNACK를 수신받도록하고 연결 수립이 될 수 있다.
class TCPListen : public TCPState
{
public:
	static TCPState* Instance();
	virtual void Send(TCPConnection*);
	// ...
private:
	TCPListen();
	static TCPState* _instance;
};

TCPState* TCPListen::_instance = 0;

void TCPListen::Send(TCPConnection* t)
{
	// send SYN, receive SYNACK
	ChangeState(t, TCPEstablished::Instance());
}

 

- TCPEstablished.h

  • Transmit() 전송은 보낼 패킷을 소켓을 통해 보내는 과정이며 연결 상태를 유지한다.
  • 소켓을 닫는 Close() 연산시 TCPClosed 상태가 된다. 
class TCPEstablished : public TCPState
{
public:
	static TCPState* Instance();

	virtual void Transmit(TCPConnection*, TCPOctetStream*);
	virtual void Close(TCPConnection*);
private:
	static TCPEstablished* _instantce;
};

TCPEstablished* TCPEstablished::_instantce = 0;

void TCPEstablished::Transmit(TCPConnection* t, TCPOctetStream* o)
{
	t->ProcessOctet(o);
}

void TCPEstablished::Close(TCPConnection* t)
{
	// send FIN, receive ACK of FIN
	ChangeState(t, TCPListen::Instance());
}