본문 바로가기
👩🏻‍💻 Computer Science/clean-code(ft.python)

Chapter5. 데코레이터 활용(어느 곳에서나 동작하는 데코레이터 만들기)

by devhertz 2026. 10. 25.

어느 곳에서나 동작하는 데코레이터 만들기

함수에만 잘 동작하는 데코레이터가 아니라, 함수와 메서드 모두에서 잘 동작하는 범용 데코레이터를 만드는 방법을 알아보자

데코레이터를 만들 때 보통 함수를 기준으로 먼저 설계한다.

 

 

예제)
DBDriver 객체는 연결 문자열을 받아서 데이터베이스에 연결하고 DB 연산을 수행하는 객체이다. 메서드는 DB 정보 문자열을 받아서 DBDriver 인스턴스를 생성한다. 데코레이터는 이러한 변환을 자동화해 문자열을 받아 DBDriver를 생성하고 함수에 전달한다. 따라서
마치 객체를 직접 받은 것처럼 가정할 수 있다.

 

  • 이 데코레이터는 일반 함수에서는 잘 동작함
def inject_db_driver(function):
    @wraps(function)
    def wrapped(dbstring):          # 문자열 하나만 받는다고 가정
        return function(DBDriver(dbstring))
    return wrapped    

@inject_db_driver
def run_query(driver):
    return driver.execute("test_function")

run_query("test_OK")   # 정상 동작
  • 하지만 클래스 메서드에 붙이면 바로 에러남
class DataHandler:
    @inject_db_driver
    def run_query(self, driver):
        return driver.execute(self.__class__.__name__)

DataHandler().run_query("test_fails")
# TypeError: wrapped() takes 1 positional argument but 2 were given

 

 왜 문제 발생? 

  • 메서드는 호출될 때 첫 번째 인자로 self가 자동으로 들어감
  • 데코레이터의 wrapped는 dbstring 하나만 받도록 설계되어 있어서 self + dbstring 두 개가 들어가면서 인자가 맞지 않게 됨

 해결 방법: 디스크립터(Descriptor) 사용 

 

메서드에서도 잘 동작하게 하려면 디스크립터 프로토콜을 구현해야 한다.

  • 디스크립터의 핵심은 __get__ 메서드
  • 이 메서드가 “메서드로 사용될 때”와 “함수로 사용될 때”를 구분해서 처리해 줌

 

c.f.) 디스크립터(Descriptor): 객체의 속성 접근을 가로채는 특별한 객체

  • "상황에 따라 다르게 동작”하게 해줌
  • 클래스가 다음 메서드 중 하나 이상을 가지고 있으면 디스크립터가 됨
    • __get__
    • __set__
    • __delete__

 

범용 데코레이터 코드

from functools import wraps
from types import MethodType

class inject_db_driver:
    """문자열을 DBDriver 인스턴스로 변환하여 래핑된 함수에 전달"""

    def __init__(self, function) -> None:
        self.function = function
        wraps(self.function)(self)          # wraps를 객체에도 적용

    def __call__(self, dbstring):
        # 일반 함수처럼 호출될 때 실행됨
        return self.function(DBDriver(dbstring))

    def __get__(self, instance, owner):
        if instance is None:
            return self             # 클래스에서 직접 접근할 때(예: DataHandler.run_query)
        
        # 인스턴스에서 접근할 때(예: obj.run_query) → self를 바인딩한 새로운 데코레이터 객체를 만들어서 반환
        return self.__class__(MethodType(self.function, instance))
  • __get__이 하는 일
    • 정의: 객체에 접근할 때 파이썬이 자동으로 호출하는 메서드
    • 여기서 함수일 때는 그냥 호출 가능한 객체로 동작하고 메서드일 때는self를 제대로 바인딩해서 다시 데코레이터 객체를 만들어 줌.
      • 일반 함수로 사용될 때: __get__이 호출되지 않음 → __call__이 실행됨
      • 클래스 메서드로 사용될 때: __get__이 호출됨 → MethodType으로 self를 바인딩한 새 객체를 반환
    • instance: 실제 인스턴스 객체 (예: DataHandler()로 만든 객체). 클래스에서 직접 접근하면 None
    • owner: 이 속성이 정의된 클래스

 

 일반 함수에 적용 

@inject_db_driver
def run_query(driver):
    return driver.execute("test_function")

print(run_query("test_OK"))
  • @inject_db_driver는 inject_db_driver(run_query)를 실행 → 객체 생성
  • 나중에 run_query("test")를 호출하면 → 객체의 __call__이 실행됨(__get__은 호출되지 않음)

 

 클래스 메서드에 적용 

class DataHandler:
    @inject_db_driver
    def run_query(self, driver):
        return driver.execute(self.__class__.__name__)

print(DataHandler().run_query("test_OK"))
  • 클래스 정의 시점에inject_db_driver(run_query)가 실행되어 객체가 만들어짐
  • obj = DataHandler() 후 obj.run_query에 접근하는 순간
  • 파이썬이 __get__(self, instance=obj, owner=DataHandler)를 호출
  • __get__ 안에서 MethodType으로 self를 바인딩한 새로운 데코레이터 객체를 반환
  • 그 객체가 호출되면서 __call__이 실행되고, 문자열을 DBDriver로 변환

댓글