메타클래스(Metaclass) 입문
메타클래스는 "클래스의 클래스"입니다. 일반 클래스가 인스턴스를 만들듯, 메타클래스는 클래스를 만들고 제어합니다. 파이썬의 모든 클래스는 기본적으로 type의 인스턴스입니다.
type — 파이썬의 기본 메타클래스
# type()으로 클래스의 타입 확인
print(type(42)) # <class 'int'>
print(type("hello")) # <class 'str'>
print(type(int)) # <class 'type'>
print(type(type)) # <class 'type'> (type 자신도 type의 인스턴스)
# type()으로 동적으로 클래스 생성
# type(이름, 기반클래스, 속성_딕셔너리)
MyClass = type(
"MyClass", # 클래스 이름
(object,), # 기반 클래스 튜플
{
"x": 10, # 클래스 속성
"greet": lambda self: f"안녕! x={self.x}", # 메서드
}
)
obj = MyClass()
print(obj.greet()) # 안녕! x=10
print(type(obj)) # <class '__main__.MyClass'>
print(type(MyClass)) # <class 'type'>
# class 문으로 정의한 클래스와 동일
class EquivalentClass:
x = 10
def greet(self):
return f"안녕! x={self.x}"
print(MyClass.__bases__) # (<class 'object'>,)
print(EquivalentClass.__bases__) # (<class 'object'>,)
메타클래스 정의
class MyMeta(type):
"""커스텀 메타클래스 — type을 상속"""
def __new__(mcs, name, bases, namespace):
"""클래스 객체 생성 전 호출"""
print(f"[MetaNew] 클래스 생성: {name}")
# namespace: 클래스 본문에서 정의된 속성들의 딕셔너리
cls = super().__new__(mcs, name, bases, namespace)
return cls
def __init__(cls, name, bases, namespace):
"""클래스 객체 생성 후 호출"""
print(f"[MetaInit] 클래스 초기화: {name}")
super().__init__(name, bases, namespace)
def __call__(cls, *args, **kwargs):
"""cls() 호출 시 (인스턴스 생성)"""
print(f"[MetaCall] {cls.__name__} 인스턴스 생성")
instance = super().__call__(*args, **kwargs)
return instance
class MyClass(metaclass=MyMeta):
def __init__(self, value: int):
self.value = value
print("--- 인스턴스 생성 ---")
obj = MyClass(42)
print(f"value: {obj.value}")
실전 메타클래스 패턴
1. 싱글톤 메타클래스
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 AppConfig(metaclass=SingletonMeta):
def __init__(self):
self.debug = False
self.db_url = "sqlite:///app.db"
print("AppConfig 초기화")
class Logger(metaclass=SingletonMeta):
def __init__(self):
self._logs: list[str] = []
print("Logger 초기화")
def log(self, msg: str) -> None:
self._logs.append(msg)
config1 = AppConfig() # 초기화됨
config2 = AppConfig() # 기존 인스턴스 반환
print(config1 is config2) # True
logger1 = Logger()
logger2 = Logger()
logger1.log("첫 번째 로그")
print(f"로그 수: {len(logger2._logs)}") # 1 (같은 인스턴스)
2. 추상 메서드 강제 메타클래스
class InterfaceMeta(type):
"""지정된 메서드를 반드시 구현하도록 강제하는 메타클래스"""
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
# 기반 클래스가 있는 경우만 검사 (인터페이스 자신은 제외)
if bases:
required = set()
for base in bases:
required.update(getattr(base, "_required_methods", set()))
missing = required - set(namespace)
if missing:
raise TypeError(
f"{name}은 다음 메서드를 구현해야 합니다: {missing}"
)
return cls
class Animal(metaclass=InterfaceMeta):
_required_methods = {"speak", "move"}
def speak(self) -> str:
raise NotImplementedError
def move(self) -> str:
raise NotImplementedError
class Dog(Animal):
def speak(self) -> str:
return "왈왈!"
def move(self) -> str:
return "달린다!"
try:
class Fish(Animal):
def speak(self) -> str:
return "..."
# move 미구현 → TypeError
except TypeError as e:
print(f"오류: {e}")
dog = Dog()
print(dog.speak())
3. 속성 자동 변환 메타클래스
class SnakeCaseMeta(type):
"""camelCase 메서드를 snake_case로 자동 변환하는 메타클래스"""
@staticmethod
def _to_snake(name: str) -> str:
import re
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
def __new__(mcs, name, bases, namespace):
new_namespace = {}
for key, value in namespace.items():
if not key.startswith("_") and callable(value):
snake_key = mcs._to_snake(key)
if snake_key != key:
new_namespace[snake_key] = value # snake_case 추가
new_namespace[key] = value # 원본도 유지 (하위 호환)
else:
new_namespace[key] = value
else:
new_namespace[key] = value
return super().__new__(mcs, name, bases, new_namespace)
class APIClient(metaclass=SnakeCaseMeta):
def getUserById(self, user_id: int) -> dict:
return {"id": user_id, "name": "Alice"}
def createNewOrder(self, items: list) -> dict:
return {"order_id": 1, "items": items}
def deleteExpiredSessions(self) -> int:
return 5
client = APIClient()
# camelCase와 snake_case 모두 동작
print(client.getUserById(1))
print(client.get_user_by_id(1)) # snake_case 버전
print(client.createNewOrder(["책"]))
print(client.create_new_order(["책"])) # snake_case 버전
__new__와 __init__ 차이
class TrackedType(type):
"""생성된 모든 클래스를 추적하는 메타클래스"""
_all_classes: list = []
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
TrackedType._all_classes.append(cls)
return cls
def __init__(cls, name, bases, namespace):
super().__init__(name, bases, namespace)
# __new__에서 이미 클래스 생성됨 — __init__에서는 추가 설정
cls._created_at = __import__("time").time()
@classmethod
def get_all_classes(mcs) -> list:
return list(mcs._all_classes)
class Base(metaclass=TrackedType):
pass
class Child(Base):
pass
class AnotherClass(metaclass=TrackedType):
pass
print("추적된 클래스:")
for cls in TrackedType.get_all_classes():
print(f" {cls.__name__}")
__init_subclass__ — 메타클래스의 대안
Python 3.6+에서 도입된 __init_subclass__는 서브클래스 등록처럼 간단한 경우에 메타클래스보다 훨씬 간결합니다.
class Plugin:
"""서브클래스 자동 등록 — 메타클래스 없이"""
_registry: dict[str, type] = {}
def __init_subclass__(cls, plugin_type: str = "", **kwargs):
super().__init_subclass__(**kwargs)
if plugin_type:
Plugin._registry[plugin_type] = cls
print(f"플러그인 등록: {plugin_type} → {cls.__name__}")
@classmethod
def create(cls, plugin_type: str) -> "Plugin":
klass = cls._registry.get(plugin_type)
if klass is None:
available = list(cls._registry)
raise KeyError(f"알 수 없는 플러그인: {plugin_type}. 가능: {available}")
return klass()
def execute(self) -> str:
raise NotImplementedError
class CSVPlugin(Plugin, plugin_type="csv"):
def execute(self) -> str:
return "CSV 처리 완료"
class JSONPlugin(Plugin, plugin_type="json"):
def execute(self) -> str:
return "JSON 처리 완료"
class XMLPlugin(Plugin, plugin_type="xml"):
def execute(self) -> str:
return "XML 처리 완료"
for pt in ["csv", "json", "xml"]:
p = Plugin.create(pt)
print(p.execute())
메타클래스 실전 활용: ORM 스타일
class Field:
"""ORM 필드 기반 클래스"""
def __init__(self, field_type: type, required: bool = True, default=None):
self.field_type = field_type
self.required = required
self.default = default
self.name = None # __set_name__으로 설정됨
def __set_name__(self, owner, name: str):
self.name = name
def validate(self, value):
if value is None:
if self.required and self.default is None:
raise ValueError(f"{self.name}: 필수 필드입니다.")
return self.default
if not isinstance(value, self.field_type):
raise TypeError(
f"{self.name}: {self.field_type.__name__} 기대, "
f"{type(value).__name__} 받음"
)
return value
class IntField(Field):
def __init__(self, required: bool = True, min_val=None, max_val=None, **kwargs):
super().__init__(int, required, **kwargs)
self.min_val = min_val
self.max_val = max_val
def validate(self, value):
value = super().validate(value)
if value is not None:
if self.min_val is not None and value < self.min_val:
raise ValueError(f"{self.name}: {value} < 최솟값 {self.min_val}")
if self.max_val is not None and value > self.max_val:
raise ValueError(f"{self.name}: {value} > 최댓값 {self.max_val}")
return value
class StrField(Field):
def __init__(self, required: bool = True, max_length: int = None, **kwargs):
super().__init__(str, required, **kwargs)
self.max_length = max_length
def validate(self, value):
value = super().validate(value)
if value is not None and self.max_length and len(value) > self.max_length:
raise ValueError(f"{self.name}: 최대 {self.max_length}자 초과")
return value
class ModelMeta(type):
"""ORM 모델을 위한 메타클래스"""
def __new__(mcs, name, bases, namespace):
fields = {}
for key, value in namespace.items():
if isinstance(value, Field):
fields[key] = value
namespace["_fields"] = fields
return super().__new__(mcs, name, bases, namespace)
class Model(metaclass=ModelMeta):
"""ORM 기반 모델 클래스"""
def __init__(self, **kwargs):
for field_name, field in self._fields.items():
raw_value = kwargs.get(field_name)
validated = field.validate(raw_value)
object.__setattr__(self, field_name, validated)
def __repr__(self) -> str:
attrs = ", ".join(
f"{k}={getattr(self, k)!r}"
for k in self._fields
)
return f"{self.__class__.__name__}({attrs})"
def to_dict(self) -> dict:
return {k: getattr(self, k) for k in self._fields}
@classmethod
def fields(cls) -> list[str]:
return list(cls._fields)
class User(Model):
name = StrField(max_length=50)
email = StrField(max_length=100)
age = IntField(min_val=0, max_val=150, required=False, default=0)
score = IntField(min_val=0, max_val=100, required=False, default=50)
# 정상 생성
u = User(name="Alice", email="alice@example.com", age=30)
print(u)
print(u.to_dict())
# 기본값 사용
u2 = User(name="Bob", email="bob@example.com")
print(u2) # age=0, score=50
# 유효성 검사 오류
try:
User(name="Charlie", email="charlie@example.com", age=200)
except ValueError as e:
print(f"오류: {e}")
try:
User(name="X" * 100, email="x@example.com")
except ValueError as e:
print(f"오류: {e}")
고수 팁
1. 메타클래스를 사용해야 하는 경우
# 메타클래스 필요:
# - 클래스 생성 자체를 제어해야 할 때
# - 모든 서브클래스에 공통 로직을 투명하게 적용할 때
# - ORM, 직렬화 프레임워크처럼 클래스 정의를 분석해야 할 때
# 메타클래스 불필요 (대안 사용):
# - 단순 속성 추가 → 클래스 데코레이터
# - 서브클래스 등록 → __init_subclass__
# - 속성 검증 → 디스크립터
# - 인스턴스 공유 → 모듈 레벨 변수
# "메타클래스는 99%의 사용자에게 필요 없는 심층 마법이다." — Tim Peters
2. 메타클래스 충돌 해결
# 두 메타클래스를 함께 사용해야 할 때 새 메타클래스로 결합
class MetaA(type):
def __new__(mcs, name, bases, ns):
print(f"MetaA: {name}")
return super().__new__(mcs, name, bases, ns)
class MetaB(type):
def __new__(mcs, name, bases, ns):
print(f"MetaB: {name}")
return super().__new__(mcs, name, bases, ns)
# 두 메타클래스를 모두 상속하는 새 메타클래스
class CombinedMeta(MetaA, MetaB):
pass
class MyClass(metaclass=CombinedMeta):
pass
정리
| 기능 | 메타클래스 | 대안 |
|---|---|---|
| 클래스 생성 제어 | type.__new__ | — |
| 서브클래스 등록 | __new__ | __init_subclass__ (권장) |
| 속성 검증 | __new__ | 디스크립터 (권장) |
| 클래스 꾸미기 | __init__ | 클래스 데코레이터 (권장) |
| 싱글톤 | __call__ | 클래스 데코레이터 또는 모듈 변수 |
메타클래스는 강력하지만 복잡합니다. 가능하면 __init_subclass__, 디스크립터, 클래스 데코레이터 등 더 간단한 대안을 먼저 고려하세요.