PYTHON · 심층 가이드
Python 디자인패턴 완전 정리
일급 함수와 덕 타이핑이 기본인 언어에서 고전적인 GoF 패턴이 어디까지 축약되고 어디서부터 계층 분리가 실제로 필요한지를, 20개 주제로 나눠 정리했습니다.
다른 언어의 패턴 책을 그대로 옮기면 Python에서는 대부분 과설계가 됩니다. 전략 패턴은 함수 하나를 인자로 넘기면 끝나고, 싱글톤은 모듈 자체가 이미 싱글톤이며, 어댑터는 __getattr__ 한 줄로 흉내 낼 수 있습니다. 남는 것은 경계를 긋는 패턴들입니다. 어디까지가 도메인이고 어디부터가 프레임워크인지, 무엇을 교체 가능하게 열어둘지를 정하는 쪽에 패턴의 값어치가 몰려 있습니다.
Protocol 기반 플러그인 시스템부터 읽으면 상속 없이 구조적 타이핑만으로 계약을 표현하는 감각이 잡힙니다. 그 계약을 데이터 접근에 적용한 것이 리포지토리 패턴이고, 여러 리포지토리를 묶어 트랜잭션 단위를 만드는 것이 서비스 계층입니다. 의존성 주입은 이 계층들을 실제로 갈아 끼우는 배선 방법이며, 플러그인 아키텍처에서 그 배선이 런타임 확장점으로 확장됩니다.
contextvars로 요청 스코프 값을 넘길 때는 전파 방향을 조심해야 합니다. asyncio는 태스크를 만들 때 현재 컨텍스트를 복사하므로, 태스크 안에서 set()한 값은 그 태스크 안에서만 보이고 부모로 돌아오지 않습니다. 반대로 태스크 생성 전에 넣어둔 값은 자식 전부에 그대로 보입니다. 이 비대칭을 모르면 로깅 상관관계 ID가 특정 경로에서만 비는 현상을 며칠씩 쫓게 됩니다.
01Context Variables & 의존성 주입
의존성 주입 패턴과 contextvars로 스레드/코루틴 안전한 컨텍스트를 관리합니다.
Python code
from contextvars import ContextVar
from typing import Optional
import asyncio
# ContextVar — 코루틴/스레드마다 독립적인 값
current_user: ContextVar[Optional[str]] = ContextVar(
"current_user", default=None
)
request_id: ContextVar[str] = ContextVar("request_id")
async def handle_request(user: str, req_id: str):
current_user.set(user)
request_id.set(req_id)
await process()
async def process():
user = current_user.get()
rid = request_id.get()
print(f"[{rid}] {user} 처리 중")
await asyncio.sleep(0.01)
# 의존성 주입 — 간단한 컨테이너
class Container:
_services = {}
@classmethod
def register(cls, name: str, factory):
cls._services[name] = factory
@classmethod
def resolve(cls, name: str):
return cls._services[name]()
# 등록
Container.register("db", lambda: {"connected": True})
Container.register("cache", lambda: {})
# 사용
db = Container.resolve("db")
cache = Container.resolve("cache")
async def main():
await asyncio.gather(
handle_request("Alice", "req-001"),
handle_request("Bob", "req-002"),
)
asyncio.run(main())ContextVar는 Python 3.7+에서 사용 가능합니다. 비동기 코드에서 스레드 로컬 변수처럼 사용하되 코루틴 간 격리가 보장됩니다.
threading.local()은 스레드 안전하지만 asyncio 코루틴 간에 값이 공유될 수 있습니다. 비동기 코드에서는 반드시 ContextVar를 사용하세요.
02Protocol 기반 플러그인 시스템
느슨한 결합(loose coupling)과 확장성 있는 플러그인 아키텍처를 구현합니다.
Python code
from typing import Protocol, runtime_checkable
from pathlib import Path
# 플러그인 인터페이스 정의
@runtime_checkable
class Exporter(Protocol):
def export(self, data: list, path: Path) -> None: ...
def extension(self) -> str: ...
# 구현체들 (Exporter를 명시적으로 상속하지 않음)
class CsvExporter:
def export(self, data: list, path: Path) -> None:
import csv
with open(path, "w", newline="") as f:
if data:
writer = csv.DictWriter(f, fieldnames=data[0])
writer.writeheader()
writer.writerows(data)
def extension(self) -> str:
return ".csv"
class JsonExporter:
def export(self, data: list, path: Path) -> None:
import json
path.write_text(json.dumps(data, indent=2, ensure_ascii=False))
def extension(self) -> str:
return ".json"
# 팩토리
_exporters: dict[str, Exporter] = {
"csv": CsvExporter(),
"json": JsonExporter(),
}
def get_exporter(fmt: str) -> Exporter:
if fmt not in _exporters:
raise ValueError(f"지원하지 않는 포맷: {fmt}")
return _exporters[fmt]
# 사용
data = [{"name": "Alice", "score": 95}]
for fmt in ["csv", "json"]:
exp = get_exporter(fmt)
print(isinstance(exp, Exporter)) # True@runtime_checkable을 추가하면 isinstance(obj, Protocol) 검사가 가능해집니다. 단, 메서드 시그니처까지는 검사하지 않습니다.
Protocol 검사는 메서드 존재 여부만 확인합니다. 타입 힌트나 반환값은 정적 분석 도구(mypy)만 검사합니다.
03ExceptionGroup 실전 활용
여러 예외를 동시에 처리하는 except* 패턴
Python code
<span class="cm">// ExceptionGroup 실전 활용 예제
// data/prompts.js의 생성 프롬프트로 상세 코드 생성 가능</span>
fun main() { println("ExceptionGroup 실전 활용") }PYTHON 공식 문서를 함께 참고하세요.
자주 발생하는 실수에 주의하세요.
04컨텍스트 매니저 — with & __enter__/__exit__
with 문은 리소스의 획득과 해제를 자동으로 관리합니다. __enter__와 __exit__ 매직 메서드를 구현하거나, contextlib.contextmanager 데코레이터로 간단히 만들 수 있습니다.
Python code
from contextlib import contextmanager
import time
# 1. 클래스 기반 컨텍스트 매니저
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self # with ... as 변수에 바인딩
def __exit__(self, exc_type, exc_val, exc_tb):
self.elapsed = time.perf_counter() - self.start
print(f"경과 시간: {self.elapsed:.4f}초")
return False # True면 예외를 억제
# 2. 제너레이터 기반 컨텍스트 매니저
@contextmanager
def managed_resource(name):
print(f"[{name}] 리소스 획득")
try:
yield name # with ... as 변수에 전달
except Exception as e:
print(f"[{name}] 예외 발생: {e}")
raise
finally:
print(f"[{name}] 리소스 해제")
# 3. 다중 컨텍스트 매니저
with Timer() as t:
total = sum(range(1_000_000))
print(f"합계: {total}")
with managed_resource("DB") as db:
print(f" {db} 연결로 작업 수행 중...")
# Python 3.10+ 괄호로 여러 개 관리
# with (
# managed_resource("DB") as db,
# managed_resource("Cache") as cache,
# ):
# print(f" {db}와 {cache} 동시 사용")__exit__에서 True를 반환하면 발생한 예외가 억제됩니다. 예외 로깅 후 정상 흐름을 유지하고 싶을 때 유용하지만 남용하면 디버깅이 어려워집니다.
__exit__에서 예외 처리를 깜빡하면 리소스 누수가 발생할 수 있습니다. finally 블록처럼 정리 코드는 반드시 실행되도록 작성하세요.
05Pydantic 모델 검증
Pydantic은 Python 타입 힌트를 활용해 런타임 데이터 검증을 자동화합니다. API 요청/응답, 설정 파일, 외부 데이터 파싱에서 타입 안전성을 보장하며, FastAPI의 핵심 기반 라이브러리입니다.
Python code
from pydantic import BaseModel, Field, field_validator, model_validator
from pydantic import EmailStr # pip install pydantic[email]
from datetime import datetime
from typing import Optional
# 1. 기본 모델 정의
class Address(BaseModel):
street: str
city: str
zip_code: str = Field(pattern=r"^\d{5}$")
class User(BaseModel):
name: str = Field(min_length=2, max_length=50)
email: str # EmailStr로 변경 가능
age: int = Field(ge=0, le=150)
address: Optional[Address] = None
tags: list[str] = []
created_at: datetime = Field(default_factory=datetime.now)
# 필드 레벨 검증
@field_validator('name')
@classmethod
def name_must_not_be_empty(cls, v):
if not v.strip():
raise ValueError('이름은 공백만으로 구성할 수 없습니다')
return v.strip()
# 모델 레벨 검증
@model_validator(mode='after')
def check_consistency(self):
if self.age < 14 and self.tags and 'admin' in self.tags:
raise ValueError('14세 미만은 관리자가 될 수 없습니다')
return self
# 2. 유효한 데이터
user = User(
name="홍길동",
email="hong@example.com",
age=25,
address={"street": "세종대로 1", "city": "서울", "zip_code": "04524"},
tags=["developer"],
)
print(f"사용자: {user.name}, {user.age}세")
print(f"주소: {user.address.city} {user.address.street}")
print(f"JSON: {user.model_dump_json(indent=2)}")
# 3. 유효하지 않은 데이터 → 자동 에러
from pydantic import ValidationError
try:
bad_user = User(name="", email="invalid", age=-5)
except ValidationError as e:
print(f"\n검증 에러 {e.error_count()}개:")
for err in e.errors():
print(f" {err['loc']}: {err['msg']}")Pydantic v2는 Rust 기반 코어로 v1 대비 5~50배 빠릅니다. model_dump()와 model_validate()를 사용하고, v1의 .dict()와 .parse_obj()는 deprecated입니다.
Pydantic은 기본적으로 타입을 강제 변환(coerce)합니다. "123"이 int 필드에 자동으로 123이 됩니다. 엄격한 검증이 필요하면 model_config = ConfigDict(strict=True)를 설정하세요.
06MVC/MTV 패턴
Model-View-Controller(MVC)와 Django의 Model-Template-View(MTV) 패턴을 Python으로 구현합니다.
Python code
from dataclasses import dataclass, field
# Model: 데이터와 비즈니스 로직
@dataclass
class UserModel:
users: list[dict] = field(default_factory=list)
def add(self, name: str, email: str):
user = {"id": len(self.users) + 1, "name": name, "email": email}
self.users.append(user)
return user
def get_all(self) -> list[dict]:
return self.users.copy()
def find(self, user_id: int) -> dict | None:
return next((u for u in self.users if u["id"] == user_id), None)
# View: 표시 로직
class UserView:
@staticmethod
def display_users(users: list[dict]):
print("=== 사용자 목록 ===")
for u in users:
print(f" [{u['id']}] {u['name']} ({u['email']})")
@staticmethod
def display_message(msg: str):
print(f">> {msg}")
# Controller: Model과 View 연결
class UserController:
def __init__(self):
self.model = UserModel()
self.view = UserView()
def create_user(self, name: str, email: str):
user = self.model.add(name, email)
self.view.display_message(f"{user['name']} 생성됨")
def list_users(self):
self.view.display_users(self.model.get_all())
# 사용
ctrl = UserController()
ctrl.create_user("Alice", "alice@test.com")
ctrl.create_user("Bob", "bob@test.com")
ctrl.list_users()Django의 MTV에서 View는 MVC의 Controller 역할, Template이 MVC의 View 역할입니다.
Model에 표시 로직을, View에 비즈니스 로직을 넣으면 관심사 분리가 무너집니다.
07리포지토리 패턴
리포지토리 패턴으로 데이터 접근 로직을 추상화하여 비즈니스 로직과 분리합니다.
Python code
from abc import ABC, abstractmethod
from dataclasses import dataclass
@dataclass
class User:
id: int
name: str
email: str
class UserRepository(ABC):
@abstractmethod
def find_by_id(self, user_id: int) -> User | None: ...
@abstractmethod
def find_all(self) -> list[User]: ...
@abstractmethod
def save(self, user: User) -> User: ...
@abstractmethod
def delete(self, user_id: int) -> bool: ...
class InMemoryUserRepository(UserRepository):
def __init__(self):
self._store: dict[int, User] = {}
self._next_id = 1
def find_by_id(self, user_id: int) -> User | None:
return self._store.get(user_id)
def find_all(self) -> list[User]:
return list(self._store.values())
def save(self, user: User) -> User:
if user.id == 0:
user = User(self._next_id, user.name, user.email)
self._next_id += 1
self._store[user.id] = user
return user
def delete(self, user_id: int) -> bool:
return self._store.pop(user_id, None) is not None
# 비즈니스 로직은 추상 인터페이스에만 의존
def register_user(repo: UserRepository, name: str, email: str):
user = repo.save(User(0, name, email))
print(f"등록: {user}")
return user
repo = InMemoryUserRepository()
register_user(repo, "Alice", "alice@test.com")
register_user(repo, "Bob", "bob@test.com")
print(f"전체: {repo.find_all()}")리포지토리 인터페이스를 ABC로 정의하면 테스트 시 InMemory 구현으로 쉽게 교체할 수 있습니다.
리포지토리에 비즈니스 로직을 넣지 마세요. 리포지토리는 순수한 데이터 접근만 담당합니다.
08서비스 계층
서비스 계층으로 비즈니스 로직을 캡슐화하여 컨트롤러와 데이터 접근을 분리합니다.
Python code
from dataclasses import dataclass
@dataclass
class Order:
id: int
user_id: int
items: list[str]
total: float
status: str = "pending"
class OrderRepository:
def __init__(self):
self._orders: dict[int, Order] = {}
self._next_id = 1
def save(self, order: Order) -> Order:
if order.id == 0:
order.id = self._next_id
self._next_id += 1
self._orders[order.id] = order
return order
def find_by_user(self, user_id: int) -> list[Order]:
return [o for o in self._orders.values() if o.user_id == user_id]
class OrderService:
"""비즈니스 로직을 캡슐화"""
def __init__(self, repo: OrderRepository):
self._repo = repo
def place_order(self, user_id: int, items: list[str]) -> Order:
if not items:
raise ValueError("주문 항목이 비어있습니다")
total = len(items) * 10000 # 간단한 가격 계산
order = self._repo.save(
Order(0, user_id, items, total)
)
print(f"주문 생성: #{order.id} (총 {order.total:,.0f}원)")
return order
def get_user_orders(self, user_id: int) -> list[Order]:
return self._repo.find_by_user(user_id)
# 사용
repo = OrderRepository()
service = OrderService(repo)
service.place_order(1, ["노트북", "마우스"])
service.place_order(1, ["키보드"])
orders = service.get_user_orders(1)
for o in orders:
print(f" #{o.id}: {o.items} → {o.total:,.0f}원")서비스 계층은 트랜잭션 경계, 권한 검사, 이벤트 발행 등 횡단 관심사를 처리하기 좋은 위치입니다.
서비스가 너무 많은 책임을 지면 "God Service"가 됩니다. 도메인별로 서비스를 분리하세요.
09의존성 주입
의존성 주입(DI)으로 모듈 간 결합도를 낮추고 테스트 용이성을 높입니다.
Python code
from abc import ABC, abstractmethod
# 인터페이스 정의
class EmailSender(ABC):
@abstractmethod
def send(self, to: str, subject: str, body: str) -> bool: ...
class Logger(ABC):
@abstractmethod
def log(self, message: str) -> None: ...
# 구현체
class SMTPSender(EmailSender):
def send(self, to, subject, body):
print(f" [SMTP] {to}: {subject}")
return True
class ConsoleLogger(Logger):
def log(self, message):
print(f" [LOG] {message}")
# DI를 받는 서비스
class NotificationService:
def __init__(self, sender: EmailSender, logger: Logger):
self._sender = sender
self._logger = logger
def notify(self, user_email: str, message: str):
self._logger.log(f"알림 전송: {user_email}")
success = self._sender.send(user_email, "알림", message)
self._logger.log(f"결과: {'성공' if success else '실패'}")
# 프로덕션
service = NotificationService(SMTPSender(), ConsoleLogger())
service.notify("user@test.com", "주문이 접수되었습니다")
# 테스트용 Mock
class MockSender(EmailSender):
def __init__(self):
self.sent = []
def send(self, to, subject, body):
self.sent.append((to, subject))
return True
mock = MockSender()
test_service = NotificationService(mock, ConsoleLogger())
test_service.notify("test@test.com", "테스트")
print(f"전송 기록: {mock.sent}")생성자 주입이 가장 일반적입니다. 의존성이 명시적이고 불변 객체를 만들 수 있습니다.
모든 것을 DI로 처리하면 설정 코드가 복잡해집니다. 변경 가능성이 높은 의존성만 주입하세요.
10이벤트 드리븐
이벤트 기반 아키텍처로 컴포넌트 간 느슨한 결합을 구현합니다.
Python code
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Callable, Any
from datetime import datetime
@dataclass
class Event:
name: str
data: dict
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
class EventBus:
def __init__(self):
self._handlers: dict[str, list[Callable]] = defaultdict(list)
self._history: list[Event] = []
def subscribe(self, event_name: str, handler: Callable):
self._handlers[event_name].append(handler)
def publish(self, event_name: str, **data):
event = Event(event_name, data)
self._history.append(event)
for handler in self._handlers.get(event_name, []):
handler(event)
def get_history(self) -> list[Event]:
return self._history.copy()
# 이벤트 핸들러들
bus = EventBus()
bus.subscribe("user.created", lambda e:
print(f" [이메일] {e.data['name']}님 환영 메일 발송"))
bus.subscribe("user.created", lambda e:
print(f" [분석] 신규 가입 기록"))
bus.subscribe("order.placed", lambda e:
print(f" [재고] {e.data['item']} 재고 차감"))
bus.subscribe("order.placed", lambda e:
print(f" [알림] 주문 확인 알림"))
# 이벤트 발행
print("=== 사용자 생성 ===")
bus.publish("user.created", name="Alice", email="alice@test.com")
print("\n=== 주문 생성 ===")
bus.publish("order.placed", item="노트북", qty=1)
print(f"\n이벤트 히스토리: {len(bus.get_history())}건")이벤트 핸들러는 독립적으로 실패해야 합니다. 한 핸들러의 실패가 다른 핸들러에 영향을 주지 않도록 하세요.
이벤트 기반 시스템에서 이벤트 순서에 의존하면 예상치 못한 동작이 발생합니다. 각 핸들러는 독립적이어야 합니다.
11pub/sub 패턴
발행/구독 패턴으로 메시지 기반의 비동기 통신을 구현합니다.
Python code
from collections import defaultdict
from typing import Callable
import re
class PubSub:
def __init__(self):
self._exact: dict[str, list[Callable]] = defaultdict(list)
self._patterns: list[tuple[str, Callable]] = []
def subscribe(self, topic: str, callback: Callable):
if "*" in topic:
pattern = topic.replace(".", "\.").replace("*", ".*")
self._patterns.append((pattern, callback))
else:
self._exact[topic].append(callback)
def publish(self, topic: str, message: dict):
# 정확한 매칭
for cb in self._exact.get(topic, []):
cb(topic, message)
# 패턴 매칭
for pattern, cb in self._patterns:
if re.match(pattern, topic):
cb(topic, message)
# 사용
ps = PubSub()
# 구체적 구독
ps.subscribe("order.created",
lambda t, m: print(f" [주문] 새 주문: {m}"))
ps.subscribe("order.paid",
lambda t, m: print(f" [결제] 결제 완료: {m}"))
# 와일드카드 구독
ps.subscribe("order.*",
lambda t, m: print(f" [감사] {t}: {m}"))
ps.subscribe("*",
lambda t, m: print(f" [로그] {t}"))
print("=== 주문 생성 ===")
ps.publish("order.created", {"id": 1, "item": "노트북"})
print("\n=== 결제 ===")
ps.publish("order.paid", {"id": 1, "amount": 1500000})와일드카드 패턴(order.*)으로 특정 카테고리의 모든 이벤트를 한 번에 구독할 수 있습니다.
너무 많은 와일드카드 구독자가 있으면 성능이 저하됩니다. 필요한 토픽만 정확히 구독하세요.
12커맨드 패턴
커맨드 패턴으로 요청을 객체로 캡슐화하여 실행, 취소, 재실행을 지원합니다.
Python code
from abc import ABC, abstractmethod
class Command(ABC):
@abstractmethod
def execute(self) -> None: ...
@abstractmethod
def undo(self) -> None: ...
class TextEditor:
def __init__(self):
self.content = ""
def __repr__(self):
return f'TextEditor("{self.content}")'
class InsertCommand(Command):
def __init__(self, editor: TextEditor, text: str):
self.editor = editor
self.text = text
def execute(self):
self.editor.content += self.text
def undo(self):
self.editor.content = self.editor.content[:-len(self.text)]
class DeleteCommand(Command):
def __init__(self, editor: TextEditor, count: int):
self.editor = editor
self.count = count
self.deleted = ""
def execute(self):
self.deleted = self.editor.content[-self.count:]
self.editor.content = self.editor.content[:-self.count]
def undo(self):
self.editor.content += self.deleted
class CommandHistory:
def __init__(self):
self._history: list[Command] = []
def execute(self, cmd: Command):
cmd.execute()
self._history.append(cmd)
def undo(self):
if self._history:
cmd = self._history.pop()
cmd.undo()
editor = TextEditor()
history = CommandHistory()
history.execute(InsertCommand(editor, "Hello"))
history.execute(InsertCommand(editor, " World"))
print(editor) # "Hello World"
history.execute(DeleteCommand(editor, 6))
print(editor) # "Hello"
history.undo()
print(editor) # "Hello World"
history.undo()
print(editor) # "Hello"커맨드 패턴은 Undo/Redo, 매크로 기록, 트랜잭션 등에 활용됩니다.
undo()에서 상태 복원이 불완전하면 데이터 정합성이 깨집니다. 모든 커맨드의 undo를 철저히 테스트하세요.
13데코레이터 패턴 활용
객체에 동적으로 기능을 추가하는 데코레이터 패턴입니다. Python 데코레이터와는 다른 GoF 패턴입니다.
Python code
from abc import ABC, abstractmethod
class DataSource(ABC):
@abstractmethod
def write(self, data: str) -> None: ...
@abstractmethod
def read(self) -> str: ...
class FileDataSource(DataSource):
def __init__(self):
self._data = ""
def write(self, data: str):
self._data = data
def read(self) -> str:
return self._data
class DataSourceDecorator(DataSource):
def __init__(self, source: DataSource):
self._source = source
def write(self, data: str):
self._source.write(data)
def read(self) -> str:
return self._source.read()
class EncryptionDecorator(DataSourceDecorator):
def write(self, data: str):
encrypted = data[::-1] # 간단한 암호화 시뮬레이션
print(f" [암호화] {data[:20]}...")
super().write(encrypted)
def read(self) -> str:
return super().read()[::-1]
class CompressionDecorator(DataSourceDecorator):
def write(self, data: str):
compressed = data.replace(" ", " ")
print(f" [압축] {len(data)} → {len(compressed)} chars")
super().write(compressed)
def read(self) -> str:
return super().read()
# 데코레이터 조합
source = FileDataSource()
encrypted = EncryptionDecorator(source)
compressed = CompressionDecorator(encrypted)
compressed.write("Hello World Python Programming")
result = compressed.read()
print(f"읽기: {result}")GoF 데코레이터 패턴은 상속 대신 조합으로 기능을 확장합니다. Python 데코레이터(@)와 혼동하지 마세요.
너무 많은 데코레이터를 중첩하면 디버깅이 어렵습니다. 3-4개 이상이면 다른 패턴을 고려하세요.
14프록시 패턴
프록시 패턴으로 객체 접근을 제어하고 지연 로딩, 캐싱, 접근 제어를 구현합니다.
Python code
from abc import ABC, abstractmethod
import time
class Database(ABC):
@abstractmethod
def query(self, sql: str) -> list: ...
class RealDatabase(Database):
def __init__(self):
print(" [DB] 무거운 연결 수립 중...")
time.sleep(0.01)
self._connected = True
def query(self, sql: str) -> list:
return [{"result": f"데이터: {sql}"}]
# 지연 로딩 프록시
class LazyDatabaseProxy(Database):
def __init__(self):
self._db: RealDatabase | None = None
def _get_db(self) -> RealDatabase:
if self._db is None:
self._db = RealDatabase()
return self._db
def query(self, sql: str) -> list:
return self._get_db().query(sql)
# 캐싱 프록시
class CachingProxy(Database):
def __init__(self, db: Database):
self._db = db
self._cache: dict[str, list] = {}
def query(self, sql: str) -> list:
if sql not in self._cache:
print(f" [캐시 MISS] {sql}")
self._cache[sql] = self._db.query(sql)
else:
print(f" [캐시 HIT] {sql}")
return self._cache[sql]
# 사용
print("=== 지연 로딩 ===")
db = LazyDatabaseProxy()
print("DB 객체 생성 (아직 연결 안 됨)")
print(db.query("SELECT 1")) # 이 시점에 연결
print("\n=== 캐싱 ===")
cached = CachingProxy(LazyDatabaseProxy())
cached.query("SELECT * FROM users") # MISS
cached.query("SELECT * FROM users") # HIT
cached.query("SELECT * FROM orders") # MISS프록시 패턴은 원본 객체의 인터페이스를 그대로 유지하므로 클라이언트 코드 변경 없이 기능을 추가할 수 있습니다.
프록시의 캐시를 무효화하지 않으면 오래된 데이터를 반환합니다. TTL이나 수동 무효화 전략을 구현하세요.
15어댑터 패턴
어댑터 패턴으로 호환되지 않는 인터페이스를 연결하여 기존 코드를 재사용합니다.
Python code
# 기존 XML 라이브러리 (변경 불가)
class XMLParser:
def parse_xml(self, xml_string: str) -> dict:
# XML 파싱 시뮬레이션
return {"format": "xml", "data": xml_string[:30]}
# 기존 CSV 라이브러리 (변경 불가)
class CSVParser:
def read_csv(self, csv_string: str) -> list:
return [row.split(",") for row in csv_string.strip().split("\n")]
# 통합 인터페이스
class DataParser:
def parse(self, data: str) -> dict:
raise NotImplementedError
# 어댑터들
class XMLAdapter(DataParser):
def __init__(self):
self._parser = XMLParser()
def parse(self, data: str) -> dict:
result = self._parser.parse_xml(data)
return {"source": "xml", "content": result}
class CSVAdapter(DataParser):
def __init__(self):
self._parser = CSVParser()
def parse(self, data: str) -> dict:
rows = self._parser.read_csv(data)
return {"source": "csv", "rows": len(rows), "content": rows}
# 팩토리로 어댑터 선택
def get_parser(format_type: str) -> DataParser:
parsers = {"xml": XMLAdapter, "csv": CSVAdapter}
return parsers[format_type]()
# 사용: 동일한 인터페이스로 다른 형식 처리
for fmt, data in [("xml", "<root>데이터</root>"),
("csv", "이름,나이\nAlice,30\nBob,25")]:
parser = get_parser(fmt)
result = parser.parse(data)
print(f" [{fmt}] {result}")어댑터는 기존 라이브러리를 수정하지 않고 통합 인터페이스를 제공할 때 유용합니다.
어댑터가 너무 많은 변환 로직을 가지면 안티패턴이 됩니다. 변환이 복잡하면 별도 변환 계층을 고려하세요.
16파사드 패턴
파사드 패턴으로 복잡한 서브시스템을 간단한 인터페이스 뒤에 숨깁니다.
Python code
# 복잡한 서브시스템들
class AuthService:
def verify_token(self, token: str) -> dict:
return {"user_id": 1, "role": "admin"}
class UserService:
def get_profile(self, user_id: int) -> dict:
return {"name": "Alice", "email": "alice@test.com"}
class PermissionService:
def check_access(self, role: str, resource: str) -> bool:
return role in ("admin", "editor")
class AuditService:
def log_access(self, user_id: int, resource: str):
print(f" [감사] 사용자 {user_id}이 {resource} 접근")
# 파사드: 복잡한 워크플로를 단순화
class APIFacade:
def __init__(self):
self._auth = AuthService()
self._user = UserService()
self._perm = PermissionService()
self._audit = AuditService()
def get_resource(self, token: str, resource: str) -> dict:
# 1. 인증
auth = self._auth.verify_token(token)
# 2. 프로필 조회
profile = self._user.get_profile(auth["user_id"])
# 3. 권한 확인
if not self._perm.check_access(auth["role"], resource):
raise PermissionError("접근 거부")
# 4. 감사 로그
self._audit.log_access(auth["user_id"], resource)
return {"user": profile, "resource": resource, "data": "..."}
# 클라이언트는 하나의 메서드만 호출
api = APIFacade()
result = api.get_resource("token123", "/api/data")
print(f"결과: {result['user']['name']}이 {result['resource']} 접근")파사드는 서브시스템을 숨기지만 직접 접근을 막지는 않습니다. 필요 시 서브시스템을 직접 사용할 수 있습니다.
파사드에 비즈니스 로직을 넣으면 God 클래스가 됩니다. 파사드는 위임만 하고 로직은 각 서비스에 두세요.
17템플릿 메서드
템플릿 메서드 패턴으로 알고리즘의 골격을 정의하고 세부 단계를 서브클래스에서 구현합니다.
Python code
from abc import ABC, abstractmethod
class DataProcessor(ABC):
"""알고리즘 골격 정의"""
def process(self, data: str) -> str:
parsed = self.parse(data)
validated = self.validate(parsed)
transformed = self.transform(validated)
return self.format_output(transformed)
@abstractmethod
def parse(self, raw: str) -> list: ...
@abstractmethod
def validate(self, data: list) -> list: ...
def transform(self, data: list) -> list:
"""기본 구현 (오버라이드 가능)"""
return data
@abstractmethod
def format_output(self, data: list) -> str: ...
class CSVProcessor(DataProcessor):
def parse(self, raw: str) -> list:
return [line.split(",") for line in raw.strip().split("\n")]
def validate(self, data: list) -> list:
return [row for row in data if len(row) >= 2]
def transform(self, data: list) -> list:
return [[col.strip().upper() for col in row] for row in data]
def format_output(self, data: list) -> str:
return "\n".join(" | ".join(row) for row in data)
class JSONProcessor(DataProcessor):
def parse(self, raw: str) -> list:
import json
return json.loads(raw)
def validate(self, data: list) -> list:
return [d for d in data if "name" in d]
def format_output(self, data: list) -> str:
return ", ".join(d.get("name", "?") for d in data)
csv_proc = CSVProcessor()
print(csv_proc.process("Alice, 30\nBob, 25\n, "))
json_proc = JSONProcessor()
print(json_proc.process('[{"name":"Alice"},{"age":30},{"name":"Bob"}]'))추상 메서드(필수 구현)와 일반 메서드(선택적 오버라이드)를 혼합하면 유연한 확장이 가능합니다.
템플릿 메서드의 단계가 너무 많으면 서브클래스 구현이 복잡해집니다. 3-5단계가 적절합니다.
18빌더 패턴
빌더 패턴으로 복잡한 객체를 단계별로 구성합니다. 메서드 체이닝으로 가독성을 높입니다.
Python code
from dataclasses import dataclass, field
@dataclass
class HttpRequest:
method: str = "GET"
url: str = ""
headers: dict = field(default_factory=dict)
params: dict = field(default_factory=dict)
body: str | None = None
timeout: int = 30
class RequestBuilder:
def __init__(self, url: str):
self._request = HttpRequest(url=url)
def method(self, method: str) -> "RequestBuilder":
self._request.method = method
return self
def header(self, key: str, value: str) -> "RequestBuilder":
self._request.headers[key] = value
return self
def param(self, key: str, value: str) -> "RequestBuilder":
self._request.params[key] = value
return self
def body(self, data: str) -> "RequestBuilder":
self._request.body = data
return self
def timeout(self, seconds: int) -> "RequestBuilder":
self._request.timeout = seconds
return self
def build(self) -> HttpRequest:
if not self._request.url:
raise ValueError("URL은 필수입니다")
return self._request
# 메서드 체이닝으로 가독성 높은 구성
request = (
RequestBuilder("https://api.example.com/users")
.method("POST")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token123")
.param("page", "1")
.body('{"name": "Alice"}')
.timeout(10)
.build()
)
print(f"{request.method} {request.url}")
print(f"헤더: {request.headers}")
print(f"본문: {request.body}")빌더의 각 메서드가 self를 반환하면 메서드 체이닝이 가능합니다.
build()에서 필수 필드 유효성 검사를 하지 않으면 불완전한 객체가 생성될 수 있습니다.
19전략 패턴 (디자인패턴)
전략 패턴으로 가격 할인 정책 같은 비즈니스 규칙을 교체 가능하게 설계합니다.
Python code
from abc import ABC, abstractmethod
from dataclasses import dataclass
class DiscountStrategy(ABC):
@abstractmethod
def calculate(self, price: float) -> float: ...
class NoDiscount(DiscountStrategy):
def calculate(self, price: float) -> float:
return price
class PercentDiscount(DiscountStrategy):
def __init__(self, percent: float):
self.percent = percent
def calculate(self, price: float) -> float:
return price * (1 - self.percent / 100)
class FixedDiscount(DiscountStrategy):
def __init__(self, amount: float):
self.amount = amount
def calculate(self, price: float) -> float:
return max(0, price - self.amount)
class BuyNGetFree(DiscountStrategy):
def __init__(self, buy: int, free: int):
self.buy = buy
self.free = free
def calculate(self, price: float) -> float:
ratio = self.buy / (self.buy + self.free)
return price * ratio
@dataclass
class Order:
items: list[tuple[str, float]]
discount: DiscountStrategy = None
def total(self) -> float:
subtotal = sum(price for _, price in self.items)
if self.discount:
return self.discount.calculate(subtotal)
return subtotal
# 런타임에 전략 교체
order = Order([("노트북", 1500000), ("마우스", 50000)])
print(f"정가: {order.total():,.0f}원")
order.discount = PercentDiscount(10)
print(f"10% 할인: {order.total():,.0f}원")
order.discount = FixedDiscount(100000)
print(f"10만원 할인: {order.total():,.0f}원")전략 패턴은 if-elif-else 체인을 제거하고 새로운 전략 추가를 열어둡니다 (개방-폐쇄 원칙).
전략 객체가 상태를 공유하면 스레드 안전성 문제가 발생합니다. 전략은 가능한 한 상태 없이(stateless) 설계하세요.
20플러그인 아키텍처
플러그인 시스템으로 애플리케이션을 확장 가능하게 설계합니다.
Python code
from abc import ABC, abstractmethod
from typing import Type
class Plugin(ABC):
@property
@abstractmethod
def name(self) -> str: ...
@abstractmethod
def execute(self, data: dict) -> dict: ...
class PluginManager:
def __init__(self):
self._plugins: dict[str, Plugin] = {}
def register(self, plugin: Plugin):
self._plugins[plugin.name] = plugin
print(f" 플러그인 등록: {plugin.name}")
def unregister(self, name: str):
self._plugins.pop(name, None)
def execute_all(self, data: dict) -> dict:
result = data.copy()
for name, plugin in self._plugins.items():
result = plugin.execute(result)
return result
def list_plugins(self) -> list[str]:
return list(self._plugins.keys())
# 플러그인 구현
class UpperCasePlugin(Plugin):
@property
def name(self): return "uppercase"
def execute(self, data):
if "text" in data:
data["text"] = data["text"].upper()
return data
class TimestampPlugin(Plugin):
@property
def name(self): return "timestamp"
def execute(self, data):
from datetime import datetime
data["processed_at"] = datetime.now().isoformat()
return data
class ValidatorPlugin(Plugin):
@property
def name(self): return "validator"
def execute(self, data):
data["is_valid"] = bool(data.get("text"))
return data
# 사용
manager = PluginManager()
manager.register(ValidatorPlugin())
manager.register(UpperCasePlugin())
manager.register(TimestampPlugin())
result = manager.execute_all({"text": "hello world"})
print(f"결과: {result}")
print(f"플러그인 목록: {manager.list_plugins()}")importlib과 entry_points를 활용하면 외부 패키지에서 플러그인을 자동 발견할 수 있습니다.
플러그인 실행 순서에 의존하면 취약합니다. 명시적 우선순위 시스템을 구현하세요.
정리하며
- ABC 상속보다 Protocol로 계약을 선언해 구현체와의 결합을 끊습니다
- 리포지토리는 ORM 쿼리를 감추는 자리지, 메서드를 1:1로 옮기는 자리가 아닙니다
- DI 컨테이너 없이 생성자 인자만으로도 대부분의 배선은 충분합니다
- contextvars 값은 태스크 생성 시점 기준으로 복사된다는 점을 기억합니다
더 깊이 들어가고 싶다면 Python 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.