전략 패턴
전략 패턴이란?
•
전략 패턴은 실행 중에 알고리즘을 선택하여 객체의 동작을 실시간으로 변경할 수 있게 하는 행위 디자인 패턴입니다.
특정한 알고리즘을 정의하고 각 알고리즘을 캡슐화하여 각 영역에서 상호 교체 가능하도록 하는 패턴
•
'전략'의 의미
◦
전략은 특정 목표를 수행하기 위한 행동 계획을 말합니다.
◦
알고리즘, 기능, 또는 동작이 될 수 있습니다.
•
적용 상황
◦
어떤 작업을 수행하는 여러 알고리즘이 존재할 때 사용합니다.
◦
알고리즘 변형이 자주 필요한 경우에 적합합니다.
▪
런타임 중에 전환 가능한 여러 알고리즘 변형이 객체 내에 필요할 때.
◦
많은 클래스들이 유사하지만 행동 실행에서 차이가 있을 때.
•
구조는 어떻게 되나요?
◦
전략 알고리즘 객체들: 실제 알고리즘, 행위, 동작을 구현한 객체들
▪
동일 계열의 알고리즘군을 정의
◦
전략 인터페이스: 모든 전략 구현체의 공통 인터페이스
▪
각각의 알고리즘을 캡슐화
▪
전략과 알고리즘 객체들은 OCP 준수
◦
컨텍스트(Context): 선택된 전략 객체의 메소드를 호출하여 알고리즘을 실행
▪
이들을 상호 교환이 가능하도록 만든다
•
합성 형식으로 전략 객체가 들어간다
▪
컨텍스트와 전략 인터페이스 간 DIP 준수
▪
setter를 이용하여 전략 객체가 장착되고 execute를 통해 장착된 전략 객체가 실행됩니다.
◦
클라이언트: 전략을 선택하고 컨텍스트에 전달하여 결과를 얻음
•
컨텍스트(Context)의 의미
◦
프로그래밍에서 컨텍스트는 콘텐츠를 담는 개체를 의미합니다.
◦
객체를 다루기 위한 접근 수단이 됩니다.
•
장점
◦
알고리즘을 캡슐화하여 교체 가능하게 만듦
◦
런타임에 동적으로 알고리즘 변경 가능
◦
코드 중복 감소
◦
새로운 전략 추가가 용이함
◦
확장성과 유지보수성 향상
구현 예제
무기를 다르게 선택할 때마다 발사되는 총알, 연사 속도, 효과음 등이 다르게 설정되는 것을 구현
무기마다 전략이 다름 (돌격 소총, 기관총, 저격총)
만약 우리가 전략 패턴을 몰랐다면 아마 아래와 같이 구현을 할 것 같습니다.
import time
class Player:
def __init__(self):
self.weapon_type = "Pistol"
self.bullet_type = "Small"
self.fire_rate = 0.5
self.sound = "Pew"
def set_weapon(self, weapon_type):
self.weapon_type = weapon_type
if weapon_type == "Pistol":
self.bullet_type = "Small"
self.fire_rate = 0.5
self.sound = "Pew"
elif weapon_type == "Rifle":
self.bullet_type = "Medium"
self.fire_rate = 0.3
self.sound = "Bang"
elif weapon_type == "Machine Gun":
self.bullet_type = "Small"
self.fire_rate = 0.1
self.sound = "Ratatat"
def fire_weapon(self):
print(f"Firing {self.weapon_type}: {self.sound}!")
print(f"Bullet type: {self.bullet_type}")
time.sleep(self.fire_rate)
def main():
player = Player()
# 피스톨 사용
print("Using Pistol:")
for _ in range(3):
player.fire_weapon()
# 라이플로 변경
player.set_weapon("Rifle")
print("\nSwitching to Rifle:")
for _ in range(3):
player.fire_weapon()
# 기관총으로 변경
player.set_weapon("Machine Gun")
print("\nSwitching to Machine Gun:")
for _ in range(5):
player.fire_weapon()
if __name__ == "__main__":
main()
Python
복사
게임이 발전해서 다양한 무기가 추가된다고 했을 때 … 어떤 것들이 우려가 될지 고민해보겠습니다.
1.
Player 내 케이스들이 결합되어 버려 런타임 시, 새로운 무기 타입을 동적으로 추가하기 어렵습니다.
2.
각 무기의 동작을 독립적으로 테스트하기 어렵습니다.
3.
새로운 무기를 추가할 때마다 Player 클래스의 set_weapon 메서드를 수정해야 합니다. (Open Closed Principle 위배)
그렇다면 위 스크립트 형식의 코드를 전략 패턴으로 리팩토링해보자면
•
전략 알고리즘 객체들
◦
Pistol, Rifle, MachineGun 클래스들
◦
이들은 각각 구체적인 무기의 동작을 구현합니다.
•
전략 인터페이스
◦
Weapon 추상 기본 클래스
◦
모든 무기 클래스가 구현해야 하는 공통 인터페이스를 정의합니다.
•
컨텍스트(Context)
◦
Player 클래스
◦
현재 선택된 무기(self.weapon)를 가지고 있으며, 이를 사용하여 fire_weapon() 메서드를 실행합니다.
•
클라이언트
◦
main() 함수
◦
전략(무기)을 생성하고, 컨텍스트(Player)에 전달하여 실행합니다.
from abc import ABC, abstractmethod
import time
# 전략 인터페이스 (추상 기본 클래스)
class Weapon(ABC):
def __init__(self, name, bullet_type, fire_rate, sound):
self.name = name
self.bullet_type = bullet_type
self.fire_rate = fire_rate
self.sound = sound
@abstractmethod
def fire(self):
pass
# 구체적인 전략 클래스들
class Pistol(Weapon):
def __init__(self):
super().__init__("Pistol", "Small", 0.5, "Pew")
def fire(self):
print(f"Firing {self.name}: {self.sound}!")
print(f"Bullet type: {self.bullet_type}")
time.sleep(self.fire_rate)
class Rifle(Weapon):
def __init__(self):
super().__init__("Rifle", "Medium", 0.3, "Bang")
def fire(self):
print(f"Firing {self.name}: {self.sound}!")
print(f"Bullet type: {self.bullet_type}")
time.sleep(self.fire_rate)
class MachineGun(Weapon):
def __init__(self):
super().__init__("Machine Gun", "Small", 0.1, "Ratatat")
def fire(self):
print(f"Firing {self.name}: {self.sound}!")
print(f"Bullet type: {self.bullet_type}")
time.sleep(self.fire_rate)
# 컨텍스트 클래스
class Player:
def __init__(self):
# 전략 인터 페이스를 composition 형태로
self.weapon = None
def set_weapon(self, weapon):
self.weapon = weapon
def fire_weapon(self):
if self.weapon:
self.weapon.fire()
else:
print("No weapon equipped!")
# 클라이언트 코드
def main():
player = Player()
pistol = Pistol()
rifle = Rifle()
machine_gun = MachineGun()
# 피스톨 사용
player.set_weapon(pistol)
print("Using Pistol:")
for _ in range(3):
player.fire_weapon()
# 라이플로 변경
player.set_weapon(rifle)
print("\nSwitching to Rifle:")
for _ in range(3):
player.fire_weapon()
# 기관총으로 변경
player.set_weapon(machine_gun)
print("\nSwitching to Machine Gun:")
for _ in range(5):
player.fire_weapon()
if __name__ == "__main__":
main()
Python
복사
유즈 케이스 톺아보기
1.
Python의 Sort
CPython의 list 객체의 sort 메서드는 Objects/listobject.c 에 구현되어 있습니다.
python으로 약간 추상화해서 대략적으로 소개하자면…
•
sort 메서드가 호출되면, Python은 제공된 key 함수를 확인합니다.
•
key 함수가 없으면 기본 비교 전략을 사용합니다.
•
key 함수가 제공되면, Python은 이를 내부적으로 비교 함수로 변환합니다.
•
정렬 알고리즘은 선택된 전략을 사용하여 요소들을 비교하고 정렬합니다.
class SortStrategy:
def compare(self, a, b):
pass
class DefaultSortStrategy(SortStrategy):
def compare(self, a, b):
return a < b
class KeyFunctionStrategy(SortStrategy):
def __init__(self, key_func):
self.key_func = key_func
def compare(self, a, b):
return self.key_func(a) < self.key_func(b)
class List:
def __init__(self, items):
self.items = items
def sort(self, key=None):
if key is None:
strategy = DefaultSortStrategy()
else:
strategy = KeyFunctionStrategy(key)
self._tim_sort(strategy)
def _tim_sort(self, strategy):
# Timsort 알고리즘 구현
# 요소 비교 시 strategy.compare(a, b) 사용
pass
Python
복사
2.
Scikit-learn의 preprocessing 모듈에서 결측치 처리에 전략 패턴을 사용합니다.
a.
sklearn/impute/_base.py
b.
SimpleImputer 클래스
•
전략 인터페이스: strategy 파라미터를 통해 다양한 결측치 처리 전략을 선택 가능
•
구체적인 전략들: "mean", "median", "most_frequent", "constant", "knn" 등 여러 전략이 구현되어 있습니다.
•
컨텍스트: SimpleImputer 클래스가 컨텍스트 역할을 합니다. 선택된 전략에 따라 적절한 결측치 처리 방법을 적용합니다.
class SimpleImputer(TransformerMixin, BaseEstimator):
def __init__(
self,
*,
missing_values=np.nan,
strategy="mean", # 전략
fill_value=None,
verbose="deprecated",
copy=True,
add_indicator=False,
):
self.missing_values = missing_values
self.strategy = strategy # 전략
self.fill_value = fill_value
self.verbose = verbose
self.copy = copy
self.add_indicator = add_indicator
....
def _impute_one_feature(
self,
feature,
feature_mask,
stat,
neighbor_feat=None,
neighbor_mask=None,
):
if self.strategy == "constant":
return np.where(feature_mask, stat, feature)
if self.strategy in ("mean", "median"):
return np.where(feature_mask, stat, feature)
if self.strategy == "most_frequent":
most_frequent = stat
return np.where(feature_mask, most_frequent, feature)
if self.strategy == "knn":
# ...
return imputed_values
def transform(self, X, y=None):
# ... (code omitted for brevity)
if self.strategy in ("constant", "most_frequent"):
# ... (code omitted for brevity)
X[:, features] = self._impute_one_feature(
X[:, features],
mask_missing_values[:, features],
self.statistics_[features],
)
else:
# ... (code omitted for brevity)
for feat_idx in features:
X[:, feat_idx] = self._impute_one_feature(
X[:, feat_idx],
mask_missing_values[:, feat_idx],
self.statistics_[feat_idx],
)
return X
Python
복사
•
다만 한가지 궁금한 점은 Concrete Strategy ~ ABC Strategy 분리가 되지 않았다는 점
•
새로운 전략을 추가하기 위해 기존 코드를 수정해야 하므로, OCP를 위반
•
client 부분
import numpy as np
from sklearn.impute import SimpleImputer
imputer = SimpleImputer(missing_values=np.nan, strategy='mean')
imputer.fit([[7, 2, 3], [4, np.nan, 6], [10, 5, 9]])
X = [[np.nan, 2, 3], [4, np.nan, 6], [10, np.nan, 9]]
imputer.transform(X)
array([[ 7., 2. , 3. ],
[ 4., 3.5, 6. ],
[10., 3.5, 9. ]])
Python
복사