Decorator란?
decorator는 함수나 class에 추가 동작을 붙이는 Python 문법이다.
예를 들어 어떤 함수가 실행될 때마다 log를 찍고 싶다고 하자.
함수 내부마다 print()를 직접 넣을 수도 있지만, decorator를 사용하면 공통 동작을 함수 밖에서 감쌀 수 있다.
def log(func):
def wrapper():
print("before")
func()
print("after")
return wrapper
이제 함수를 decorator로 감싼다.
@log
def hello():
print("hello")
호출하면 다음 순서로 실행된다.
hello()
before
hello
after
@ 문법의 의미
Decorator 문법은 사실 함수 재할당을 짧게 쓴 것이다.
다음 코드는
@log
def hello():
print("hello")
아래 코드와 거의 같다.
def hello():
print("hello")
hello = log(hello)
즉 @log는 hello 함수를 log() 함수에 넣고, 그 결과를 다시 hello라는 이름에 넣는 문법이다.
원래 함수 -> decorator 함수 -> wrapper 함수
그래서 decorator는 함수를 인자로 받고, 보통 새로운 함수를 반환한다.
함수도 객체다
Decorator를 이해하려면 Python에서 함수도 객체라는 점을 알아야 한다.
함수를 변수에 담을 수 있다.
def hello():
print("hello")
func = hello
func()
함수를 다른 함수의 인자로 넘길 수도 있다.
def run(func):
func()
run(hello)
함수 안에서 함수를 만들고 반환할 수도 있다.
def outer():
def inner():
print("inner")
return inner
func = outer()
func()
Decorator는 이 성질을 이용한다.
인자가 있는 함수 감싸기
실제 함수는 대부분 인자를 가진다. 이때 wrapper도 인자를 받아야 한다.
def log(func):
def wrapper(name):
print("before")
result = func(name)
print("after")
return result
return wrapper
사용 예시는 다음과 같다.
@log
def greet(name: str) -> str:
return f"hello, {name}"
message = greet("Alice")
print(message)
하지만 이 방식은 인자 개수가 바뀌면 wrapper도 수정해야 한다.
그래서 보통 *args, **kwargs를 사용한다.
def log(func):
def wrapper(*args, **kwargs):
print("before")
result = func(*args, **kwargs)
print("after")
return result
return wrapper
이렇게 하면 다양한 함수에 같은 decorator를 적용할 수 있다.
functools.wraps
Decorator를 사용할 때는 functools.wraps를 함께 쓰는 것이 좋다.
그냥 wrapper를 반환하면 원래 함수의 이름과 docstring 같은 metadata가 사라진다.
def log(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@log
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
print(add.__name__)
출력은 원래 함수 이름인 add가 아니라 wrapper가 된다.
wrapper
wraps를 사용하면 원래 함수의 metadata를 유지할 수 있다.
from functools import wraps
def log(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
이제 add.__name__은 add로 유지된다.
add
Decorator를 직접 만들 때는 특별한 이유가 없다면 @wraps(func)를 붙이는 것이 좋다.
실행 시간 측정 decorator
Decorator는 실행 시간 측정 같은 공통 기능에 잘 맞는다.
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:.6f}s")
return result
return wrapper
사용 예시는 다음과 같다.
@timer
def work():
total = 0
for i in range(1_000_000):
total += i
return total
work()
함수 내부 로직을 바꾸지 않고 실행 시간 측정 기능을 붙일 수 있다.
인증 확인 decorator
웹 서버에서는 인증이나 권한 확인에도 decorator를 많이 사용한다.
예를 들어 단순한 예시는 다음과 같다.
from functools import wraps
def login_required(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if user is None:
raise PermissionError("login required")
return func(user, *args, **kwargs)
return wrapper
사용 예시는 다음과 같다.
@login_required
def get_profile(user):
return {"name": user["name"]}
user = {"name": "Alice"}
profile = get_profile(user)
실제 Flask, FastAPI, Django 같은 framework에서도 비슷한 패턴을 자주 볼 수 있다.
인자를 받는 decorator
Decorator 자체에 인자를 넘기고 싶을 때가 있다.
예를 들어 권한 이름을 decorator에 넘긴다고 하자.
@require_role("admin")
def delete_user(user_id: int):
...
이런 decorator는 함수가 한 겹 더 필요하다.
from functools import wraps
def require_role(role: str):
def decorator(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if user.get("role") != role:
raise PermissionError(f"{role} role required")
return func(user, *args, **kwargs)
return wrapper
return decorator
호출 구조는 다음과 같다.
require_role("admin") -> decorator 반환
decorator(delete_user) -> wrapper 반환
delete_user = wrapper
즉 인자를 받는 decorator는 다음 형태를 가진다.
def decorator_factory(option):
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
return decorator
여러 decorator를 함께 사용하기
하나의 함수에 여러 decorator를 붙일 수도 있다.
@decorator_a
@decorator_b
def func():
pass
이 코드는 다음과 같다.
def func():
pass
func = decorator_a(decorator_b(func))
가까운 decorator인 decorator_b가 먼저 적용되고, 그 결과에 decorator_a가 적용된다.
실행 순서를 예로 보면 다음과 같다.
from functools import wraps
def a(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("a before")
result = func(*args, **kwargs)
print("a after")
return result
return wrapper
def b(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("b before")
result = func(*args, **kwargs)
print("b after")
return result
return wrapper
@a
@b
def hello():
print("hello")
hello()
출력은 다음과 같다.
a before
b before
hello
b after
a after
class method 관련 decorator
Python class에서 자주 보는 decorator도 있다.
| decorator | 의미 |
|---|---|
@staticmethod | instance 없이 호출 가능한 method |
@classmethod | class 자체를 첫 번째 인자로 받는 method |
@property | method를 attribute처럼 접근 |
@staticmethod
@staticmethod는 self나 cls를 받지 않는다.
class Math:
@staticmethod
def add(a: int, b: int) -> int:
return a + b
print(Math.add(1, 2))
class 안에 넣었지만, 실제로는 독립 함수에 가깝다.
@classmethod
@classmethod는 첫 번째 인자로 class를 받는다.
보통 대체 생성자에 사용한다.
class User:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
@classmethod
def from_dict(cls, data: dict):
return cls(name=data["name"], age=data["age"])
user = User.from_dict({"name": "Alice", "age": 20})
여기서 cls는 User class를 의미한다.
@property
@property는 method를 attribute처럼 접근하게 만든다.
class Rectangle:
def __init__(self, width: int, height: int):
self.width = width
self.height = height
@property
def area(self) -> int:
return self.width * self.height
rect = Rectangle(3, 4)
print(rect.area)
rect.area()가 아니라 rect.area로 접근한다.
class decorator
Decorator는 함수뿐 아니라 class에도 사용할 수 있다.
def add_repr(cls):
def __repr__(self):
return f"{cls.__name__}({self.__dict__})"
cls.__repr__ = __repr__
return cls
@add_repr
class User:
def __init__(self, name: str):
self.name = name
user = User("Alice")
print(user)
class decorator는 class를 인자로 받고, 수정된 class나 새로운 class를 반환한다.
@dataclass도 class decorator의 대표적인 예시이다.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
decorator를 쓸 때 주의할 점
Decorator는 편리하지만 남용하면 코드 흐름을 숨긴다.
주의할 점은 다음과 같다.
| 주의점 | 설명 |
|---|---|
| metadata 손실 | functools.wraps를 사용한다 |
| 실행 흐름 숨김 | decorator 안에서 너무 많은 일을 하지 않는다 |
| debugging 어려움 | wrapper가 여러 겹이면 stack trace가 복잡해진다 |
| 인자 처리 실수 | *args, **kwargs를 적절히 사용한다 |
| 순서 의존성 | 여러 decorator를 쓸 때 적용 순서를 확인한다 |
Decorator는 logging, timing, caching, permission check처럼 공통 관심사를 분리할 때 효과적이다. 하지만 핵심 비즈니스 로직을 decorator 안에 숨기면 코드를 읽기 어려워진다.
정리
Python decorator는 함수나 class에 추가 동작을 붙이는 문법이다.
핵심은 다음과 같다.
@decorator는func = decorator(func)와 같은 의미이다.- decorator는 함수를 인자로 받고 함수를 반환한다.
- 일반적으로 wrapper 함수 안에서 원래 함수를 호출한다.
- 인자를 받는 함수에는
*args,**kwargs를 사용한다. - 직접 decorator를 만들 때는
functools.wraps를 사용하는 것이 좋다. - decorator 자체에 인자가 필요하면 함수를 한 겹 더 감싼다.
@staticmethod,@classmethod,@property,@dataclass도 decorator이다.
Decorator는 반복되는 공통 동작을 함수 밖으로 분리할 때 유용하다. 다만 코드 흐름을 숨길 수 있으므로, 사용 목적이 명확할 때 적용하는 것이 좋다.