Skip to main content

디자인 패턴

학습 목표

  • 디자인 패턴의 목적과 가치를 이해한다
  • Strategy 패턴으로 알고리즘을 교체 가능하게 설계할 수 있다
  • Factory 패턴으로 객체 생성을 유연하게 관리할 수 있다
  • Observer 패턴으로 이벤트 기반 시스템을 구현할 수 있다

왜 중요한가

디자인 패턴은 반복적으로 발생하는 설계 문제에 대한 검증된 해법입니다. ML/DL 프레임워크는 이 패턴들을 적극 활용합니다. 패턴을 알면 프레임워크 코드를 이해하고, 자신의 코드를 더 유연하게 설계할 수 있습니다.

Strategy 패턴

알고리즘을 캡슐화하여 실행 시점에 교체할 수 있게 합니다.
from typing import Protocol

# 전략 인터페이스
class Optimizer(Protocol):
    def step(self, params: list, gradients: list) -> list:
        ...

# 구체적 전략들
class SGD:
    def __init__(self, lr=0.01):
        self.lr = lr

    def step(self, params, gradients):
        return [p - self.lr * g for p, g in zip(params, gradients)]

class Adam:
    def __init__(self, lr=0.001, beta1=0.9, beta2=0.999):
        self.lr = lr
        self.beta1 = beta1
        self.beta2 = beta2

    def step(self, params, gradients):
        # 단순화된 Adam
        return [p - self.lr * g for p, g in zip(params, gradients)]

# 컨텍스트: 전략을 교체 가능
class Trainer:
    def __init__(self, optimizer: Optimizer):
        self.optimizer = optimizer

    def train(self, params, gradients):
        return self.optimizer.step(params, gradients)

# 사용 - 옵티마이저 교체가 간단
trainer_sgd = Trainer(SGD(lr=0.01))
trainer_adam = Trainer(Adam(lr=0.001))

params = [1.0, 2.0, 3.0]
grads = [0.1, 0.2, 0.3]
print(trainer_sgd.train(params, grads))
print(trainer_adam.train(params, grads))

Factory 패턴

객체 생성 로직을 분리하여 유연한 인스턴스 생성을 지원합니다.
from dataclasses import dataclass

@dataclass
class ModelConfig:
    name: str
    input_dim: int
    hidden_dim: int = 128
    output_dim: int = 10

class LinearModel:
    def __init__(self, config: ModelConfig):
        self.config = config
        print(f"LinearModel 생성: {config.input_dim} -> {config.output_dim}")

class CNNModel:
    def __init__(self, config: ModelConfig):
        self.config = config
        print(f"CNNModel 생성: {config.input_dim} -> {config.output_dim}")

class TransformerModel:
    def __init__(self, config: ModelConfig):
        self.config = config
        print(f"TransformerModel 생성: {config.input_dim} -> {config.output_dim}")

# 팩토리 함수
def create_model(config: ModelConfig):
    """설정에 따라 적절한 모델을 생성합니다."""
    models = {
        "linear": LinearModel,
        "cnn": CNNModel,
        "transformer": TransformerModel,
    }

    model_class = models.get(config.name)
    if model_class is None:
        raise ValueError(f"지원하지 않는 모델: {config.name}")
    return model_class(config)

# 사용 - 모델 변경이 설정만으로 가능
config = ModelConfig(name="transformer", input_dim=512)
model = create_model(config)

# 레지스트리 패턴 (확장 가능한 팩토리)
class ModelRegistry:
    _registry = {}

    @classmethod
    def register(cls, name):
        def decorator(model_class):
            cls._registry[name] = model_class
            return model_class
        return decorator

    @classmethod
    def create(cls, name, **kwargs):
        if name not in cls._registry:
            raise ValueError(f"등록되지 않은 모델: {name}")
        return cls._registry[name](**kwargs)

@ModelRegistry.register("bert")
class BertModel:
    def __init__(self, **kwargs):
        self.config = kwargs

model = ModelRegistry.create("bert", hidden_size=768)

Observer 패턴

객체의 상태 변화를 관찰자에게 자동으로 알립니다.
from typing import Protocol

class TrainingCallback(Protocol):
    def on_epoch_end(self, epoch: int, logs: dict) -> None:
        ...

class EarlyStopping:
    def __init__(self, patience=5):
        self.patience = patience
        self.best_loss = float("inf")
        self.wait = 0
        self.should_stop = False

    def on_epoch_end(self, epoch, logs):
        val_loss = logs.get("val_loss", float("inf"))
        if val_loss < self.best_loss:
            self.best_loss = val_loss
            self.wait = 0
        else:
            self.wait += 1
            if self.wait >= self.patience:
                self.should_stop = True
                print(f"Epoch {epoch}: 조기 종료")

class ModelCheckpoint:
    def __init__(self, filepath="best_model.pt"):
        self.filepath = filepath
        self.best_loss = float("inf")

    def on_epoch_end(self, epoch, logs):
        val_loss = logs.get("val_loss", float("inf"))
        if val_loss < self.best_loss:
            self.best_loss = val_loss
            print(f"Epoch {epoch}: 모델 저장 (loss={val_loss:.4f})")

class CSVLogger:
    def __init__(self, filename="training.csv"):
        self.filename = filename
        self.logs = []

    def on_epoch_end(self, epoch, logs):
        self.logs.append({"epoch": epoch, **logs})

# 트레이너 - 콜백을 통해 확장
class Trainer:
    def __init__(self, callbacks=None):
        self.callbacks = callbacks or []

    def fit(self, epochs=100):
        for epoch in range(epochs):
            # 학습 로직 (의사 코드)
            logs = {"train_loss": 1.0/(epoch+1), "val_loss": 1.2/(epoch+1)}

            for callback in self.callbacks:
                callback.on_epoch_end(epoch, logs)

            if any(
                getattr(cb, "should_stop", False) for cb in self.callbacks
            ):
                break

# 사용 - 콜백 조합으로 동작 확장
trainer = Trainer(callbacks=[
    EarlyStopping(patience=5),
    ModelCheckpoint(),
    CSVLogger(),
])
trainer.fit(epochs=100)

Singleton 패턴

인스턴스를 하나만 생성하도록 보장합니다.
class Config:
    """싱글톤 설정 관리자"""
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        if not hasattr(self, "_initialized"):
            self.settings = {}
            self._initialized = True

    def set(self, key, value):
        self.settings[key] = value

    def get(self, key, default=None):
        return self.settings.get(key, default)

c1 = Config()
c2 = Config()
print(c1 is c2)  # True - 같은 인스턴스

c1.set("lr", 0.001)
print(c2.get("lr"))  # 0.001
처음부터 패턴을 적용하기보다, 코드가 복잡해지는 시점에 리팩토링으로 도입하세요. “같은 종류의 객체를 조건문으로 분기” -> Factory, “알고리즘 교체 필요” -> Strategy, “상태 변화 알림 필요” -> Observer를 고려합니다.
Python에서는 모듈 자체가 싱글톤으로 동작합니다. 모듈 수준 변수로 충분한 경우가 많습니다. 클래스 기반 싱글톤은 상속이나 lazy initialization이 필요할 때만 사용합니다.

체크리스트

  • Strategy 패턴으로 알고리즘을 교체 가능하게 설계할 수 있다
  • Factory 패턴으로 객체 생성을 유연하게 관리할 수 있다
  • Observer/Callback 패턴으로 이벤트 기반 확장을 구현할 수 있다

다음 문서