클래스 데코레이터
클래스 데코레이터는 함수 데코레이터와 동일한 문법(@)으로 클래스에 적용되어 클래스 정의를 수정하거나 새 클래스로 대체합니다.
클래스 데코레이터 기본
# 클래스 데코레이터 = 클래스를 인수로 받아 수정된 클래스(또는 새 클래스)를 반환하는 함수
def add_repr(cls):
"""__repr__을 자동으로 추가하는 데코레이터"""
def __repr__(self):
attrs = ", ".join(
f"{k}={v!r}"
for k, v in vars(self).items()
if not k.startswith("_")
)
return f"{cls.__name__}({attrs})"
cls.__repr__ = __repr__
return cls
def add_eq(cls):
"""__eq__를 자동으로 추가하는 데코레이터"""
def __eq__(self, other):
if type(self) is not type(other):
return NotImplemented
return vars(self) == vars(other)
def __hash__(self):
return hash(tuple(sorted(vars(self).items())))
cls.__eq__ = __eq__
cls.__hash__ = __hash__
return cls
@add_repr
@add_eq
class Point:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
p1 = Point(1.0, 2.0)
p2 = Point(1.0, 2.0)
p3 = Point(3.0, 4.0)
print(p1) # Point(x=1.0, y=2.0)
print(p1 == p2) # True
print(p1 == p3) # False
print(hash(p1) == hash(p2)) # True
클래스에 기능 주입
import time
from typing import Any
def singleton(cls):
"""클래스를 싱글톤으로 만드는 데코레이터"""
instances: dict = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
get_instance.__name__ = cls.__name__
get_instance.__doc__ = cls.__doc__
get_instance._cls = cls # 원본 클래스 접근용
return get_instance
@singleton
class DatabaseConnection:
"""DB 연결 풀 (싱글톤)"""
def __init__(self, host: str = "localhost", port: int = 5432):
self.host = host
self.port = port
self._pool: list = []
print(f"[DB] 연결 초기화: {host}:{port}")
def query(self, sql: str) -> str:
return f"[{self.host}] {sql} 실행"
db1 = DatabaseConnection("db.example.com", 5432)
db2 = DatabaseConnection("other.host", 9999) # 무시됨
print(db1 is db2) # True
print(db1.host) # db.example.com
def add_timestamps(cls):
"""생성/수정 타임스탬프를 추가하는 데코레이터"""
original_init = cls.__init__
def new_init(self, *args, **kwargs):
original_init(self, *args, **kwargs)
self.created_at = time.time()
self.updated_at = time.time()
def touch(self):
self.updated_at = time.time()
def age(self) -> float:
return time.time() - self.created_at
cls.__init__ = new_init
cls.touch = touch
cls.age = age
return cls
@add_timestamps
class Article:
def __init__(self, title: str, content: str):
self.title = title
self.content = content
article = Article("파이썬 데코레이터", "데코레이터는...")
print(f"생성 시각: {article.created_at:.0f}")
print(f"나이: {article.age():.4f}초")
time.sleep(0.01)
article.touch()
print(f"수정 시각: {article.updated_at:.0f}")
메서드 자동 래핑
import functools
import time
import logging
logging.basicConfig(level=logging.INFO)
def log_all_methods(cls):
"""클래스의 모든 public 메서드에 로깅을 추가하는 데코레이터"""
for name, method in vars(cls).items():
if callable(method) and not name.startswith("_"):
@functools.wraps(method)
def make_logged(m):
@functools.wraps(m)
def logged(*args, **kwargs):
logging.info(f"{cls.__name__}.{m.__name__} 호출")
start = time.perf_counter()
result = m(*args, **kwargs)
elapsed = time.perf_counter() - start
logging.info(f"{cls.__name__}.{m.__name__} 완료 ({elapsed:.4f}초)")
return result
return logged
setattr(cls, name, make_logged(method))
return cls
def validate_types(cls):
"""__init__의 타입 힌트를 기반으로 자동 타입 검증을 추가하는 데코레이터"""
import inspect
original_init = cls.__init__
hints = {}
try:
hints = original_init.__annotations__
except AttributeError:
pass
@functools.wraps(original_init)
def new_init(self, *args, **kwargs):
sig = inspect.signature(original_init)
bound = sig.bind(self, *args, **kwargs)
bound.apply_defaults()
for param_name, expected_type in hints.items():
if param_name == "return":
continue
if param_name in bound.arguments:
value = bound.arguments[param_name]
if not isinstance(value, expected_type):
raise TypeError(
f"{cls.__name__}.{param_name}: "
f"{expected_type.__name__} 기대, "
f"{type(value).__name__} 받음"
)
original_init(self, *args, **kwargs)
cls.__init__ = new_init
return cls
@log_all_methods
class Calculator:
def add(self, a: float, b: float) -> float:
return a + b
def multiply(self, a: float, b: float) -> float:
return a * b
calc = Calculator()
print(calc.add(3, 5)) # 8
print(calc.multiply(4, 6)) # 24
@validate_types
class Person:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def __repr__(self) -> str:
return f"Person(name={self.name!r}, age={self.age})"
p = Person("Alice", 30)
print(p)
try:
Person("Bob", "서른") # TypeError: age: int 기대, str 받음
except TypeError as e:
print(f"타입 오류: {e}")
레지스트리 패턴
from typing import Type
def register(registry: dict):
"""클래스를 레지스트리에 자동 등록하는 데코레이터 팩토리"""
def decorator(cls):
name = cls.__name__.lower()
registry[name] = cls
print(f"등록: {name} → {cls.__name__}")
return cls
return decorator
# 플러그인 레지스트리
PLUGINS: dict[str, Type] = {}
@register(PLUGINS)
class TextPlugin:
def process(self, data: str) -> str:
return data.upper()
@register(PLUGINS)
class JsonPlugin:
def process(self, data: str) -> str:
import json
return json.dumps({"processed": data})
@register(PLUGINS)
class CompressPlugin:
def process(self, data: str) -> str:
return f"compressed({len(data)} chars)"
def get_plugin(name: str):
cls = PLUGINS.get(name.lower())
if cls is None:
raise KeyError(f"알 수 없는 플러그인: {name}. 가능한 플러그인: {list(PLUGINS)}")
return cls()
# 사용
for plugin_name in ["text", "json", "compress"]:
plugin = get_plugin(plugin_name)
print(plugin.process("Hello, World!"))
dataclass와 조합
from dataclasses import dataclass, field
from typing import ClassVar
import json
def to_json_mixin(cls):
"""JSON 직렬화 메서드를 추가하는 데코레이터"""
def to_dict(self) -> dict:
result = {}
for f in cls.__dataclass_fields__:
value = getattr(self, f)
if hasattr(value, "to_dict"):
result[f] = value.to_dict()
elif isinstance(value, list):
result[f] = [
v.to_dict() if hasattr(v, "to_dict") else v
for v in value
]
else:
result[f] = value
return result
def to_json(self, indent: int | None = None) -> str:
return json.dumps(self.to_dict(), ensure_ascii=False, indent=indent)
@classmethod
def from_dict(klass, data: dict):
return klass(**{
k: v for k, v in data.items()
if k in klass.__dataclass_fields__
})
@classmethod
def from_json(klass, json_str: str):
return klass.from_dict(json.loads(json_str))
cls.to_dict = to_dict
cls.to_json = to_json
cls.from_dict = from_dict
cls.from_json = from_json
return cls
@to_json_mixin
@dataclass
class Product:
name: str
price: float
stock: int
tags: list[str] = field(default_factory=list)
@to_json_mixin
@dataclass
class Order:
order_id: str
items: list[Product] = field(default_factory=list)
total: float = 0.0
product = Product("파이썬 책", 35000, 100, ["교육", "프로그래밍"])
print(product.to_json(indent=2))
order = Order("ORD-001", [product], 35000)
print(order.to_json())
# 역직렬화 (중첩 객체는 별도 처리 필요)
json_str = product.to_json()
restored = Product.from_json(json_str)
print(restored)
고수 팁
1. 클래스 데코레이터 vs 상속
# 클래스 데코레이터: 여러 클래스에 동일 기능을 추가할 때 유리
def serializable(cls):
"""JSON 직렬화를 추가"""
import json
def to_json(self) -> str:
return json.dumps(vars(self), ensure_ascii=False)
cls.to_json = to_json
return cls
@serializable
class User:
def __init__(self, name: str):
self.name = name
@serializable
class Product:
def __init__(self, name: str, price: float):
self.name = name
self.price = price
# 상속: 공통 인터페이스가 필요할 때
class Serializable:
def to_json(self) -> str:
import json
return json.dumps(vars(self), ensure_ascii=False)
class Admin(Serializable):
def __init__(self, name: str, level: int):
self.name = name
self.level = level
2. __init_subclass__와 조합
class AutoRegister:
"""서브클래스를 자동 등록하는 기반 클래스"""
_subclasses: dict[str, type] = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
AutoRegister._subclasses[cls.__name__] = cls
@classmethod
def create(cls, name: str) -> "AutoRegister":
klass = cls._subclasses.get(name)
if klass is None:
raise KeyError(f"등록되지 않음: {name}")
return klass()
class PluginA(AutoRegister):
def run(self) -> str:
return "Plugin A 실행"
class PluginB(AutoRegister):
def run(self) -> str:
return "Plugin B 실행"
plugin = AutoRegister.create("PluginA")
print(plugin.run()) # Plugin A 실행
정리
| 용도 | 패턴 |
|---|---|
| 메서드 추가 | cls.method = new_method; return cls |
| 싱글톤 | 클로저로 인스턴스 저장 |
| 타임스탬프/감사 | __init__ 래핑 |
| 레지스트리 | 클래스를 dict에 등록 |
| 타입 검증 | __annotations__ 기반 __init__ 래핑 |
클래스 데코레이터는 dataclass, @property 등 파이썬 내장 데코레이터와 잘 조합되며, 횡단 관심사(cross-cutting concerns)를 깔끔하게 분리할 수 있습니다.