목차
17 장

메타클래스 — 언제 정말 필요한가

클래스를 만드는 클래스를 다룹니다. type의 정체, __init_subclass__와의 분담, 클래스 데코레이터로 풀 수 있는 경우, 그리고 진짜 메타클래스가 필요한 좁은 영역까지 정리합니다.

16장 디스크립터와 __set_name__에서 속성을 객체화하는 도구를 봤다면, 본 챕터는 클래스 자체를 객체로 다루는 도구 — 메타클래스입니다. “고급 파이썬"의 상징처럼 자주 언급되지만, 실제 코드에서는 드물게 쓰는 편이 더 안전합니다. 무엇이 메타클래스인지, 그리고 더 가벼운 대안들로 어디까지 풀 수 있는지를 정리합니다.

본 챕터의 핵심 메시지는 “메타클래스가 풀어주는 일의 90%는 12장 데코레이터 패턴의 클래스 데코레이터 또는 15장 매직 메소드__init_subclass__로 풀린다"입니다. 진짜 메타클래스가 필요한 좁은 영역을 식별하는 게 본 챕터의 목적입니다.

출발점 — 클래스도 객체 #

파이썬에서는 클래스도 객체입니다.

클래스 = 객체
class User:
    pass

print(type(User))   # <class 'type'>
print(User.__class__)   # <class 'type'>

User 클래스의 **타입은 type**입니다. 즉 Usertype의 인스턴스입니다. 모든 클래스는 어떤 타입(메타클래스)의 인스턴스로 만들어집니다. 기본 메타클래스가 type입니다.

type으로 클래스 동적으로 만들기 #

type으로 클래스 생성
def greet(self):
    print(f"hi, {self.name}")

# 동등: class User: ...
User = type("User", (object,), {
    "name": "default",
    "greet": greet,
})

u = User()
u.greet()   # hi, default

type(name, bases, namespace)클래스를 동적으로 만드는 함수입니다. 사실 class User: 문법이 내부적으로 이걸 호출합니다.

이건 드물게 쓰는 기능 이지만, 메타클래스의 정체를 보여줍니다 — 메타클래스란 결국 type의 서브클래스입니다.

메타클래스 정의 #

메타클래스
class Meta(type):
    def __new__(mcs, name, bases, namespace):
        print(f"클래스 만드는 중: {name}")
        return super().__new__(mcs, name, bases, namespace)

class User(metaclass=Meta):
    pass

# 출력: 클래스 만드는 중: User

metaclass=Meta로 어떤 메타클래스로 만들지 지정합니다. 클래스가 정의되는 그 순간 Meta.__new__가 호출됩니다.

__init_subclass__ — 메타클래스가 필요 없을 때 #

15장에서 짧게 본 __init_subclass__거의 모든 메타클래스 일을 풀어줍니다. 메타클래스 없이.

자동 등록 — __init_subclass__
class Plugin:
    registry = []

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        Plugin.registry.append(cls)

class JsonPlugin(Plugin): pass
class CsvPlugin(Plugin): pass

print(Plugin.registry)
# [<class 'JsonPlugin'>, <class 'CsvPlugin'>]

같은 일을 메타클래스로 하면:

같은 일 — 메타클래스
class PluginMeta(type):
    registry = []
    def __init__(cls, name, bases, namespace):
        super().__init__(name, bases, namespace)
        if name != "Plugin":
            PluginMeta.registry.append(cls)

class Plugin(metaclass=PluginMeta): pass
class JsonPlugin(Plugin): pass
class CsvPlugin(Plugin): pass

길고, 베이스 클래스 자체를 등록하지 않기 위한 if name != "Plugin" 같은 예외 처리도 필요합니다. 일반적인 자동 등록은 __init_subclass__가 더 단순 합니다.

클래스 데코레이터 — 또 다른 대안 #

12장 데코레이터 패턴의 클래스 데코레이터도 메타클래스가 할 일의 상당 부분을 합니다.

클래스 데코레이터로
def register(cls):
    Plugin.registry.append(cls)
    return cls

class Plugin:
    registry = []

@register
class JsonPlugin(Plugin): pass

@register
class CsvPlugin(Plugin): pass

명시적이라는 장점이 있습니다. 암묵적인 등록보다 명시적인 등록이 좋을 때가 많습니다.

그럼 메타클래스는 언제? #

다음 경우에는 메타클래스가 가장 어울립니다.

1) type 자체의 동작을 바꾸고 싶을 때 #

isinstance, issubclass 같은 검사 동작을 메타클래스가 결정합니다. ABC (abc.ABCMeta)가 그 예입니다.

ABC = 메타클래스
from abc import ABC, abstractmethod

class Storage(ABC):
    @abstractmethod
    def save(self, data): ...

class FileStorage(Storage):
    def save(self, data): ...

class BadStorage(Storage):
    pass

s = FileStorage()    # OK
b = BadStorage()     # ✗ TypeError: Can't instantiate abstract class

@abstractmethod가 있는 클래스를 인스턴스화 못 하게 막는 동작이 메타클래스 레벨에서 일어납니다. 이런 경우는 __init_subclass__로 풀기 어렵거나 불가능합니다.

2) DSL — 클래스 정의 자체를 새 문법처럼 #

ORM 모델 — 사용자 관점
class User(Model):
    id = IntegerField(primary_key=True)
    name = StringField(max_length=50)

이런 코드에서 메타클래스가 하는 일:

  • 어떤 필드들이 정의됐는지 수집
  • 테이블 이름 결정
  • _meta 같은 메타데이터 객체 생성
  • 검증 메소드 자동 추가

Django ORM, SQLAlchemy, Peewee 등이 이 패턴을 씁니다. 클래스 정의를 일종의 DSL 로 쓸 때 메타클래스가 필요합니다. 25장 DB 연동 — SQLAlchemy 2.x + Alembic의 ORM 모델이 여기에 해당합니다.

3) 다중 상속에서 일관된 동작 보장 #

여러 베이스 클래스가 각자 다른 메타클래스를 가지면 충돌이 납니다. 메타클래스 한 단계 위에서 모든 동작을 통제할 때.

이건 일반 코드에서는 거의 없습니다. 라이브러리 작성자에게 해당하는 영역입니다.

메타클래스 vs 다른 도구 — 결정 가이드 #

첫 시도
서브클래스를 자동 등록__init_subclass__
한 클래스에 동작 추가 (eq, repr 등)클래스 데코레이터 (@dataclass 등)
속성 접근 가로채기디스크립터
인스턴스 메소드 / 속성 추가일반 메소드 / 속성
isinstance / issubclass 동작 변경메타클래스
베이스 클래스 정의 자체를 DSL처럼메타클래스
클래스 정의 시점에 문법 검증메타클래스 또는 __init_subclass__

판단 기준: __init_subclass__ 또는 클래스 데코레이터로 해결할 수 있다면 그쪽이 대체로 더 가볍습니다. 메타클래스는 그것들이 풀 수 없을 때 선택합니다.

__init_subclass__의 실력 #

본 후크가 얼마나 많은 일을 할 수 있는지 보면, 메타클래스가 줄어드는 이유가 보입니다.

자동 등록 #

등록
class Handler:
    handlers = {}
    def __init_subclass__(cls, *, route=None, **kwargs):
        super().__init_subclass__(**kwargs)
        if route:
            Handler.handlers[route] = cls

class HomeHandler(Handler, route="/"): pass
class AboutHandler(Handler, route="/about"): pass

print(Handler.handlers)
# {'/': <class 'HomeHandler'>, '/about': <class 'AboutHandler'>}

서브클래스 정의부에 route="/" 같은 인자를 넘길 수 있다는 것도 잘 알려지지 않은 사실입니다. __init_subclass__가 그 인자를 받습니다.

필수 메소드 강제 #

추상 메소드 강제
class Service:
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        for method in ("start", "stop"):
            if not hasattr(cls, method):
                raise TypeError(f"{cls.__name__}{method} 가 필요")

class Good(Service):
    def start(self): ...
    def stop(self): ...

class Bad(Service):    # ✗ TypeError
    def start(self): ...

abc.ABC보다 가벼운 강제 — 직접 일을 정의할 수 있습니다.

클래스 변수 검증 #

필드 검증
class Form:
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        if not hasattr(cls, "name"):
            raise TypeError(f"{cls.__name__}.name 필요")
        if not isinstance(cls.name, str):
            raise TypeError("name 은 str")

class LoginForm(Form):
    name = "login"

__class_getitem__ — 제네릭 문법 후크 #

MyClass[int] 같은 표기를 가능하게 하는 후크.

__class_getitem__
class Box:
    def __class_getitem__(cls, item):
        return f"Box[{item.__name__}]"

print(Box[int])   # Box[int]

list[int], dict[str, int] 같은 빌트인 제네릭이 본 후크 위에 만들어졌습니다. 사용자 클래스를 제네릭으로 만들 때 (Python 3.12의 class Foo[T]: 새 문법 이전 방식) 본 후크가 일을 했습니다. 새 코드는 9장 typing 본격class Stack[T]: 문법을 씁니다.

type.__call__ — 인스턴스 생성 흐름 #

User()가 실행되면:

  1. type(User).__call__(User)가 호출됨 (메타클래스의 __call__)
  2. 그 안에서 User.__new__(User)로 인스턴스 생성
  3. User.__init__(instance)로 초기화

본 흐름을 가로채려면 메타클래스의 __call__을 오버라이드합니다. 싱글턴, 인스턴스 캐싱 같은 경우에 등장.

싱글턴 — 메타클래스 버전
class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class Logger(metaclass=SingletonMeta):
    def __init__(self):
        self.history = []

a = Logger()
b = Logger()
print(a is b)   # True

__new__ 만으로 가능한 일이지만, __init__을 두 번 호출하는 함정이 있어 메타클래스 쪽이 더 안전한 경우가 있습니다.

메타클래스 충돌 #

서브클래스의 메타클래스는 모든 부모의 메타클래스의 서브타입이어야 합니다.

🚫 충돌
class MetaA(type): pass
class MetaB(type): pass

class A(metaclass=MetaA): pass
class B(metaclass=MetaB): pass

class C(A, B): pass
# TypeError: metaclass conflict

여러 라이브러리의 클래스를 다중 상속할 때 만나는 골치 — 그래서 라이브러리 작성자도 메타클래스를 신중하게 씁니다.

연습문제 #

  1. __init_subclass__로 자동 등록기를 만드세요. class Handler:의 서브클래스 정의부에 route="/path" 같은 키워드 인자를 받아 Handler.routes: dict[str, type]에 등록합니다. HomeHandler, AboutHandler 두 서브클래스로 동작을 확인합니다.
  2. __init_subclass__로 필수 메소드 강제 검증을 만드세요. 서브클래스가 start / stop 메소드를 가지지 않으면 정의 시점에 TypeError가 발생합니다.
  3. 메타클래스 SingletonMeta로 싱글턴 패턴을 구현하세요. 같은 클래스의 두 번째 호출이 첫 번째 인스턴스를 반환하는지 확인합니다. 같은 일을 __new__ 만으로 구현한 뒤 두 방식의 함정(특히 __init__가 매번 호출되는지)을 비교합니다.

한 줄 요약: 클래스도 객체, 클래스의 타입이 type (기본 메타클래스). 메타클래스는 type의 서브클래스. 일반적인 메타클래스 일의 90% 이상은 __init_subclass__ 또는 클래스 데코레이터로 풀린다. 진짜 메타클래스가 필요한 경우는 ABC, ORM DSL, 다중 상속 일관성 같은 좁은 영역. 메타클래스 충돌이 잦아 가능한 한 안 쓰는 쪽이 좋다.

다음 챕터 #

다음 18장 비동기 깊이 — 이벤트 루프, gather/wait, async generator에서는 14장 비동기 입문에서 본 asyncio의 다음 단계 — 이벤트 루프의 동작, gather / wait의 미세한 차이, async generator, 컨커런시 패턴들을 다룹니다.

X