디자인 패턴 기초
**디자인 패턴(Design Pattern)**은 소프트웨어 설계에서 자주 발생하는 문제에 대한 재사용 가능한 해결책입니다. "바퀴를 다시 발명하지 말라"는 원칙처럼, 검증된 패턴을 활용하면 코드 품질을 높이고 팀원들과의 소통도 쉬워집니다.
Singleton 패턴
**싱글톤(Singleton)**은 클래스의 인스턴스를 오직 하나만 생성하고, 어디서든 그 인스턴스에 접근할 수 있게 하는 패턴입니다. 설정 관리, 로거, DB 연결 풀 등에 사용됩니다.
방법 1: __new__ 오버라이딩
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, name: str = "default"):
# __init__은 매번 호출되므로 이미 초기화됐는지 확인
if not hasattr(self, "_initialized"):
self.name = name
self._initialized = True
s1 = Singleton("first")
s2 = Singleton("second")
print(s1 is s2) # True — 동일한 객체
print(s1.name) # first (s2는 새 인스턴스가 아님)
print(id(s1) == id(s2)) # True
방법 2: 모듈 레벨 (가장 Python스러운 방법)
# config.py 모듈로 분리하면 자연스러운 싱글톤
# Python 모듈은 처음 import 시 한 번만 실행되고 이후 캐싱됨
class AppConfig:
def __init__(self):
self.debug = False
self.db_url = "sqlite:///app.db"
self.secret_key = "change-me"
self.allowed_hosts: list[str] = ["localhost"]
def update(self, **kwargs) -> None:
for key, value in kwargs.items():
if hasattr(self, key):
setattr(self, key, value)
else:
raise KeyError(f"알 수 없는 설정: {key}")
# 모듈 레벨에서 인스턴스 생성 — 사실상 싱글톤
config = AppConfig()
# 사용처에서는 import만 하면 됨
# from config import config
# config.debug = True
방법 3: Metaclass 활용
class SingletonMeta(type):
"""싱글톤 동작을 부여하는 메타클래스"""
_instances: dict = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class DatabasePool(metaclass=SingletonMeta):
"""DB 연결 풀 — 애플리케이션에 하나만 존재"""
def __init__(self, max_connections: int = 10):
self.max_connections = max_connections
self._connections: list = []
print(f"DB 연결 풀 초기화 (최대 {max_connections}개)")
def get_connection(self):
if len(self._connections) < self.max_connections:
conn = f"conn_{len(self._connections) + 1}"
self._connections.append(conn)
return conn
raise RuntimeError("연결 풀이 가득 찼습니다.")
def release(self, conn) -> None:
self._connections.remove(conn)
def status(self) -> str:
return f"사용 중: {len(self._connections)}/{self.max_connections}"
class Logger(metaclass=SingletonMeta):
"""애플리케이션 전역 로거 — 싱글톤"""
def __init__(self):
self._logs: list[str] = []
def info(self, msg: str) -> None:
import datetime
entry = f"[INFO {datetime.datetime.now():%H:%M:%S}] {msg}"
self._logs.append(entry)
print(entry)
def warning(self, msg: str) -> None:
import datetime
entry = f"[WARN {datetime.datetime.now():%H:%M:%S}] {msg}"
self._logs.append(entry)
print(entry)
def get_all(self) -> list[str]:
return list(self._logs)
# 싱글톤 동작 확인
pool1 = DatabasePool(5)
pool2 = DatabasePool(100) # 이미 생성된 인스턴스 반환, 인자 무시
print(pool1 is pool2) # True
print(pool1.max_connections) # 5 (pool2로 바뀌지 않음)
logger1 = Logger()
logger2 = Logger()
logger1.info("서버 시작")
logger2.warning("메모리 부족") # logger1과 같은 객체
print(logger1 is logger2) # True
print(len(logger1.get_all())) # 2 (두 개 다 같은 로거에 기록됨)
Factory 패턴
팩토리(Factory) 패턴은 객체 생성 로직을 분리하여 클라이언트 코드가 구체적인 클래스를 몰라도 되게 합니다.
Simple Factory
from abc import ABC, abstractmethod
class Animal(ABC):
def __init__(self, name: str):
self.name = name
@abstractmethod
def speak(self) -> str: ...
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.name!r})"
class Dog(Animal):
def speak(self) -> str:
return f"{self.name}: 왈왈!"
class Cat(Animal):
def speak(self) -> str:
return f"{self.name}: 야옹~"
class Bird(Animal):
def speak(self) -> str:
return f"{self.name}: 짹짹!"
class AnimalFactory:
"""Simple Factory: 정적 메서드로 객체 생성"""
_creators = {
"dog": Dog,
"cat": Cat,
"bird": Bird,
}
@classmethod
def create(cls, animal_type: str, name: str) -> Animal:
creator = cls._creators.get(animal_type.lower())
if creator is None:
available = ", ".join(cls._creators)
raise ValueError(f"알 수 없는 동물: {animal_type!r}. 가능한 타입: {available}")
return creator(name)
@classmethod
def register(cls, animal_type: str, creator: type) -> None:
"""새 동물 타입 등록 — OCP 적용"""
cls._creators[animal_type.lower()] = creator
# 사용
animals = [
AnimalFactory.create("dog", "바둑이"),
AnimalFactory.create("cat", "나비"),
AnimalFactory.create("bird", "짹짹이"),
]
for animal in animals:
print(animal.speak())
# 새 동물 추가 — AnimalFactory 코드 수정 없음
class Rabbit(Animal):
def speak(self) -> str:
return f"{self.name}: 토토~"
AnimalFactory.register("rabbit", Rabbit)
bunny = AnimalFactory.create("rabbit", "뭉치")
print(bunny.speak())
Factory Method 패턴
from abc import ABC, abstractmethod
class Button(ABC):
@abstractmethod
def render(self) -> str: ...
@abstractmethod
def on_click(self) -> str: ...
class WindowsButton(Button):
def render(self) -> str:
return "[ Windows Button ]"
def on_click(self) -> str:
return "Windows 클릭 이벤트 발생"
class MacButton(Button):
def render(self) -> str:
return "( Mac Button )"
def on_click(self) -> str:
return "Mac 클릭 이벤트 발생"
class WebButton(Button):
def render(self) -> str:
return "<button>Web Button</button>"
def on_click(self) -> str:
return "DOM 클릭 이벤트 발생"
class Dialog(ABC):
"""Factory Method를 정의하는 추상 클래스"""
@abstractmethod
def create_button(self) -> Button:
"""팩토리 메서드 — 서브클래스에서 구현"""
...
def render_dialog(self) -> str:
"""팩토리 메서드로 생성한 버튼을 사용"""
button = self.create_button()
return f"Dialog: {button.render()}"
def handle_click(self) -> str:
button = self.create_button()
return button.on_click()
class WindowsDialog(Dialog):
def create_button(self) -> Button:
return WindowsButton()
class MacDialog(Dialog):
def create_button(self) -> Button:
return MacButton()
class WebDialog(Dialog):
def create_button(self) -> Button:
return WebButton()
import platform
def get_dialog() -> Dialog:
"""환경에 따라 적절한 Dialog 반환"""
system = platform.system()
if system == "Windows":
return WindowsDialog()
elif system == "Darwin":
return MacDialog()
else:
return WebDialog()
dialog = get_dialog()
print(dialog.render_dialog())
print(dialog.handle_click())
Abstract Factory 패턴
from abc import ABC, abstractmethod
# 추상 제품들
class TextInput(ABC):
@abstractmethod
def render(self) -> str: ...
class Checkbox(ABC):
@abstractmethod
def render(self) -> str: ...
class Button(ABC):
@abstractmethod
def render(self) -> str: ...
# 구체 제품들 (Windows)
class WindowsTextInput(TextInput):
def render(self) -> str:
return "[___ Windows TextInput ___]"
class WindowsCheckbox(Checkbox):
def render(self) -> str:
return "[x] Windows Checkbox"
class WindowsButton(Button):
def render(self) -> str:
return "[ OK ]"
# 구체 제품들 (Mac)
class MacTextInput(TextInput):
def render(self) -> str:
return "( Mac TextInput )"
class MacCheckbox(Checkbox):
def render(self) -> str:
return "(✓) Mac Checkbox"
class MacButton(Button):
def render(self) -> str:
return "( OK )"
# 추상 팩토리
class UIFactory(ABC):
@abstractmethod
def create_text_input(self) -> TextInput: ...
@abstractmethod
def create_checkbox(self) -> Checkbox: ...
@abstractmethod
def create_button(self) -> Button: ...
# 구체 팩토리들
class WindowsUIFactory(UIFactory):
def create_text_input(self) -> TextInput:
return WindowsTextInput()
def create_checkbox(self) -> Checkbox:
return WindowsCheckbox()
def create_button(self) -> Button:
return WindowsButton()
class MacUIFactory(UIFactory):
def create_text_input(self) -> TextInput:
return MacTextInput()
def create_checkbox(self) -> Checkbox:
return MacCheckbox()
def create_button(self) -> Button:
return MacButton()
class LoginForm:
"""팩토리로 생성된 UI 컴포넌트를 사용 — 구체 클래스 모름"""
def __init__(self, factory: UIFactory):
self.username_input = factory.create_text_input()
self.remember_me = factory.create_checkbox()
self.submit_btn = factory.create_button()
def render(self) -> str:
return (f"=== 로그인 ===\n"
f" {self.username_input.render()}\n"
f" {self.remember_me.render()}\n"
f" {self.submit_btn.render()}")
for factory in [WindowsUIFactory(), MacUIFactory()]:
form = LoginForm(factory)
print(form.render())
print()
Observer 패턴
옵저버(Observer) 패턴은 객체의 상태 변화를 다른 객체들에게 자동으로 알리는 패턴입니다. 이벤트 시스템, GUI 업데이트, 알림 기능 등에 활용됩니다.
기본 구현
from abc import ABC, abstractmethod
from typing import Any
class Observer(ABC):
@abstractmethod
def update(self, event: str, data: Any) -> None: ...
class Subject:
"""관찰 대상 (Publisher)"""
def __init__(self):
self._observers: dict[str, list[Observer]] = {}
def subscribe(self, event: str, observer: Observer) -> None:
self._observers.setdefault(event, []).append(observer)
def unsubscribe(self, event: str, observer: Observer) -> None:
if event in self._observers:
self._observers[event].remove(observer)
def notify(self, event: str, data: Any = None) -> None:
for observer in self._observers.get(event, []):
observer.update(event, data)
class StockMarket(Subject):
"""주식 시장 — 가격 변동을 옵저버에게 알림"""
def __init__(self):
super().__init__()
self._prices: dict[str, float] = {}
def update_price(self, symbol: str, price: float) -> None:
old_price = self._prices.get(symbol, 0)
self._prices[symbol] = price
change = price - old_price
change_pct = (change / old_price * 100) if old_price else 0
self.notify("price_changed", {
"symbol": symbol,
"price": price,
"change": change,
"change_pct": change_pct,
})
def get_price(self, symbol: str) -> float:
return self._prices.get(symbol, 0)
class AlertObserver(Observer):
"""가격 변동 알림"""
def __init__(self, threshold_pct: float = 5.0):
self.threshold = threshold_pct
def update(self, event: str, data: dict) -> None:
if abs(data["change_pct"]) >= self.threshold:
direction = "급등" if data["change"] > 0 else "급락"
print(f"[알림] {data['symbol']} {direction}! "
f"{data['price']:,.0f}원 ({data['change_pct']:+.1f}%)")
class PortfolioObserver(Observer):
"""포트폴리오 손익 추적"""
def __init__(self, name: str):
self.name = name
self._holdings: dict[str, tuple[int, float]] = {} # {symbol: (수량, 매수가)}
self._pnl = 0.0
def add_holding(self, symbol: str, quantity: int, buy_price: float) -> None:
self._holdings[symbol] = (quantity, buy_price)
def update(self, event: str, data: dict) -> None:
symbol = data["symbol"]
if symbol in self._holdings:
qty, buy_price = self._holdings[symbol]
pnl = (data["price"] - buy_price) * qty
pnl_pct = (data["price"] - buy_price) / buy_price * 100
print(f"[{self.name}] {symbol}: {pnl:+,.0f}원 ({pnl_pct:+.1f}%)")
class LogObserver(Observer):
"""모든 가격 변동 기록"""
def __init__(self):
self._log: list[str] = []
def update(self, event: str, data: dict) -> None:
entry = (f"{data['symbol']}: {data['price']:,.0f}원 "
f"({data['change_pct']:+.1f}%)")
self._log.append(entry)
def get_log(self) -> list[str]:
return list(self._log)
# 사용 예시
market = StockMarket()
alert = AlertObserver(threshold_pct=3.0)
portfolio = PortfolioObserver("김철수")
logger = LogObserver()
portfolio.add_holding("SAMSUNG", 10, 70_000)
portfolio.add_holding("KAKAO", 5, 50_000)
market.subscribe("price_changed", alert)
market.subscribe("price_changed", portfolio)
market.subscribe("price_changed", logger)
# 가격 업데이트
market.update_price("SAMSUNG", 75_000)
market.update_price("KAKAO", 45_000)
market.update_price("NAVER", 200_000)
print("\n전체 로그:")
for entry in logger.get_log():
print(f" {entry}")
weakref를 활용한 메모리 안전 Observer
import weakref
from typing import Callable, Any
class EventEmitter:
"""weakref를 사용하여 옵저버 메모리 누수를 방지하는 이벤트 에미터"""
def __init__(self):
self._listeners: dict[str, list] = {}
def on(self, event: str, callback: Callable) -> None:
"""이벤트 리스너 등록 (weakref 사용)"""
if event not in self._listeners:
self._listeners[event] = []
# 메서드는 weakref.WeakMethod, 함수는 weakref.ref 사용
try:
ref = weakref.WeakMethod(callback)
except TypeError:
ref = weakref.ref(callback)
self._listeners[event].append(ref)
def emit(self, event: str, *args: Any, **kwargs: Any) -> None:
"""이벤트 발생 — 죽은 weakref는 자동 제거"""
listeners = self._listeners.get(event, [])
alive = []
for ref in listeners:
callback = ref()
if callback is not None:
callback(*args, **kwargs)
alive.append(ref)
self._listeners[event] = alive
class Button(EventEmitter):
def __init__(self, label: str):
super().__init__()
self.label = label
def click(self) -> None:
print(f"[버튼 '{self.label}' 클릭]")
self.emit("click", self)
class ClickCounter:
def __init__(self, name: str):
self.name = name
self.count = 0
def on_click(self, button: Button) -> None:
self.count += 1
print(f"[{self.name}] '{button.label}' 클릭 횟수: {self.count}")
btn = Button("확인")
counter = ClickCounter("카운터")
btn.on("click", counter.on_click)
btn.click()
btn.click()
# counter 삭제 후 — weakref이므로 자동 정리
del counter
btn.click() # 아무 일도 없음 (죽은 weakref 자동 제거)
Strategy 패턴
전략(Strategy) 패턴은 알고리즘 군을 정의하고 각각을 캡슐화하여 교환 가능하게 만드는 패턴입니다. Python은 함수가 일급 객체이므로 더 간결하게 구현할 수 있습니다.
클래스 기반 Strategy
from abc import ABC, abstractmethod
class SortStrategy(ABC):
@abstractmethod
def sort(self, data: list) -> list: ...
@abstractmethod
def name(self) -> str: ...
class BubbleSort(SortStrategy):
def sort(self, data: list) -> list:
arr = list(data)
n = len(arr)
for i in range(n):
for j in range(n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
return arr
def name(self) -> str:
return "버블 정렬"
class QuickSort(SortStrategy):
def sort(self, data: list) -> list:
if len(data) <= 1:
return list(data)
pivot = data[len(data) // 2]
left = [x for x in data if x < pivot]
middle = [x for x in data if x == pivot]
right = [x for x in data if x > pivot]
return self.sort(left) + middle + self.sort(right)
def name(self) -> str:
return "퀵 정렬"
class MergeSort(SortStrategy):
def sort(self, data: list) -> list:
if len(data) <= 1:
return list(data)
mid = len(data) // 2
left = self.sort(data[:mid])
right = self.sort(data[mid:])
return self._merge(left, right)
def _merge(self, left: list, right: list) -> list:
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
def name(self) -> str:
return "병합 정렬"
class Sorter:
def __init__(self, strategy: SortStrategy):
self._strategy = strategy
def set_strategy(self, strategy: SortStrategy) -> None:
"""런타임에 전략 변경 가능"""
self._strategy = strategy
def sort(self, data: list) -> list:
print(f"[{self._strategy.name()}] 정렬 시작...")
result = self._strategy.sort(data)
return result
data = [64, 34, 25, 12, 22, 11, 90]
sorter = Sorter(BubbleSort())
print(sorter.sort(data))
sorter.set_strategy(QuickSort())
print(sorter.sort(data))
함수를 전략으로 — Python 스타일
from typing import Callable
# Python에서는 함수 자체를 전략으로 사용할 수 있음
SortFunc = Callable[[list], list]
class FlexibleSorter:
def __init__(self, strategy: SortFunc | None = None):
self._strategy = strategy or sorted
def sort(self, data: list) -> list:
return self._strategy(data)
def reverse_sort(data: list) -> list:
return sorted(data, reverse=True)
def sort_by_length(data: list) -> list:
return sorted(data, key=lambda x: len(str(x)))
# 함수를 전략으로 주입
sorter1 = FlexibleSorter() # 기본 정렬
sorter2 = FlexibleSorter(reverse_sort) # 역순 정렬
sorter3 = FlexibleSorter(sort_by_length) # 길이 기준 정렬
sorter4 = FlexibleSorter(lambda d: sorted(d, key=lambda x: -x)) # 람다도 가능
nums = [42, 7, 100, 3, 55, 18]
print(sorter1.sort(nums)) # [3, 7, 18, 42, 55, 100]
print(sorter2.sort(nums)) # [100, 55, 42, 18, 7, 3]
print(sorter3.sort(nums)) # [7, 3, 42, 18, 55, 100]
# 실전: 결제 처리 전략
class PaymentProcessor:
def __init__(self, strategy: Callable[[float], str]):
self._strategy = strategy
def process(self, amount: float) -> str:
return self._strategy(amount)
def pay_with_card(amount: float) -> str:
return f"카드 결제: {amount:,.0f}원"
def pay_with_kakao(amount: float) -> str:
return f"카카오페이 결제: {amount:,.0f}원"
def pay_with_crypto(amount: float) -> str:
return f"암호화폐 결제: {amount:,.0f}원 (변동 환율 적용)"
processor = PaymentProcessor(pay_with_card)
print(processor.process(35_000))
processor._strategy = pay_with_kakao
print(processor.process(35_000))
Decorator 패턴
데코레이터(Decorator) 패턴은 객체에 동적으로 새로운 기능을 추가하는 패턴입니다. 주의: Python의 @decorator 문법과는 다른 개념입니다 (동일한 이름이지만 GoF 패턴과 Python 언어 기능은 별개입니다).
GoF Decorator 패턴
from abc import ABC, abstractmethod
class Coffee(ABC):
"""음료 기반 인터페이스"""
@abstractmethod
def cost(self) -> float: ...
@abstractmethod
def description(self) -> str: ...
class Espresso(Coffee):
def cost(self) -> float:
return 3_000
def description(self) -> str:
return "에스프레소"
class Americano(Coffee):
def cost(self) -> float:
return 4_000
def description(self) -> str:
return "아메리카노"
class CoffeeDecorator(Coffee, ABC):
"""데코레이터 기반 클래스"""
def __init__(self, coffee: Coffee):
self._coffee = coffee
def cost(self) -> float:
return self._coffee.cost()
def description(self) -> str:
return self._coffee.description()
class MilkDecorator(CoffeeDecorator):
def cost(self) -> float:
return super().cost() + 500
def description(self) -> str:
return f"{super().description()} + 우유"
class SyrupDecorator(CoffeeDecorator):
def __init__(self, coffee: Coffee, syrup_type: str = "바닐라"):
super().__init__(coffee)
self.syrup_type = syrup_type
def cost(self) -> float:
return super().cost() + 700
def description(self) -> str:
return f"{super().description()} + {self.syrup_type} 시럽"
class WhipDecorator(CoffeeDecorator):
def cost(self) -> float:
return super().cost() + 1_000
def description(self) -> str:
return f"{super().description()} + 휘핑크림"
# 주문 조합
order1 = Espresso()
order2 = MilkDecorator(Americano())
order3 = WhipDecorator(SyrupDecorator(MilkDecorator(Americano()), "카라멜"))
for order in [order1, order2, order3]:
print(f"{order.description()}: {order.cost():,}원")
Python 데코레이터로 GoF Decorator 구현
import functools
import time
from typing import Callable
# Python의 @decorator는 함수에 기능을 추가하는 구문 설탕
def timer(func: Callable) -> Callable:
"""함수 실행 시간을 측정하는 데코레이터"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"[타이머] {func.__name__}: {elapsed:.4f}초")
return result
return wrapper
def retry(max_attempts: int = 3, exceptions: tuple = (Exception,)):
"""실패 시 재시도하는 데코레이터"""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == max_attempts:
raise
print(f"[재시도] {func.__name__} 실패 ({attempt}/{max_attempts}): {e}")
return wrapper
return decorator
def cache(func: Callable) -> Callable:
"""결과를 캐싱하는 데코레이터"""
_cache: dict = {}
@functools.wraps(func)
def wrapper(*args):
if args not in _cache:
_cache[args] = func(*args)
return _cache[args]
wrapper.cache_clear = lambda: _cache.clear()
wrapper.cache_info = lambda: {"size": len(_cache), "keys": list(_cache.keys())}
return wrapper
def validate_types(**type_hints):
"""인수 타입을 검증하는 데코레이터"""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs):
import inspect
sig = inspect.signature(func)
bound = sig.bind(*args, **kwargs)
for param_name, expected_type in type_hints.items():
if param_name in bound.arguments:
value = bound.arguments[param_name]
if not isinstance(value, expected_type):
raise TypeError(
f"{param_name}은 {expected_type.__name__}이어야 합니다, "
f"받은 타입: {type(value).__name__}"
)
return func(*args, **kwargs)
return wrapper
return decorator
# 여러 데코레이터 조합 (데코레이터도 조합 가능 — GoF Decorator처럼)
@timer
@cache
def fibonacci(n: int) -> int:
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
@validate_types(name=str, age=int)
def greet(name: str, age: int) -> str:
return f"안녕하세요, {name}님! ({age}세)"
@retry(max_attempts=3, exceptions=(ValueError,))
def unstable_operation(value: int) -> int:
import random
if random.random() < 0.5:
raise ValueError("임시 오류 발생!")
return value * 2
print(fibonacci(30))
print(fibonacci(30)) # 캐시에서 즉시 반환
print(greet("김철수", 25))
try:
greet("이름", "나이") # TypeError 발생
except TypeError as e:
print(f"타입 오류: {e}")
고수 팁
1. 패턴의 선택 기준
# 패턴은 문제가 있을 때만 사용하라
# 단순한 경우: 패턴 불필요
def process(data):
return sorted(data)
# 복잡한 경우 (알고리즘 교체 필요): Strategy 패턴
class DataProcessor:
def __init__(self, sort_strategy=sorted):
self.sort = sort_strategy
def process(self, data):
return self.sort(data)
2. Singleton의 대안: 모듈 수준 변수
# Singleton 클래스보다 모듈 수준 변수가 더 간단하고 Python스럽다
# settings.py
DEBUG = False
DATABASE_URL = "sqlite:///app.db"
MAX_CONNECTIONS = 10
# 사용처
# import settings
# if settings.DEBUG: ...
3. __init_subclass__로 Factory 자동 등록
class Plugin:
_registry: dict[str, type] = {}
def __init_subclass__(cls, plugin_name: str = "", **kwargs):
super().__init_subclass__(**kwargs)
if plugin_name:
Plugin._registry[plugin_name] = cls
print(f"플러그인 등록: {plugin_name} → {cls.__name__}")
@classmethod
def create(cls, name: str) -> "Plugin":
if name not in cls._registry:
raise KeyError(f"등록되지 않은 플러그인: {name}")
return cls._registry[name]()
def run(self) -> str:
raise NotImplementedError
class AudioPlugin(Plugin, plugin_name="audio"):
def run(self) -> str:
return "오디오 처리 중..."
class VideoPlugin(Plugin, plugin_name="video"):
def run(self) -> str:
return "비디오 처리 중..."
for name in ["audio", "video"]:
plugin = Plugin.create(name)
print(plugin.run())
정리
| 패턴 | 목적 | Python 특징 |
|---|---|---|
| Singleton | 인스턴스 하나만 | 모듈 레벨 변수가 더 Python스럽다 |
| Factory | 생성 로직 분리 | __init_subclass__로 자동 등록 |
| Observer | 이벤트 알림 | weakref로 메모리 누수 방지 |
| Strategy | 알고리즘 교체 | 함수가 일급 객체이므로 더 간결 |
| Decorator | 기능 추가 | @decorator 문법으로 우아하게 |
디자인 패턴은 수단이지 목적이 아닙니다. "이 패턴을 쓰고 싶다"가 아니라 "이 문제를 해결하는 데 이 패턴이 적합하다"는 관점으로 접근하세요.