스테이트 패턴이란 |
State : 상태
상태패턴이라고도 불림
상태에 따라 객체의 행동을 바꾸는 패턴
상태들을 클래스로 만들고 해당 클래스에서는 해당 상태에서 할 수 있는 행위들을 메서드로 정의
Context가 현재 상태를 확인하고 해당 상태에 동작해야 할 상태클래스에 행동을 위임
객체의 클래스가 바뀌는것과 같은 결과를 만들어냄
스태이트 패턴을 사용하는 상황
- 객체의 행동이 상태에 따라 달라지는 경우
- 런타임에 객체의 상태에 따라 행동이 바뀌어야 하는 경우
- 객체의 상태에 따라 달라지는 분기문이 많은 경우
- 상태 간 전환이 명시적인 경우
스테이트 패턴의 구조 |
1) Context
- 여러 상태를 가질수 있는 객체
- State를 이용하는 역할
- 현재 시스템의 상태를 나타내는 state변수를 갖는다.
- 각 Concrete State 클래스에서 상태 변경을 요청할 수 있도록 setState함수를 제공한다.
- request()함수는 실제 동작을 실행하는것이 아니라, 해당 상태 클래스의 객체에게 실제 동작을 위임한다.
2) State
- 시스템의 모든 상태에 공통의 인터페이스 제공
- State 인터페이스를 상속받아 구현한 모든 상태클래스들은 기존 상태 클래스를 대신해서 사용될 수 있는 것
3) State1, 2, 3
- Concrete State 클래스로 Context 객체가 요청한 작업을 자신의 방식대로 실제로 실행하는 역할
- 자신이 수행할 동작을 마친 후 다음 상태를 결정해서 Context의 setState로 상태변경을 요청하는 역할도 수행함
스테이트 패턴이 없다면? |
노트북의 전원버튼을 예시로 든다면
1) 전원이 켜져있는 상태 : 전원버튼을 누르면 전원이 꺼진다.
2) 전원이 꺼져있는 상태 : 전원버튼을 누르면 전원이 켜진다.
3) 절전모드인 상태 : 전원버튼을 누르면 전원이 켜진다.
#include <iostream>
using namespace std;
class Laptop
{
private:
string power_state = "";
public:
const string ON = "on";
const string OFF = "off";
const string SAVING = "saving";
Laptop() { setPowerState(OFF); }
void setPowerState(string state) { power_state = state; }
void PushPowerBtn() {
// 전원이 켜져있을때 버튼을 누르면 꺼진다.
if (power_state == ON) cout << "system off\n";
// 전원이 꺼져있을때 버튼을 누르면 켜진다.
else if (power_state == OFF) cout << "system on\n";
else if (power_state == SAVING) cout << "절전모드 해제\n";
}
};
void main()
{
Laptop laptop;
laptop.PushPowerBtn();
laptop.setPowerState("on");
laptop.PushPowerBtn();
laptop.setPowerState("saving");
laptop.PushPowerBtn();
laptop.setPowerState("off");
laptop.PushPowerBtn();
laptop.setPowerState("on");
}
결과)
여기서 상태가 추가된다면 클래스 내부의 분기문을 추가해줘야 하므로 OCP원칙을 위배하게 된다.
또한 상태가 아주 많아진다면 분기문이 길어지므로 가독성이 떨어지고
버튼이 Laptop에만 있는게 아니라 TV나 스마트폰등에도 있다면 기능변경이 생길때마다 각 클래스를 다 찾아다니며 수정해야한다.
스테이트 패턴을 사용한다면 |
스테이트 패턴을 적용한다면 각 상태들을 클래스로 만들게 된다.
즉 ON클래스, OFF클래스, SAVING클래스를 만들고 이 클래스들을 STATE클래스로 묶는다.
그리고 Laptop이 상태인터페이스의 메서드를 호출하면 각 상태클래스에서 정의된 행위가 수행된다.
#include <iostream>
using namespace std;
/*
State 추상클래스
*/
class PowerState
{
public:
virtual void PushPowerBtn() {};
};
/*
concrete State클래스들
*/
class ON : public PowerState
{
void PushPowerBtn() { cout << "system off\n"; }
};
class OFF : public PowerState
{
void PushPowerBtn() { cout << "system on\n"; }
};
class SAVING : public PowerState
{
void PushPowerBtn() { cout << "절전모두 해제\n"; }
};
/*
Context 클래스(여기선 Laptop)
여러 상태를 가질 수 있는 객체이며
각 상태마다 행동이 달라지는 객체
*/
class Laptop
{
private:
PowerState* state;
public:
Laptop() { state = new OFF(); }
void setPowerState(PowerState* powerstate) { state = powerstate; }
void PushPowerBtn() { state->PushPowerBtn(); }
};
void main()
{
Laptop laptop;
ON* on = new ON();
OFF* off = new OFF();
SAVING* saving = new SAVING();
laptop.PushPowerBtn();
laptop.setPowerState(on);
laptop.PushPowerBtn();
laptop.setPowerState(saving);
laptop.PushPowerBtn();
laptop.setPowerState(off);
laptop.PushPowerBtn();
}
장단점 |
1) 장점
- 서로 다른 상태에 대한 행동을 별도의 객체로 관리할 수 있음
- 새로운 상태가 추가되더라도 context 객체가 받는 영향이 적음
- 하나의 객체에 대한 여러 동작을 구현해야 할 때 상태변수만 변경하면 동작의 추가/삭제/수정이 간단해짐
- 상태에 따른 조건분기문이 줄어들어 가독성이 좋아진다.
- 특정 하나의 상태에 대한 행동양식이 하나의 클래스에 모여있으므로 유지보수가 쉽다.
2) 단점
- 조건문을 대신해 클래스가 증가하므로 클래스의 수가 늘어나 유지보수가 오히려 힘들수도 있다.
- 상태에 따라 변하는 메서드의 숫자가 적으면 쓸데없이 복잡성만 증가한다.
※ 참고 문헌