상태 패턴
정의
•
상태 패턴 정의
◦
객체의 내부 상태에 따라 행위를 변경할 수 있게 하는 패턴
◦
(중요) 상태를 객체화하여 상태별 행동을 위임하는 방식
◦
각 상태 클래스들은 싱글톤으로 구현될 필요가 있다
▪
왠만한 상황에선 각 상태는 새로 인스턴스화 할 필요가 전혀 없다. 괜히 메모리 낭비인 셈이다. 따라서 각 상태 클래스들을 싱글톤화 시킨다.
•
목적
◦
조건문 사용을 줄이고 객체 지향적인 방식으로 상태 변화 관리
◦
상태별 행동을 분리하여 코드의 유지보수성 향상
•
주요 특징
◦
상태를 클래스로 표현
◦
객체의 상태 변화를 클래스 교체로 구현
◦
각 상태에 특화된 행동을 해당 상태 클래스에서 구현
•
구조
◦
컨텍스트(Context):
이 클래스는 현재 상태 객체에 대한 참조를 유지하고 상태별 동작을 해당 상태 객체에 위임합니다.
◦
상태(State):
상태 인터페이스는 특정 상태와 관련된 행동을 캡슐화하는 일련의 메서드를 정의합니다.
◦
구체적 상태(Concrete States):
이들은 상태 인터페이스를 구현하는 클래스들입니다. 각 구체적 상태는 컨텍스트의 특정 상태를 나타내며, 상태별 행동에 대한 고유한 구현을 제공합니다.
•
장점
◦
새로운 상태와 행동 추가가 용이
◦
기존 코드 수정 없이 상태별 동작 변경 가능
◦
상태 전이 로직을 중앙화하여 관리 가능
•
전략 패턴 과의 비교
◦
클래스 다이어그램 구조가 유사
◦
목적:
▪
전략 패턴: 알고리즘을 런타임에 교체 가능하게 캡슐화
▪
상태 패턴: 객체의 내부 상태 변화에 따른 행동 관리
◦
객체 간 관계:
▪
전략 패턴: 전략 객체들은 서로 독립적, Context를 알 필요 없음
▪
상태 패턴: 상태 객체들은 서로를 알 수 있고, 상태 전환 담당 가능
◦
전환 로직:
▪
전략 패턴: 주로 외부에서 전략 변경
▪
상태 패턴: 상태 객체 내에서 다른 상태로의 전환 로직 포함 가능
◦
확장성:
▪
상태 패턴은 전략 패턴의 확장으로 볼 수 있음
◦
클래스 수:
▪
상태 패턴에서 모든 행동을 별도 상태 클래스로 만들면 클래스 수가 급증할 수 있음
◦
인스턴스 관리:
▪
전략 패턴: 다양한 입력에 따라 전략이 변할 수 있어 인스턴스로 구성
▪
상태 패턴: 정의된 상태 간 전환이 주요하므로 메모리 절약을 위해 싱글톤으로 구성
•
간단 구현
from abc import ABC, abstractmethod
class AbstractState(ABC):
@abstractmethod
def handle(self, context):
pass
class ConcreteStateA(AbstractState):
_instance = None
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = cls()
return cls._instance
def handle(self, context):
print("처리 - 상태 A")
# 상태 A에서의 특정 동작 수행
# 필요시 상태 변경
context.set_state(ConcreteStateB.get_instance())
class ConcreteStateB(AbstractState):
_instance = None
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = cls()
return cls._instance
def handle(self, context):
print("처리 - 상태 B")
# 상태 B에서의 특정 동작 수행
# 예: 전원 on 상태에서 끄기 동작 실행 후 상태 변경
context.set_state(ConcreteStateC.get_instance())
class ConcreteStateC(AbstractState):
_instance = None
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = cls()
return cls._instance
def handle(self, context):
print("처리 - 상태 C")
# 상태 C에서의 특정 동작 수행
class Context:
def __init__(self):
self._state = None
def set_state(self, state):
self._state = state
def request(self):
if self._state:
self._state.handle(self)
else:
print("상태가 설정되지 않았습니다.")
# 사용 예시
if __name__ == "__main__":
context = Context()
# 초기 상태 설정
context.set_state(ConcreteStateA.get_instance())
# 상태 변화에 따른 동작 실행
context.request() # A 상태 처리 및 B로 전환
context.request() # B 상태 처리 및 C로 전환
context.request() # C 상태 처리
Python
복사
•
적용 예시
◦
TV의 전원 상태에 따른 동작 변화
◦
주문 처리 시스템의 주문 상태별 처리 로직
•
핵심 아이디어
◦
무형의 개념(상태)도 클래스로 표현 가능
◦
상태 변화에 따른 행동 변경을 객체 지향적으로 구현
간단한 예제
ecommerce 쇼핑 결제 관련 예제
# Step 1: Define the State Interface
class CheckoutState:
def add_item(self, item):
pass
def review_cart(self):
pass
def enter_shipping_info(self, info):
pass
def process_payment(self):
pass
# Step 2: Create Concrete State Classes
class EmptyCartState(CheckoutState):
def add_item(self, item):
print("Item added to the cart.")
return ItemAddedState()
def review_cart(self):
print("Cannot review an empty cart.")
def enter_shipping_info(self, info):
print("Cannot enter shipping info with an empty cart.")
def process_payment(self):
print("Cannot process payment with an empty cart.")
class ItemAddedState(CheckoutState):
def add_item(self, item):
print("Item added to the cart.")
def review_cart(self):
print("Reviewing cart contents.")
return CartReviewedState()
def enter_shipping_info(self, info):
print("Cannot enter shipping info without reviewing the cart.")
def process_payment(self):
print("Cannot process payment without entering shipping info.")
class CartReviewedState(CheckoutState):
def add_item(self, item):
print("Cannot add items after reviewing the cart.")
def review_cart(self):
print("Cart already reviewed.")
def enter_shipping_info(self, info):
print("Entering shipping information.")
return ShippingInfoEnteredState(info)
def process_payment(self):
print("Cannot process payment without entering shipping info.")
class ShippingInfoEnteredState(CheckoutState):
def add_item(self, item):
print("Cannot add items after entering shipping info.")
def review_cart(self):
print("Cannot review cart after entering shipping info.")
def enter_shipping_info(self, info):
print("Shipping information already entered.")
def process_payment(self):
print("Processing payment with the entered shipping info.")
# Step 3: Create the Context Class
class CheckoutContext:
def __init__(self):
self.current_state = EmptyCartState()
def add_item(self, item):
self.current_state = self.current_state.add_item(item)
def review_cart(self):
self.current_state = self.current_state.review_cart()
def enter_shipping_info(self, info):
self.current_state = self.current_state.enter_shipping_info(info)
def process_payment(self):
self.current_state.process_payment()
# Step 4: Example of Usage
if __name__ == "__main__":
cart = CheckoutContext()
cart.add_item("Product 1")
cart.review_cart()
cart.enter_shipping_info("123 Main St, City")
cart.process_payment()
Python
복사
상태 패턴의 10가지 실제 사용 사례
상태 패턴은 전자상거래 외에도 다양한 분야에서 응용됩니다.
1.
문서 편집: 편집, 저장, 인쇄 등 문서의 상태를 추적합니다.
2.
미디어 플레이어: 재생, 일시정지, 정지와 같은 재생 상태를 관리합니다.
3.
교통 신호등: 녹색에서 적색으로 변하는 등 교통 신호등의 상태를 제어합니다.
4.
자판기: 자판기 거래의 상태를 처리합니다.
5.
게임 캐릭터: 게임 캐릭터의 행동에 따른 상태를 관리합니다.
6.
워크플로우 관리: 기업 환경에서의 승인 워크플로우와 같은 프로세스의 흐름을 나타냅니다.
7.
채팅 애플리케이션: 온라인, 오프라인, 자리비움과 같은 사용자 상태를 처리합니다.
8.
예약 시스템: 다양한 단계를 통한 예약 및 예매 관리를 합니다.
9.
휴대전화 상태: 전원 켜짐, 꺼짐, 비행기 모드와 같은 전원 상태를 처리합니다.
10.
로봇공학: 다양한 작업 중 로봇의 행동을 제어합니다.
오픈 소스 구현 예제
transitions 라이브러리는 상태 기계(State Machine)를 쉽게 구현할 수 있게 해줍니다 (말 그대로 상태 구현을 위한 라이브러리)
from transitions import Machine
# The states
states=['solid', 'liquid', 'gas', 'plasma']
# And some transitions between states. We're lazy, so we'll leave out
# the inverse phase transitions (freezing, condensation, etc.).
transitions = [
{ 'trigger': 'melt', 'source': 'solid', 'dest': 'liquid' },
{ 'trigger': 'evaporate', 'source': 'liquid', 'dest': 'gas' },
{ 'trigger': 'sublimate', 'source': 'solid', 'dest': 'gas' },
{ 'trigger': 'ionize', 'source': 'gas', 'dest': 'plasma' }
]
# Initialize
machine = Machine(lump, states=states, transitions=transitions, initial='liquid')
# Now lump maintains state...
lump.state
>>> 'liquid'
# And that state can change...
# Either calling the shiny new trigger methods
lump.evaporate()
lump.state
>>> 'gas'
# Or by calling the trigger method directly
lump.trigger('ionize')
lump.state
>>> 'plasma'
Python
복사
•
핵심 상태 machine 로직
trigger 메서드가 호출되면, 해당하는 Event 객체를 찾아 현재 상태에서 가능한 전이를 확인하고, 조건을 만족하면 상태를 변경
◦
과정
1.
각 딕셔너리는 하나의 전이로 해석됩니다.
2.
trigger는 이벤트 이름이 됩니다.
3.
source는 시작 상태, dest는 목적지 상태가 됩니다.
4.
각 전이에 대해 Transition 객체가 생성됩니다.
5.
생성된 Transition 객체는 해당 trigger의 Event 객체에 추가됩니다.
◦
파일: transitions/core.py
◦
클래스: Machine
◦
이 클래스가 상태 패턴의 핵심 로직을 구현합니다.
class Machine:
# 여기서 전이(transitions)가 초기화되고 처리
def __init__(self, model='self', ..., transitions=None, ...):
# ...
if transitions:
self.add_transitions(transitions)
# 개별 전이를 추가하고 트리거 메서드를 생성합니다.
def add_transition(self, trigger, source, dest, ..., conditions=None, **kwargs):
"""Create a new Transition instance and add it to the internal list."""
if trigger == self.model_attribute:
raise ValueError("Trigger name cannot be same as model attribute name.")
if trigger not in self.events:
self.events[trigger] = self._create_event(trigger, self)
for model in self.models:
self._add_trigger_to_model(trigger, model)
if source == self.wildcard_all:
source = list(self.states.keys())
else:
# states are checked lazily which means we will only raise exceptions when the passed state
# is a State object because of potential confusion (see issue #155 for more details)
source = [s.name if isinstance(s, State) and self._has_state(s, raise_error=True) or hasattr(s, 'name') else
s for s in listify(source)]
for state in source:
if dest == self.wildcard_same:
_dest = state
elif dest is not None:
if isinstance(dest, State):
_ = self._has_state(dest, raise_error=True)
_dest = dest.name if hasattr(dest, 'name') else dest
else:
_dest = None
_trans = self._create_transition(state, _dest, conditions, unless, before,
after, prepare, **kwargs)
self.events[trigger].add_transition(_trans)
# 전이를 추가
def add_transitions(self, transitions):
""" Add transitions to the machine and triggers to the model. """
for trans in listify(transitions):
self.add_transition(**trans)
Python
복사
•
상태 클래스:
◦
파일: transitions/core.py
◦
클래스: State
◦
각 상태를 표현하는 클래스입니다.
•
전이 클래스:
◦
파일: transitions/core.py
◦
클래스: Transition
◦
상태 간 transition을 표현하는 클래스입니다.
▪
개별 전이의 로직을 정의
class Transition:
def execute(self, event_data):
# ...
# 조건 확인, 콜백 실행 등
# ...
event_data.machine.set_state(self.dest, event_data.model)
# ...
Python
복사
•
이벤트 클래스:
◦
파일: transitions/core.py
◦
클래스: Event
◦
상태 전이를 트리거하는 이벤트를 표현합니다.
▪
트리거와 관련된 전이를 관리
class Event:
def __init__(self, name, machine):
self.name = name
self.machine = machine
self.transitions = defaultdict(list)
# Event 객체에 전이가 추가
def add_transition(self, transition):
self.transitions[transition.source].append(transition)
...
# 실제 상태 전이를 수행
def trigger(self, model, *args, **kwargs):
func = partial(self._trigger, model, *args, **kwargs)
return self.machine._process(func)
def _trigger(self, model, *args, **kwargs):
state = self.machine.get_model_state(model)
if state.name not in self.transitions:
msg = "%sCan't trigger event %s from state %s!" % (self.machine.name, self.name, state.name)
raise MachineError(msg)
event_data = EventData(state, self, self.machine, model, args=args, kwargs=kwargs)
return self._process(event_data)
Python
복사
•
상태 모델:
◦
파일: transitions/core.py
◦
클래스: StateModel
◦
사용자 정의 모델에 상태 관련 기능을 추가하는 데 사용됩니다.