데코레이터 (Decorators)
데코레이터 (Decorators)¶
1. 데코레이터란?¶
데코레이터는 함수나 클래스를 수정하지 않고 기능을 추가하는 패턴입니다. @ 문법을 사용하여 적용합니다.
@decorator
def function():
pass
# 위 코드는 아래와 동일
def function():
pass
function = decorator(function)
데코레이터의 구조¶
┌─────────────────────────────────────────┐
│ 데코레이터 │
│ ┌─────────────────────────────────┐ │
│ │ wrapper 함수 │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ 원본 함수 호출 │ │ │
│ │ └─────────────────────────┘ │ │
│ │ + 추가 기능 (전/후) │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
2. 기본 데코레이터¶
가장 단순한 형태¶
def my_decorator(func):
def wrapper():
print("함수 실행 전")
func()
print("함수 실행 후")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
출력:
함수 실행 전
Hello!
함수 실행 후
인자를 받는 함수에 적용¶
def my_decorator(func):
def wrapper(*args, **kwargs):
print(f"인자: {args}, {kwargs}")
result = func(*args, **kwargs)
print(f"결과: {result}")
return result
return wrapper
@my_decorator
def add(a, b):
return a + b
add(3, 5)
출력:
인자: (3, 5), {}
결과: 8
3. @wraps로 메타데이터 보존¶
데코레이터를 적용하면 원본 함수의 메타데이터(이름, docstring 등)가 사라집니다.
def my_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet(name):
"""인사 함수"""
return f"Hello, {name}"
print(greet.__name__) # wrapper (원본 이름이 아님!)
print(greet.__doc__) # None (docstring 손실!)
functools.wraps 사용¶
from functools import wraps
def my_decorator(func):
@wraps(func) # 메타데이터 보존
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet(name):
"""인사 함수"""
return f"Hello, {name}"
print(greet.__name__) # greet (보존됨!)
print(greet.__doc__) # 인사 함수 (보존됨!)
4. 인자를 받는 데코레이터¶
데코레이터 자체에 인자를 전달하려면 한 단계 더 감싸야 합니다.
from functools import wraps
def repeat(times):
"""함수를 n번 반복 실행하는 데코레이터"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def say_hi():
print("Hi!")
say_hi()
출력:
Hi!
Hi!
Hi!
실용 예제: 권한 검사¶
from functools import wraps
def require_role(role):
"""특정 역할이 필요한 함수에 적용"""
def decorator(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if user.get("role") != role:
raise PermissionError(f"'{role}' 권한이 필요합니다")
return func(user, *args, **kwargs)
return wrapper
return decorator
@require_role("admin")
def delete_user(user, target_id):
print(f"사용자 {target_id} 삭제됨")
admin = {"name": "Alice", "role": "admin"}
guest = {"name": "Bob", "role": "guest"}
delete_user(admin, 123) # OK
# delete_user(guest, 123) # PermissionError!
5. 클래스 기반 데코레이터¶
__call__ 메서드를 구현하면 클래스를 데코레이터로 사용할 수 있습니다.
class CountCalls:
"""함수 호출 횟수를 추적하는 데코레이터"""
def __init__(self, func):
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"{self.func.__name__} 호출 횟수: {self.count}")
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print("Hello!")
say_hello() # 호출 횟수: 1
say_hello() # 호출 횟수: 2
say_hello() # 호출 횟수: 3
인자를 받는 클래스 데코레이터¶
class Retry:
"""실패 시 재시도하는 데코레이터"""
def __init__(self, max_attempts=3):
self.max_attempts = max_attempts
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, self.max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"시도 {attempt} 실패: {e}")
if attempt == self.max_attempts:
raise
return wrapper
@Retry(max_attempts=3)
def unstable_function():
import random
if random.random() < 0.7:
raise ValueError("랜덤 실패!")
return "성공!"
result = unstable_function()
6. 내장 데코레이터¶
@property¶
getter/setter/deleter를 정의합니다.
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""반지름 (읽기)"""
return self._radius
@radius.setter
def radius(self, value):
"""반지름 (쓰기)"""
if value < 0:
raise ValueError("반지름은 양수여야 합니다")
self._radius = value
@property
def area(self):
"""면적 (계산된 속성)"""
return 3.14159 * self._radius ** 2
circle = Circle(5)
print(circle.radius) # 5
print(circle.area) # 78.53975
circle.radius = 10
print(circle.area) # 314.159
@staticmethod¶
인스턴스나 클래스에 접근하지 않는 메서드입니다.
class Math:
@staticmethod
def add(a, b):
return a + b
@staticmethod
def multiply(a, b):
return a * b
# 인스턴스 없이 호출
print(Math.add(3, 5)) # 8
print(Math.multiply(3, 5)) # 15
@classmethod¶
클래스를 첫 번째 인자로 받는 메서드입니다.
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
@classmethod
def from_string(cls, date_string):
"""문자열에서 Date 생성"""
year, month, day = map(int, date_string.split('-'))
return cls(year, month, day)
@classmethod
def today(cls):
"""오늘 날짜로 Date 생성"""
import datetime
t = datetime.date.today()
return cls(t.year, t.month, t.day)
def __repr__(self):
return f"Date({self.year}, {self.month}, {self.day})"
date1 = Date.from_string("2024-01-23")
date2 = Date.today()
print(date1) # Date(2024, 1, 23)
7. 실용적 데코레이터 패턴¶
타이밍 측정¶
import time
from functools import wraps
def timer(func):
"""함수 실행 시간 측정"""
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.__name__}: {end - start:.4f}초")
return result
return wrapper
@timer
def slow_function():
time.sleep(1)
return "완료"
slow_function() # slow_function: 1.0012초
로깅¶
import logging
from functools import wraps
logging.basicConfig(level=logging.INFO)
def log_calls(func):
"""함수 호출을 로깅"""
@wraps(func)
def wrapper(*args, **kwargs):
logging.info(f"호출: {func.__name__}({args}, {kwargs})")
result = func(*args, **kwargs)
logging.info(f"반환: {result}")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
add(3, 5)
캐싱 (메모이제이션)¶
from functools import wraps
def memoize(func):
"""결과를 캐싱하는 데코레이터"""
cache = {}
@wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(100)) # 캐싱 없이는 매우 느림
참고: Python 내장 functools.lru_cache를 사용하면 더 편리합니다.
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
입력 검증¶
from functools import wraps
def validate_types(**expected_types):
"""인자 타입을 검증하는 데코레이터"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 키워드 인자 검증
for name, expected in expected_types.items():
if name in kwargs:
if not isinstance(kwargs[name], expected):
raise TypeError(
f"{name}은(는) {expected.__name__} 타입이어야 합니다"
)
return func(*args, **kwargs)
return wrapper
return decorator
@validate_types(name=str, age=int)
def create_user(name, age):
return {"name": name, "age": age}
create_user(name="Alice", age=30) # OK
# create_user(name="Alice", age="30") # TypeError!
8. 데코레이터 체이닝¶
여러 데코레이터를 동시에 적용할 수 있습니다. 적용 순서는 아래에서 위입니다.
@decorator1
@decorator2
@decorator3
def func():
pass
# 위 코드는 아래와 동일
func = decorator1(decorator2(decorator3(func)))
예제¶
from functools import wraps
def bold(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def italic(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
@bold
@italic
def greet(name):
return f"Hello, {name}"
print(greet("World")) # <b><i>Hello, World</i></b>
9. 클래스 데코레이터¶
클래스 전체에 데코레이터를 적용할 수 있습니다.
def singleton(cls):
"""싱글톤 패턴 데코레이터"""
instances = {}
@wraps(cls)
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class Database:
def __init__(self):
print("데이터베이스 연결 생성")
db1 = Database() # 데이터베이스 연결 생성
db2 = Database() # (출력 없음 - 같은 인스턴스)
print(db1 is db2) # True
dataclass (내장 클래스 데코레이터)¶
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
def distance(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
p = Point(3, 4)
print(p) # Point(x=3, y=4)
print(p.distance()) # 5.0
print(p == Point(3, 4)) # True
10. 요약¶
| 패턴 | 설명 | 예시 |
|---|---|---|
| 기본 데코레이터 | 함수를 감싸서 기능 추가 | @timer |
| 인자 있는 데코레이터 | 데코레이터에 설정 전달 | @repeat(3) |
| 클래스 기반 데코레이터 | 상태 유지가 필요할 때 | @CountCalls |
| @wraps | 메타데이터 보존 | @wraps(func) |
| @property | getter/setter 정의 | @property |
| @staticmethod | 정적 메서드 | @staticmethod |
| @classmethod | 클래스 메서드 | @classmethod |
| @lru_cache | 결과 캐싱 | @lru_cache(128) |
11. 연습 문제¶
연습 1: 실행 시간 제한¶
함수가 지정된 시간 내에 완료되지 않으면 TimeoutError를 발생시키는 데코레이터를 작성하세요.
연습 2: 결과 로깅¶
함수의 입력과 출력을 파일에 로깅하는 데코레이터를 작성하세요.
연습 3: 디버그 모드¶
DEBUG 플래그가 True일 때만 디버그 정보를 출력하는 데코레이터를 작성하세요.
다음 단계¶
03_Context_Managers.md에서 with문과 리소스 관리를 배워봅시다!