디자인 패턴 - 전략 패턴 (Strategy Pattern)
파이썬에서 전략(Strategy) 패턴을 배워 봅시다!
전략 패턴 (Strategy Pattern)
전략 패턴(Strategy Pattern)은 행동 패턴(Behavioral Pattern)에 속합니다.비슷한 기능을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있게 만드는 패턴입니다. 프로그램 실행 중에 알고리즘을 선택하고 교체할 수 있습니다.
사용 사례
- 결제 시스템: 신용카드, 현금, 포인트 등 다양한 결제 방식
- 파일 압축: ZIP, RAR 등 다양한 압축 알고리즘
- 게임 캐릭터: 걷기, 뛰기 등 다양한 이동 방식
캐릭터 이동
여기서는 게임 캐릭터의 ‘캐릭터 이동’(걷기, 뛰기 등)을 구현해보면서 전략 패턴에 대해 알아보겠습니다.
상황
게임 캐릭터를 움직여 보도록 하겠습니다. 캐릭터는 걷기, 뛰기 등의 움직임을 수행할 수 있습니다. 이러한 움직임은 게임 진행 중에 자유롭게 변경될 수 있어야 하며, 새로운 움직임을 쉽게 추가할 수 있어야 합니다.
구성
전략 패턴은 다음과 같이 구성요소를 분리합니다.
- 컨텍스트(Context): 전략을 사용하는 객체.
- 전략(Strategy): 실제 알고리즘을 구현한 클래스.
여기서는 움직임 전략을 사용하는 캐릭터 클래스가 컨텍스트, 실제 움직임 알고리즘을 구현하는 클래스들이 전략에 해당합니다.
인터페이스
먼저 모든 움직임 전략이 따라야 할 인터페이스를 정의합니다.
from abc import ABC, abstractmethod
class MovementStrategy(ABC):
@abstractmethod
def move(self):
pass
전략 클래스(Strategy class)
다음으로 구체적인 움직임 전략들을 구현합니다. 각각의 전략은 자신만의 고유한 이동 방식을 구현합니다.
class WalkStrategy(MovementStrategy):
def move(self):
print("캐릭터가 걸어서 이동합니다. 이동속도: 5")
class RunStrategy(MovementStrategy):
def move(self):
print("캐릭터가 뛰어서 이동합니다. 이동속도: 10")
class CrawlStrategy(MovementStrategy):
def move(self):
print("캐릭터가 기어서 이동합니다. 이동속도: 2")
캐릭터
움직임 전략을 사용하는 캐릭터 클래스를 만듭니다. 캐릭터는 현재 설정된 전략에 따라 이동합니다. 그리고 움직임을 변경 할 수 있게, Setter 메서드 set_movement
를 가지고 있습니다.
class Character:
def __init__(self, strategy: MovementStrategy):
self.strategy = strategy
def move(self):
return self.strategy.move()
def set_movement(self, strategy: MovementStrategy):
self.strategy = strategy
실행
이제 실제로 캐릭터를 생성하고 다양한 움직임을 테스트해봅시다.
캐릭터가 가지고 있는 전략에 따라서 같은 이동이 다르게 동작하는 것을 알 수 있습니다. 그리고 Setter 메서드를 통해, 원하는 다른 전략으로 변경할 수 있습니다.
# 캐릭터 생성 (초기 전략: 걷기)
character = Character(WalkStrategy())
# 걷기 실행
character.move() # 출력: 캐릭터가 걸어서 이동합니다. 이동속도: 5
# 전략을 뛰기로 변경
character.set_movement(RunStrategy())
character.move() # 출력: 캐릭터가 뛰어서 이동합니다. 이동속도: 10
# 전략을 기어가기로 변경
character.set_movement(CrawlStrategy())
character.move() # 출력: 캐릭터가 기어서 이동합니다. 이동속도: 2
클래스 다이어그램
%%{ init: { 'theme': 'base', 'themeVariables': { 'primaryColor': '#2a3844', 'lineColor': '#fff', 'primaryTextColor': '#fff', 'tertiaryColor': '#fff' } } }%% classDiagram direction LR class MovementStrategy { <<interface>> +move() } class WalkStrategy { +move() } class RunStrategy { +move() } class CrawlStrategy { +move() } class Character { -strategy: MovementStrategy +\_\_init\_\_(strategy: MovementStrategy) +move() +set_movement(strategy: MovementStrategy) } MovementStrategy <|.. WalkStrategy MovementStrategy <|.. RunStrategy MovementStrategy <|.. CrawlStrategy Character o-- MovementStrategy
전략 패턴의 장점
전략 패턴을 사용하면 다음과 같은 이점이 있습니다.
- 새로운 전략을 추가할 때 기존 코드를 수정하지 않고 추가 가능.
- 프로그램 실행 중에 전략을 자유롭게 변경 가능.
- 각각의 전략이 독립적으로 캡슐화되어 있어 코드 관리가 용이.
전략 패턴 사용 시 주의사항
전략 패턴도 사용 시 고려해야 할 주의사항이 있습니다. 다양한 전략 객체를 사용하는 것은 좋지만, 반대로 무분별하게 전략 객체를 생성하는 경우에는 성능상 문제가 발생 할 수 있습니다.
주의 사항: 메모리 관리
class Character:
def __init__(self):
self._current_strategy = WalkStrategy()
def set_movement(self, movement_type: str):
# 매번 새로운 전략 객체를 생성하는 경우
match movement_type
case "walk":
self._current_strategy = WalkStrategy() # 객체 생성 비용 발생
case "run":
self._current_strategy = RunStrategy() # 객체 생성 비용 발생
해결 방법: 싱글턴 패턴 활용
싱글턴 패턴을 사용하여 각 전략 객체가 단 한 번만 생성되어 재사용하게 만들어 줄 수 있습니다. 아래와 같이 싱글턴 패턴과 팩토리 패턴을 활용하여 객체 생성을 관리 할 수 있습니다.
class MovementStrategyFactory:
_strategies = {} # 클래스 변수로 전략 객체들을 캐싱
@classmethod
def get_strategy(cls, movement_type: str) -> MovementStrategy:
# 이미 생성된 전략이 있다면 재사용
if movement_type not in cls._strategies:
# 처음 요청될 때만 전략 객체 생성 (지연 초기화)
match movement_type
case "walk":
cls._strategies[movement_type] = WalkStrategy()
case "run":
cls._strategies[movement_type] = RunStrategy()
case "crawl":
cls._strategies[movement_type] = CrawlStrategy()
return cls._strategies[movement_type]
# 사용 예시
class Character:
def __init__(self):
# 초기화 시점에 Factory를 통해 전략 획득
self._current_strategy = MovementStrategyFactory.get_strategy("walk")
def set_movement(self, movement_type: str):
# 전략 변경 시에도 Factory를 통해 재사용
self._current_strategy = MovementStrategyFactory.get_strategy(movement_type)
마무리
전략 패턴은 알고리즘의 교체와 확장이 자주 일어나는 상황에서 유용한 디자인 패턴입니다. 특히 개방-폐쇄 원칙(OCP)을 잘 따르는 패턴이며, 확장성 있는 시스템을 설계할 때 고려해볼 만한 패턴입니다.
- 비슷한 역할을 하는 알고리즘군이 존재하는 경우
- 런타임에 알고리즘을 선택하고 교체해야 하는 경우
- 알고리즘의 변형이 자주 발생하는 경우
다양한 정렬 알고리즘, 파일 압축 방식, 할인 정책 등 여러 상황에서 활용할 수 있습니다. 😊