Search

상태 패턴 (디자인 패턴)

상태 패턴

정의

상태 패턴 정의
객체의 내부 상태에 따라 행위를 변경할 수 있게 하는 패턴
(중요) 상태를 객체화하여 상태별 행동을 위임하는 방식
각 상태 클래스들은 싱글톤으로 구현될 필요가 있다
왠만한 상황에선 각 상태는 새로 인스턴스화 할 필요가 전혀 없다. 괜히 메모리 낭비인 셈이다. 따라서 각 상태 클래스들을 싱글톤화 시킨다.
목적
조건문 사용을 줄이고 객체 지향적인 방식으로 상태 변화 관리
상태별 행동을 분리하여 코드의 유지보수성 향상
주요 특징
상태를 클래스로 표현
객체의 상태 변화를 클래스 교체로 구현
각 상태에 특화된 행동을 해당 상태 클래스에서 구현
구조
컨텍스트(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
pytransitions
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 객체는 해당 triggerEvent 객체에 추가됩니다.
파일: 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
사용자 정의 모델에 상태 관련 기능을 추가하는 데 사용됩니다.

Reference