
파이썬 데코레이터 심층 분석
파이썬의 데코레이터는 기존 함수의 소스 코드를 건드리지 않고도 그 기능을 확장하거나 변경할 수 있도록 돕는 유연하고 강력한 기능입니다. ‘장식하다’라는 뜻처럼, 함수에 추가적인 기능을 덧붙이는 역할을 수행합니다.
데코레이터의 핵심 원리
데코레이터는 본질적으로 다음 세 가지 특성을 가집니다.
- 함수를 인수로 받는 함수: 다른 함수를 입력으로 받아들입니다.
- 새로운 함수를 반환하는 함수: 입력받은 함수를 “감싸서” 추가 기능이 적용된 새로운 함수를 만들어 반환합니다.
- 구문 간편화:
@
기호를 사용하여함수 = 데코레이터(함수)
와 같은 복잡한 코드를 간결하게 표현할 수 있게 해주는 기능입니다.
데코레이터가 가능한 이유: 일급 객체로서의 함수
데코레이터가 파이썬에서 구현 가능한 이유는 함수가 ‘일급 객체’이기 때문입니다. 일급 객체란 다른 데이터 타입처럼 자유롭게 다룰 수 있는 대상을 의미하며, 함수가 다음 조건을 만족한다는 뜻입니다.
- 변수에 할당할 수 있습니다.
- 다른 함수의 인수로 전달할 수 있습니다.
- 다른 함수의 반환 값으로 사용할 수 있습니다.
- 리스트나 딕셔너리 같은 자료 구조에 저장할 수 있습니다.
데코레이터의 기본 구조와 동작 방식
가장 기본적인 데코레이터는 함수를 인수로 받아 새로운 함수를 반환하는 중첩 함수 구조를 가집니다.
def my_decorator(func): # 1. 함수(func)를 인수로 받음
def wrapper(*args, **kwargs): # 2. func를 감싸는 내부 함수(wrapper) 정의
print("함수 실행 전에 할 작업") # 3. 추가 기능 (함수 실행 전)
result = func(*args, **kwargs) # 4. 원본 함수(func) 실행
print("함수 실행 후에 할 작업") # 5. 추가 기능 (함수 실행 후)
return result # 6. 원본 함수의 결과 반환
return wrapper # 7. wrapper 함수를 반환
이 데코레이터를 사용하는 방법은 두 가지가 있습니다.
1. @ 구문 활용 (일반적으로 사용)
이 방식은 코드를 더 직관적으로 만듭니다.
@my_decorator
def say_hello():
print("안녕하세요!")
say_hello()
위 코드는 내부적으로 say_hello = my_decorator(say_hello)
와 동일하게 작동합니다. 즉, say_hello
변수에 my_decorator
가 반환한 wrapper
함수가 할당됩니다. 따라서 say_hello()
를 호출하면 실제로는 wrapper
함수가 실행됩니다.
2. 직접 할당 방식
위 @ 구문이 어떻게 동작하는지 이해하는 데 도움이 됩니다.
def say_hello():
print("안녕하세요!")
decorated_say_hello = my_decorator(say_hello)
decorated_say_hello()
데코레이터의 다양한 활용 사례
데코레이터는 여러 상황에서 유용하게 사용됩니다.
- 로그 기록: 함수 호출 전후에 로그를 남기거나 함수의 입력 및 출력 값을 기록할 때 사용합니다.
- 성능 분석: 함수의 실행 시간을 측정하여 성능을 분석할 때 활용합니다.
- 사용자 인증 및 권한 확인: 특정 함수를 실행하기 전에 사용자 로그인 상태나 권한을 확인해야 할 때 사용합니다.
- 데이터 캐싱: 함수의 결과를 저장해두고, 동일한 입력으로 다시 호출될 때 연산을 다시 수행하지 않고 저장된 값을 반환합니다.
- 프레임워크: Django나 Flask 같은 웹 프레임워크에서 URL 라우팅 (
@app.route('/hello')
) 등에 널리 쓰입니다.
예제: 함수 실행 시간 측정
다음은 함수의 실행 시간을 측정하는 데코레이터 예시입니다.
import time
def timer_decorator(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"'{func.__name__}' 함수 실행 시간: {end_time - start_time:.4f}초")
return result
return wrapper
@timer_decorator
def long_running_task():
time.sleep(2) # 2초간 대기
print("작업 완료!")
@timer_decorator
def calculate_sum(a, b):
time.sleep(0.5) # 0.5초 대기
return a + b
long_running_task()
print(calculate_sum(10, 20))
인수를 받는 데코레이터
데코레이터도 인수를 받을 수 있습니다. 이 경우, 세 개의 중첩된 함수 구조를 가집니다. 가장 바깥 함수는 데코레이터의 인수를 받고, 중간 함수는 실제 데코레이터 역할을 하며, 가장 안쪽 함수는 원본 함수를 감싸는 역할을 합니다.
def repeat(num_times):
def decorator_repeat(func):
def wrapper(*args, **kwargs):
for _ in range(num_times):
func(*args, **kwargs)
return wrapper
return decorator_repeat
@repeat(num_times=3)
def greet(name):
print(f"안녕하세요, {name}!")
greet("앨리스")
위 예제에서 @repeat(num_times=3)
는 먼저 repeat(3)
을 호출하고, 이 호출이 반환하는 decorator_repeat
함수가 greet
함수에 적용됩니다.
클래스를 이용한 데코레이터
데코레이터는 함수 외에 클래스로도 만들 수 있습니다. 클래스 데코레이터는 상태(state)를 유지할 수 있다는 장점이 있습니다. 클래스 데코레이터를 만들 때는 __init__
메서드에서 데코레이트될 함수를 받고, __call__
메서드에서 데코레이션 로직을 정의합니다.
class CountCalls:
def __init__(self, func):
self.func = func
self.num_calls = 0
def __call__(self, *args, **kwargs):
self.num_calls += 1
print(f"'{self.func.__name__}' 함수 호출 횟수: {self.num_calls}회")
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print("Hello!")
say_hello()
say_hello()
say_hello()
클래스 메서드에 데코레이터 적용하기
클래스 내부의 메서드에 데코레이터를 적용하는 방법은 일반 함수에 적용하는 것과 동일합니다. 단, 메서드는 첫 번째 인수로 self
를 받으므로, 데코레이터의 래퍼(wrapper) 함수 또한 self
를 반드시 포함해야 합니다.
def my_method_decorator(func):
def wrapper(self, *args, **kwargs):
print(f"'{self.__class__.__name__}.{func.__name__}' 메서드 실행 전")
result = func(self, *args, **kwargs)
print(f"'{self.__class__.__name__}.{func.__name__}' 메서드 실행 후")
return result
return wrapper
class MyClass:
def __init__(self, name):
self.name = name
@my_method_decorator
def greet(self):
print(f"안녕하세요, {self.name}!")
obj = MyClass("김민준")
obj.greet()
클래스 데코레이터의 기본 개념
클래스 데코레이터는 인수로 클래스 자체를 받아 새로운 클래스를 반환합니다. class
키워드 바로 위에 @
기호와 함께 사용됩니다.
def my_class_decorator(cls):
print(f"클래스 '{cls.__name__}'를 데코레이트합니다.")
cls.new_method = lambda self: print("데코레이터가 추가한 새로운 메서드입니다.")
return cls
@my_class_decorator
class MyClass:
def __init__(self, name):
self.name = name
def greet(self):
print(f"Hello, {self.name}!")
obj = MyClass("Charlie")
obj.greet()
obj.new_method()
functools.wraps
의 중요성
데코레이터를 사용하면 원본 함수의 이름이나 문서 문자열(__doc__
) 같은 메타데이터가 손실될 수 있습니다. 이를 방지하기 위해 파이썬 표준 라이브러리의 functools.wraps
를 사용합니다.
import functools
def simple_decorator(func):
@functools.wraps(func) # 이 줄이 핵심
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@simple_decorator
def my_function():
"""이 함수는 중요한 작업을 수행합니다."""
pass
print(my_function.__name__)
print(my_function.__doc__)
@functools.wraps(func)
를 사용하면 wrapper
함수가 원본 함수 func
의 메타데이터를 그대로 유지하게 되어 디버깅 및 문서화에 큰 도움이 됩니다.
마무리
파이썬의 데코레이터는 코드의 재사용성을 높이고, 기능을 모듈화하여 여러 함수에 쉽게 적용할 수 있는 강력한 패턴입니다. 함수의 동작을 직접 수정하지 않고도 확장할 수 있어 많은 파이썬 프레임워크와 라이브러리에서 핵심적인 역할을 수행합니다.