데코레이터의 본질
파이썬 데코레이터는 코드를 간결하고 재사용 가능하게 만드는 강력한 도구다.
데코레이터의 핵심 아이디어는 기존 함수나 클래스를 수정하지 않고도 그 동작을 확장하거나 변경할 수 있다는 점이다.
이는 개방-폐쇄 원칙(Open-Closed Principle)을 따르는 우아한 방법으로, 코드의 유지보수성과 확장성을 크게 향상시킨다.
데코레이터는 '@' 기호를 사용하여 함수나 클래스 위에 선언된다. 이 간단한 구문은 복잡한 로직을 숨기고, 코드의 가독성을 높이는 역할을 한다.
데코레이터의 작동 원리를 이해하기 위해서는 파이썬의 일급 객체(First-Class Objects) 개념을 알아야 한다.
일급 객체로서의 함수
파이썬에서 함수는 일급 객체다. 이는 함수를 변수에 할당하거나, 다른 함수의 인자로 전달하거나, 함수에서 반환할 수 있음을 의미한다. 이러한 특성이 데코레이터의 기반이 된다.
def greet(name):
return f"안녕하세요, {name}님!"
# 함수를 변수에 할당
say_hello = greet
# 함수를 인자로 전달
def apply_function(func, value):
return func(value)
result = apply_function(greet, "철수")
print(result) # 출력: 안녕하세요, 철수님!
이 예제에서 greet 함수는 변수에 할당되고 다른 함수의 인자로 전달된다. 이는 함수가 일급 객체임을 보여준다.
데코레이터의 기본 구조
데코레이터의 가장 기본적인 형태는 다음과 같다.
def my_decorator(func):
def wrapper():
print("함수 실행 전")
func()
print("함수 실행 후")
return wrapper
@my_decorator
def say_hello():
print("안녕하세요!")
say_hello()
이 코드를 실행하면 다음과 같은 출력이 나온다.
함수 실행 전
안녕하세요!
함수 실행 후
@my_decorator
구문은 사실 다음 코드의 축약형이다.
say_hello = my_decorator(say_hello)
데코레이터는 원래 함수를 받아 새로운 함수를 반환한다. 이 새로운 함수는 원래 함수를 감싸고 있으며, 추가적인 기능을 제공한다.
인자를 가진 함수에 데코레이터 적용하기
실제 상황에서는 대부분의 함수가 인자를 받는다. 이런 경우에도 데코레이터를 사용할 수 있다.
def log_function_call(func):
def wrapper(*args, **kwargs):
print(f"{func.__name__} 함수 호출. 인자: {args}, {kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} 함수 반환. 결과: {result}")
return result
return wrapper
@log_function_call
def add(a, b):
return a + b
result = add(3, 5)
print(f"최종 결과: {result}")
이 코드의 출력은 다음과 같다.
add 함수 호출. 인자: (3, 5), {}
add 함수 반환. 결과: 8
최종 결과: 8
여기서 *args
와 **kwargs
를 사용하여 어떤 인자라도 받을 수 있는 범용적인 래퍼 함수를 만들었다.
데코레이터에 인자 전달하기
때로는 데코레이터 자체에 인자를 전달하고 싶을 때가 있다. 이를 위해서는 추가적인 레벨의 중첩 함수가 필요하다.
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def greet(name):
print(f"안녕하세요, {name}님!")
greet("영희")
이 코드는 "안녕하세요, 영희님!"
을 세 번 출력한다.
클래스 데코레이터
함수뿐만 아니라 클래스에도 데코레이터를 적용할 수 있다.
def singleton(cls):
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class DatabaseConnection:
def __init__(self):
print("데이터베이스 연결 생성")
# 여러 번 인스턴스를 생성해도 항상 같은 인스턴스가 반환된다
conn1 = DatabaseConnection()
conn2 = DatabaseConnection()
print(conn1 is conn2) # 출력: True
이 예제에서 singleton 데코레이터는 클래스의 인스턴스가 하나만 생성되도록 보장한다.
데코레이터의 실제 사용 사례
데코레이터는 다양한 실제 상황에서 유용하게 사용된다. 몇 가지 예를 살펴보자.
1. 실행 시간 측정
함수의 실행 시간을 측정하는 데코레이터를 만들 수 있다.
import time
def measure_time(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:.5f}초")
return result
return wrapper
@measure_time
def slow_function():
time.sleep(2)
print("함수 실행 완료")
slow_function()
이 코드는 함수의 실행 시간을 측정하고 출력한다.
2. 캐싱 (메모이제이션)
계산 비용이 큰 함수의 결과를 캐싱하는 데코레이터를 만들 수 있다.
def memoize(func):
cache = {}
def wrapper(*args):
if args in cache:
return cache[args]
result = func(*args)
cache[args] = result
return result
return wrapper
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(100)) # 매우 빠르게 계산된다
이 데코레이터는 함수의 결과를 캐시에 저장하여 동일한 인자로 함수가 다시 호출될 때 계산을 반복하지 않고 저장된 결과를 반환한다.
3. 권한 검사
웹 애플리케이션에서 특정 뷰 함수에 대한 접근 권한을 검사하는 데코레이터를 만들 수 있다.
def require_auth(func):
def wrapper(request, *args, **kwargs):
if not request.user.is_authenticated:
return redirect('login')
return func(request, *args, **kwargs)
return wrapper
@require_auth
def protected_view(request):
return render(request, 'protected_page.html')
이 데코레이터는 사용자가 인증되지 않은 경우 로그인 페이지로 리다이렉트한다.
데코레이터의 고급 기능
1. functools.wraps 사용하기
데코레이터를 사용할 때 주의해야 할 점 중 하나는 원래 함수의 메타데이터(이름, 문서열 등)가 손실될 수 있다는 것이다. 이를 방지하기 위해 functools.wraps를 사용할 수 있다.
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
"""래퍼 함수"""
print("함수 실행 전")
result = func(*args, **kwargs)
print("함수 실행 후")
return result
return wrapper
@my_decorator
def greet(name):
"""인사를 하는 함수"""
print(f"안녕하세요, {name}님!")
print(greet.__name__) # 출력: greet
print(greet.__doc__) # 출력: 인사를 하는 함수
@wraps(func)
를 사용함으로써 원래 함수의 메타데이터가 보존된다.
2. 클래스를 이용한 데코레이터
함수 대신 클래스를 사용하여 데코레이터를 만들 수 있다. 이 방법은 데코레이터가 상태를 유지해야 할 때 유용하다.
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("안녕하세요!")
say_hello()
say_hello()
say_hello()
이 예제에서 CountCalls
클래스는 데코레이터로 사용되며, 함수가 호출된 횟수를 추적한다.
데코레이터의 장단점
장점
1. 코드 재사용성
공통 기능을 데코레이터로 분리하여 여러 함수에 쉽게 적용할 수 있다.
2. 관심사의 분리
핵심 로직과 부가 기능을 분리하여 코드의 구조를 개선한다.
3. 가독성 향상
복잡한 로직을 간단한 '@' 구문으로 표현할 수 있다.
4. 유지보수성
공통 기능의 변경이 필요할 때 데코레이터만 수정하면 되므로 유지보수가 용이하다.
단점
1. 디버깅의 어려움
데코레이터가 여러 겹 중첩되면 디버깅이 복잡해질 수 있다.
2. 성능 오버헤드
데코레이터는 추가적인 함수 호출을 발생시키므로 미세한 성능 저하가 있을 수 있다.
3. 이해의 어려움
데코레이터의 동작 원리를 이해하지 못하면 코드를 해석하기 어려울 수 있다.
'프로그래밍 > 파이썬' 카테고리의 다른 글
파이썬 - 메모리 관리와 가비지 컬렉션 (0) | 2024.12.01 |
---|---|
파이썬 - 네임드 튜플(Named Tuple)과 데이터 클래스(Data Class) 비교 (0) | 2024.12.01 |
파이썬 - 람다 함수와 함수형 프로그래밍 기초 (0) | 2024.12.01 |
파이썬 - 이터레이터(Iterator)와 이터러블(Iterable) (0) | 2024.11.30 |
파이썬 - 정규표현식 (0) | 2024.11.30 |