파이썬의 classmethod vs staticmethod

파이썬의 classmethod와 staticmethod를 예제로 비교한다.

요약

메서드가 자신이 속한 클래스 를 알아야 할 때 @classmethod 를 쓴다 (예: 대체 생성자, 다형적 팩토리, 클래스 전역 설정). 인스턴스(self)나 클래스(cls) 접근이 필요 없고 그저 클래스 아래 네임스페이스 를 갖는 헬퍼 함수를 원할 때 @staticmethod 를 쓴다.

이 데코레이터들이 존재하는 이유

파이썬 메서드는 그저 클래스 위에 놓인 함수다. 접근하는 순간 파이썬의 디스크립터 프로토콜이 첫 번째 인자로 무엇을 바인딩할지 결정한다:

  • 인스턴스 메서드 (기본): 인스턴스를 바인딩한다 → 첫 매개변수는 self.
  • classmethod: 클래스를 바인딩한다 → 첫 매개변수는 cls.
  • staticmethod: 아무것도 바인딩하지 않는다 → 암묵적 첫 매개변수가 없다.

이 바인딩 동작이 올바른 데코레이터를 고르는 핵심 사고 모델이다.

간단한 정의

  • @classmethod

    • 첫 매개변수가 cls(메서드를 호출한 실제 클래스)다.
    • 클래스 속성을 볼 수 있고, 인스턴스를 만들어 반환할 수 있으며, 상속과 다형성을 자연스럽게 지원한다.
  • @staticmethod

    • self나 cls가 주입되지 않는다.
    • 정리나 발견 용이성을 위해 클래스 위에 놓인 순수 헬퍼 함수다.

나란히 놓고 보는 치트시트

특성 인스턴스 메서드 classmethod staticmethod
첫 인자 self cls (없음)
인스턴스 상태 읽기 ✅ ❌ ❌
클래스 상태 읽기/변경 ⚠️ self.__class__ 경유 ✅ ❌
대체 생성자 ❌ ✅ ❌
서브클래스별 다형적 디스패치 ✅ ✅ ⚠️ (조회는 되지만 바인딩은 없음)
유틸리티에 적합 ❌ ⚠️ ✅
테스트 용이성 좋음 좋음 훌륭함 (순수 함수)

@classmethod를 쓸 때

대체 생성자

다른 표현으로부터 인스턴스를 만드는 명확한 방법을 독자에게 제공한다.

class User:
    def __init__(self, name, is_admin=False):
        self.name = name
        self.is_admin = is_admin

    @classmethod
    def from_env(cls):
        import os
        return cls(os.getenv("USER", "anonymous"))

    @classmethod
    def admin(cls, name):
        return cls(name, is_admin=True)

다형적 팩토리

서브클래스는 classmethod를 상속받고 재정의할 수 있다. 서브클래스를 통해 호출하면 cls가 그 서브클래스에 바인딩되므로, 팩토리가 계속 올바른 타입을 반환한다.

class User:
    def __init__(self, name):
        self.name = name

    @classmethod
    def from_dict(cls, data):
        return cls(data["name"])

이제 서브클래스가 이를 재사용하면서 자동으로 자기 타입을 반환한다:

class Admin(User):
    pass

user = User.from_dict({"name": "Alice"})
admin = Admin.from_dict({"name": "Bob"})

print(type(user))   # <class '__main__.User'>
print(type(admin))  # <class '__main__.Admin'>

클래스 수준 설정과 레지스트리

공유되는 타입별 상태를 클래스에 둔다.

class Service:
    _registry = {}

    @classmethod
    def register(cls, name, impl):
        cls._registry[name] = impl

    @classmethod
    def get(cls, name):
        return cls._registry[name]

typing.Self를 쓴 버전별/타입별 생성자

파이썬 3.11부터는 Self로 “호출한 클래스의 인스턴스를 반환한다”를 표현한다.

from typing import Self

class Config:
    @classmethod
    def load(cls, path: str) -> Self:
        ...
        return cls(...)

@staticmethod를 쓸 때

클래스/인스턴스 상태를 건드리지 않는 순수 헬퍼

진짜 유틸리티라면 모듈 수준 함수를 선호하되, 클래스 옆에 두는 편이 발견 용이성이나 API 사용감을 개선한다면 예외로 한다.

class Password:
    @staticmethod
    def hash(plain: str) -> str:
        import hashlib
        return hashlib.sha256(plain.encode()).hexdigest()

여러 생성자가 쓰는 검증이나 작은 변환

관련 헬퍼를 쓰이는 곳 가까이 둔다.

class Color:
    @staticmethod
    def _clamp(x: int) -> int:
        return max(0, min(255, x))

경험 법칙: 명료함을 잃지 않고 모듈 스코프에 놓을 수 있다면 @staticmethod로 충분하다. 클래스를 아는 것이 이득이라면 @classmethod로 만든다.

흔한 함정과 피하는 법

  1. cls가 필요한데 @staticmethod를 씀

    • cls(...)를 반환할 가능성이 있거나(대체 생성자) 클래스 속성을 읽는다면 @classmethod를 쓴다.
  2. 묶기만 하려고 @classmethod를 씀

    • cls를 전혀 건드리지 않는다면 @staticmethod(또는 최상위 함수)가 낫다.
  3. 상속 동작을 헷갈림

    • @classmethod는 다형성에 자연스럽게 참여한다.
    • @staticmethod도 일반 속성 조회를 따르므로 재정의할 수 있지만, 클래스가 자동으로 바인딩되지는 않는다.
  4. ABC와 함께 쓸 때 데코레이터 순서

    • abc.ABC와는 이렇게 조합한다:

      from abc import ABC, abstractmethod
      
      class Repo(ABC):
          @classmethod
          @abstractmethod
          def from_url(cls, url: str): ...
      
          @staticmethod
          @abstractmethod
          def validate(url: str) -> bool: ...
      
    • (순서가 중요하다. @classmethod / @staticmethod를 @abstractmethod 위에 둔다.)

  5. 내장 @classproperty는 없다

    • 파이썬에는 인스턴스용 @property와 메서드용 @classmethod가 있다. 계산되는 클래스 수준 프로퍼티를 원한다면 작은 커스텀 디스크립터나 메타클래스가 필요하다. 대개는 def info(cls) 같은 단순한 @classmethod가 더 명확하다.

실제로 마주치게 되는 설계 패턴

  • 이름 있는 생성자: Path.home(), datetime.fromtimestamp(...)
  • 포맷 디코더: Message.from_json(...), Model.from_pretrained(...)
  • 레지스트리/플러그인: Handler.register(...), Handler.create(name)
  • 유틸리티 네임스페이스: Bytes.to_hex(...), Password.hash(...)

테스트 팁

  • @classmethod 대체 생성자는 기반 클래스와 서브클래스 호출을 모두 테스트해 다형성을 확인한다:

    • Base.from_x(...)는 Base를 반환
    • Sub.from_x(...)는 Sub를 반환
  • @staticmethod는 순수 함수처럼 다룬다:

    • 인스턴스/클래스용 픽스처가 필요 없다.
    • 빠르고 결정적인 단위 테스트가 된다.

결정 가이드

  1. 메서드에 인스턴스 가 필요한가? → 평범한 메서드 (데코레이터 없음).
  2. 메서드에 클래스 가 필요한가? (클래스 데이터를 읽거나, 생성하거나, 다형적이어야 하거나) → @classmethod.
  3. 둘 다 아니다? → @staticmethod (발견 용이성이 문제가 아니라면 모듈 수준 함수).

미니 FAQ

  • “staticmethod가 클래스 변수에 접근할 수 있나요?” 암묵적으로는 안 된다. 클래스를 이름으로 직접 참조해야 하는데(그러면 서브클래싱이 깨진다), 대개 나쁜 신호다. @classmethod를 선호하자.

  • ”@staticmethod가 더 빠른가요?” 바인딩 오버헤드를 피하긴 하지만 그 비용은 아주 작다. 미세 성능이 아니라 API 명료성을 기준으로 고르자.

  • “classmethod를 캐싱할 수 있나요?” 하위 함수를 functools.cache/lru_cache로 감쌀 수 있지만, 첫 인자가 cls라는 점에 유의해야 한다. 클래스 쪽에 캐싱하는 편(예: cls._cache[...])이 더 간단할 때가 많다.

정리

  • 타입을 아는 동작을 표현할 때 @classmethod 를 꺼내 든다. 대체 생성자, 다형적 팩토리, 클래스 수준 설정.
  • 클래스 옆에 두는 게 나은 순수 유틸리티 에는 @staticmethod 를 쓴다.
  • 어느 바인딩도 도움이 안 된다면 모듈 수준 함수가 가장 깔끔한 선택인 경우가 많다.

즐거운 파이썬 되세요! 🐍