본문으로 건너뛰기
Advertisement

클래스 데코레이터

클래스 데코레이터는 함수 데코레이터와 동일한 문법(@)으로 클래스에 적용되어 클래스 정의를 수정하거나 새 클래스로 대체합니다.


클래스 데코레이터 기본

# 클래스 데코레이터 = 클래스를 인수로 받아 수정된 클래스(또는 새 클래스)를 반환하는 함수
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)를 깔끔하게 분리할 수 있습니다.

Advertisement