파이썬(Python) 최강의 기술, 데코레이터(Decorator) 완전 정복
파이썬(Python) 데코레이터 상세 설명
1. 데코레이터란 무엇일까요?
파이썬(Python)에서 데코레이터(decorator)는 본질적으로 파이썬(Python) 함수입니다.
데코레이터(decorator)는 다른 함수의 원본 코드를 수정하지 않고도 추가 기능을 더할 수 있는 독특한 능력을 가지고 있습니다.
데코레이터(decorator)의 반환 값 또한 함수 객체입니다.
간단히 말해, 다른 함수를 반환하도록 특별히 설계된 함수라고 할 수 있습니다.
데코레이터(decorator)는 관점 지향(aspect-oriented) 요구 사항이 있는 많은 시나리오에서 중요한 역할을 합니다.
예를 들면 다음과 같은데요.
로그 삽입: 함수의 실행 과정과 관련 정보를 기록하는 것을 용이하게 하여 디버깅 및 시스템 모니터링에 도움이 됩니다.
성능 테스트: 함수의 실행 시간을 계산하여 성능을 평가할 수 있습니다.
트랜잭션 처리: 일련의 작업이 모두 성공하거나 모두 실패하도록 보장하여 데이터 일관성과 무결성을 보장합니다.
캐싱: 계산 비용이 높은 함수의 경우 계산 결과를 캐시합니다.
다음에 동일한 입력이 발생하면 캐시된 값을 직접 반환하여 효율성을 향상시킵니다.
권한 검증: 함수를 실행하기 전에 사용자에게 해당 권한이 있는지 확인하여 시스템의 보안을 보장합니다.
데코레이터(decorator)는 이러한 문제를 해결하기 위한 훌륭한 설계 솔루션을 제공합니다.
데코레이터(decorator)를 사용하면 함수의 핵심 기능과 관련이 없지만 반복적으로 나타나는 많은 코드를 추출하여 높은 수준의 코드 재사용을 달성할 수 있습니다.
요약하자면, 데코레이터(decorator)의 핵심 기능은 기존 객체에 추가 기능을 추가하여 코드 구조를 더 명확하게 만들고 기능을 더욱 풍부하고 유연하게 만드는 것입니다.
2. 왜 데코레이터가 필요할까요?
(1) 간단한 예시
먼저 간단한 함수를 생각해 보겠습니다.
def foo():
print('i am foo')
이 함수는 단순히 i am foo
라는 문자열을 출력합니다.
(2) 요구 사항 추가
이제 함수의 실행 로그를 기록해야 하는 새로운 요구 사항이 생겼습니다.
그래서 코드에 로그 관련 코드를 추가합니다.
def foo():
print('i am foo')
print("foo is running")
이 시점에서 foo
함수는 원래 기능 외에 로그를 출력하는 기능이 추가되었습니다.
(3) 더 많은 함수에 대한 요구 사항
100개의 함수 모두에 이러한 로그 기록 요구 사항을 추가해야 하고, 앞으로 이 100개 함수에 실행 전 로그를 출력하는 요구 사항을 추가해야 할 수도 있다고 가정해 보겠습니다.
함수 코드를 하나씩 수정하면 많은 중복 코드가 생성되어 분명 좋은 해결책이 아닙니다.
중복 코드 작성을 줄이기 위해 로그 관련 작업을 특별히 처리하는 함수를 다시 정의할 수 있습니다.
로그 처리가 완료된 후 실제 비즈니스 코드가 실행됩니다.
예는 다음과 같습니다.
# use_logging 함수는 func의 이름을 출력하고 func를 실행합니다.
def use_logging(func):
print("%s is running" % func.__name__) # func의 이름으로 실행 중임을 알립니다.
func() # 전달받은 함수를 실행합니다.
# bar 함수는 'i am bar'를 출력합니다.
def bar():
print('i am bar')
use_logging(bar) # use_logging 함수에 bar 함수를 전달하여 실행합니다.
실행 결과는 다음과 같습니다.
bar is running
i am bar
이 예제에서 use_logging
함수는 데코레이터(decorator)입니다.
함수 내부에 실제 비즈니스 메서드를 실행하는 func
를 래핑합니다.
형식적으로는 bar
함수가 use_logging
에 의해 장식된 것처럼 보입니다.
함수가 시작되고 끝날 때의 로그 기록 작업은 관점(aspect)이라고 하며, 이러한 프로그래밍 방식을 관점 지향 프로그래밍(Aspect-Oriented Programming)이라고 합니다.
이 use_logging
함수를 통해 함수에 로깅 기능을 성공적으로 추가했습니다.
앞으로 얼마나 많은 함수에 로깅 기능을 추가해야 하든, 또는 로그 형식을 수정해야 하든, use_logging
함수만 수정하고 use_logging(장식된 함수)
를 호출하면 원하는 효과를 얻을 수 있습니다.
예를 들면 다음과 같습니다.
# use_logging 함수는 func의 이름을 출력하고 func 자체를 반환합니다.
def use_logging(func):
print("%s is running" % func.__name__) # func의 이름으로 실행 중임을 알립니다.
return func # 전달받은 함수를 그대로 반환합니다.
@use_logging # @use_logging 데코레이터를 bar 함수에 적용합니다.
def bar():
print('i am bar')
bar() # bar 함수를 호출합니다.
3. 기본 데코레이터 소개
(1) 데코레이터 문법 설탕(Syntax Sugar)
파이썬(Python)은 데코레이터(decorator)를 위한 문법 설탕으로 @
기호를 제공하여 장식 함수를 더 편리하게 적용할 수 있도록 합니다.
그러나 문법 설탕을 사용하려면 장식 함수가 함수 객체를 반환해야 한다는 요구 사항이 있습니다.
따라서 일반적으로 장식할 함수를 내부 함수로 래핑하고 이 내부 함수를 반환합니다.
다음 코드를 예로 들어 보겠습니다.
데코레이터(decorator) use_logging
은 이 함수를 먼저 실행한 다음 장식된 함수 bar
를 반환하는 것과 같습니다.
따라서 bar()
가 호출될 때 실제로는 두 함수를 실행하는 것과 같으며, 이는 use_logging(bar)()
를 직접 호출하는 것과 같습니다.
# use_logging 데코레이터 함수입니다.
def use_logging(func):
# 내부 래퍼 함수 _deco를 정의합니다.
def _deco():
print("%s is running" % func.__name__) # 원본 함수의 이름을 사용하여 실행 중임을 알립니다.
func() # 원본 함수를 실행합니다.
return _deco # 래퍼 함수를 반환합니다.
@use_logging # @use_logging 데코레이터를 bar 함수에 적용합니다.
def bar():
print('i am bar')
bar() # bar 함수를 호출합니다. (실제로는 _deco 함수가 호출됩니다.)
(2) 매개변수가 있는 함수 장식
함수가 두 개의 매개변수를 받아 계산을 수행해야 하는 경우, 전달된 두 매개변수 a
와 b
를 받도록 내부 함수를 해당하게 변경해야 합니다.
이때 bar(1, 2)
를 호출하는 것은 use_logging(bar)(1, 2)
를 호출하는 것과 같습니다.
샘플 코드는 다음과 같습니다.
# use_logging 데코레이터 함수입니다. (매개변수 받는 버전)
def use_logging(func):
# 내부 래퍼 함수 _deco는 매개변수 a와 b를 받습니다.
def _deco(a, b):
print("%s is running" % func.__name__) # 원본 함수의 이름을 사용하여 실행 중임을 알립니다.
func(a, b) # 원본 함수에 매개변수를 전달하여 실행합니다.
return _deco # 래퍼 함수를 반환합니다.
@use_logging # @use_logging 데코레이터를 bar 함수에 적용합니다.
def bar(a, b):
print('i am bar:%s' % (a + b)) # 전달받은 두 매개변수의 합을 출력합니다.
bar(1, 2) # bar 함수를 호출합니다. (실제로는 _deco(1, 2)가 호출됩니다.)
그러나 실제 응용 프로그램에서는 장식하는 함수의 매개변수 수와 유형이 다를 수 있습니다.
매번 다른 매개변수 상황에 맞게 데코레이터(decorator)를 수정하는 것은 분명히 과학적이지 않습니다.
이 매개변수 문제를 해결하기 위해 파이썬(Python)의 가변 길이 매개변수 *args
와 **kwargs
를 사용할 수 있습니다.
(3) 함수 매개변수 개수가 불확실한 경우
다음은 매개변수가 없는 데코레이터(decorator) 버전이며, 이 형식은 매개변수가 없는 함수를 장식하는 데 적합합니다.*args
와 **kwargs
를 사용하면 데코레이터(decorator)가 이미 다양한 길이와 유형의 매개변수에 적응할 수 있습니다.
즉, 이 버전의 데코레이터(decorator)는 모든 유형의 매개변수 없는 함수를 장식할 수 있습니다.
샘플 코드는 다음과 같습니다.
# use_logging 데코레이터 함수입니다. (가변 인자 사용 버전)
def use_logging(func):
# 내부 래퍼 함수 _deco는 *args와 **kwargs를 사용하여 모든 종류의 인자를 받을 수 있습니다.
def _deco(*args, **kwargs):
print("%s is running" % func.__name__) # 원본 함수의 이름을 사용하여 실행 중임을 알립니다.
func(*args, **kwargs) # 원본 함수에 인자를 그대로 전달하여 실행합니다.
return _deco # 래퍼 함수를 반환합니다.
@use_logging # bar 함수에 데코레이터를 적용합니다.
def bar(a, b):
print('i am bar:%s' % (a + b)) # 두 인자의 합을 출력합니다.
@use_logging # foo 함수에 데코레이터를 적용합니다.
def foo(a, b, c):
print('i am bar:%s' % (a + b + c)) # 세 인자의 합을 출력합니다.
bar(1, 2) # bar 함수를 호출합니다.
foo(1, 2, 3) # foo 함수를 호출합니다.
(4) 매개변수가 있는 데코레이터
경우에 따라 데코레이터(decorator)가 매개변수를 받도록 해야 합니다.
이를 위해서는 데코레이터(decorator)를 반환하는 고차 함수를 작성해야 하며, 구현하기가 비교적 더 복잡합니다.
예를 들면 다음과 같습니다.
#! /usr/bin/env python
# use_logging은 데코레이터를 반환하는 함수입니다. level 매개변수를 받습니다.
def use_logging(level):
# 실제 데코레이터 함수 _deco를 정의합니다.
def _deco(func):
# 내부 래퍼 함수 __deco를 정의합니다.
def __deco(*args, **kwargs):
if level == "warn": # level이 "warn"이면 로그를 출력합니다.
print "%s is running" % func.__name__
return func(*args, **kwargs) # 원본 함수를 실행하고 결과를 반환합니다.
return __deco # 내부 래퍼 함수를 반환합니다.
return _deco # 데코레이터 함수를 반환합니다.
@use_logging(level="warn") # 매개변수가 있는 데코레이터를 bar 함수에 적용합니다.
def bar(a, b):
print('i am bar:%s' % (a + b))
bar(1, 3) # bar 함수를 호출합니다.
# 위 코드는 use_logging(level="warn")(bar)(1, 3)과 동일합니다.
(5) functools.wraps
데코레이터(decorator)를 사용하면 코드를 크게 재사용할 수 있지만, 원본 함수의 메타 정보가 손실된다는 단점이 있습니다.
예를 들어 함수의 docstring
, __name__
, 매개변수 목록과 같은 정보입니다.
먼저 다음 예를 살펴보겠습니다.
# use_logging 데코레이터 함수입니다.
def use_logging(func):
# 내부 래퍼 함수 _deco를 정의합니다.
def _deco(*args, **kwargs):
print("%s is running" % func.__name__) # 이때 func.__name__은 bar입니다.
func(*args, **kwargs)
return _deco # _deco 함수가 반환됩니다.
@use_logging # bar 함수에 데코레이터를 적용합니다. bar는 이제 _deco를 가리킵니다.
def bar():
print('i am bar')
print(bar.__name__) # bar는 _deco를 가리키므로 _deco의 이름이 출력됩니다.
bar()
# 출력 결과는 다음과 같습니다:
# bar is running
# i am bar
# _deco
함수 이름이 원래 bar
가 아닌 _deco
가 된 것을 볼 수 있습니다.
리플렉션 기능을 사용할 때 이러한 상황은 문제를 일으킬 것입니다.
이 문제를 해결하기 위해 functools.wraps
를 도입할 수 있습니다.functools.wraps
를 사용하는 샘플 코드는 다음과 같습니다.
import functools # functools 모듈을 가져옵니다.
# use_logging 데코레이터 함수입니다.
def use_logging(func):
@functools.wraps(func) # _deco 함수가 func 함수의 메타 정보를 유지하도록 합니다.
def _deco(*args, **kwargs):
print("%s is running" % func.__name__) # 이제 func.__name__은 여전히 'bar'입니다.
func(*args, **kwargs)
return _deco
@use_logging # bar 함수에 데코레이터를 적용합니다.
def bar():
print('i am bar')
print(bar.__name__) # functools.wraps 덕분에 bar의 이름이 유지됩니다.
bar()
# 출력 결과는 다음과 같습니다:
# bar is running
# i am bar
# bar
위 결과에서 볼 수 있듯이 functools.wraps
를 사용한 후 예상 결과를 얻었고 원본 함수의 이름을 성공적으로 유지했습니다.
(6) 매개변수가 있거나 없는 데코레이터에 대한 적응성 달성
import functools # functools 모듈을 가져옵니다.
# use_logging 데코레이터 함수입니다.
# 인자(arg)를 받아 함수인지 아닌지 판단하여 분기합니다.
def use_logging(arg):
if callable(arg): # 전달된 매개변수가 함수인지 확인합니다. 매개변수 없는 데코레이터는 이 분기를 호출합니다.
@functools.wraps(arg) # arg(원본 함수)의 메타 정보를 유지합니다.
def _deco(*args, **kwargs):
print("%s is running" % arg.__name__)
arg(*args, **kwargs)
return _deco
else: # 매개변수 있는 데코레이터는 이 분기를 호출합니다. arg는 데코레이터의 매개변수가 됩니다.
# 실제 데코레이터 함수 _deco를 반환하는 함수를 정의합니다.
def _deco(func):
@functools.wraps(func) # func(장식될 함수)의 메타 정보를 유지합니다.
def __deco(*args, **kwargs):
if arg == "warn": # 데코레이터 매개변수 arg가 "warn"이면 로그를 출력합니다.
print "warn%s is running" % func.__name__
return func(*args, **kwargs)
return __deco
return _deco
@use_logging("warn") # 매개변수 있는 데코레이터 사용 예시
# @use_logging # 매개변수 없는 데코레이터 사용 예시 (주석 처리)
def bar():
print('i am bar')
print(bar.__name__)
bar()
4. 클래스 데코레이터
클래스 데코레이터(decorator)를 사용하면 매개변수가 있는 데코레이터(decorator)의 효과를 얻을 수 있을 뿐만 아니라 구현 방법도 더 우아하고 간결합니다.
동시에 상속을 통해 유연하게 확장할 수 있습니다.
(1) 클래스 데코레이터
# loging 클래스는 데코레이터 역할을 합니다.
class loging(object):
# 초기화 메서드, level 매개변수를 받아 인스턴스 변수로 저장합니다.
def __init__(self, level="warn"):
self.level = level
# __call__ 메서드는 클래스 인스턴스가 함수처럼 호출될 때 실행됩니다.
# 데코레이터로 사용될 때 func는 장식될 함수입니다.
def __call__(self, func):
@functools.wraps(func) # func의 메타 정보를 유지합니다.
def _deco(*args, **kwargs):
if self.level == "warn": # level이 "warn"이면 notify 메서드를 호출합니다.
self.notify(func)
return func(*args, **kwargs) # 원본 함수를 실행하고 결과를 반환합니다.
return _deco # 내부 래퍼 함수를 반환합니다.
# notify 메서드는 로그를 출력합니다.
def notify(self, func):
# logit은 로깅만 하고 다른 작업은 하지 않습니다.
print "%s is running" % func.__name__
@loging(level="warn") # loging 클래스의 인스턴스를 데코레이터로 사용합니다. __call__ 메서드가 실행됩니다.
def bar(a, b):
print('i am bar:%s' % (a + b))
bar(1, 3) # bar 함수를 호출합니다.
(2) 클래스 데코레이터 상속 및 확장
# email_loging 클래스는 Loging 클래스를 상속받아 확장합니다.
# (Loging 클래스명이 이전 예제에서 loging으로 되어 있어 Loging으로 수정합니다.)
class Loging(object): # 이전 예제의 loging 클래스입니다.
def __init__(self, level="warn"):
self.level = level
def __call__(self, func):
@functools.wraps(func)
def _deco(*args, **kwargs):
if self.level == "warn":
self.notify(func)
return func(*args, **kwargs)
return _deco
def notify(self, func):
print "%s is running" % func.__name__
class email_loging(Loging): # Loging 클래스를 상속합니다.
'''
함수가 호출될 때 관리자에게 이메일을 보낼 수 있는 loging의 구현 버전입니다.
'''
# 초기화 메서드, email 매개변수를 추가로 받습니다.
def __init__(self, email='admin@myproject.com', *args, **kwargs):
self.email = email
super(email_loging, self).__init__(*args, **kwargs) # 부모 클래스의 초기화 메서드를 호출합니다.
# notify 메서드를 오버라이드하여 이메일 전송 기능을 추가합니다.
def notify(self, func):
# self.email로 이메일을 보냅니다.
print "%s is running" % func.__name__
print "sending email to %s" % self.email
@email_loging(level="warn") # email_loging 클래스의 인스턴스를 데코레이터로 사용합니다.
def bar(a, b):
print('i am bar:%s' % (a + b))
bar(1, 3) # bar 함수를 호출합니다.
위 코드에서 email_loging
클래스는 Loging
클래스를 상속합니다.
이 상속 관계를 통해 원본 클래스의 핵심 로직을 변경하지 않고도 함수가 호출될 때 관리자에게 이메일을 보내는 것과 같은 새로운 기능을 추가할 수 있습니다.
이는 코드 확장 및 재사용에서 클래스 데코레이터(decorator)의 장점을 충분히 보여줍니다.
'Python' 카테고리의 다른 글
SQLAlchemy 2.0 전격 해부: 왜 파이썬(Python) 최강 ORM으로 불릴까요? (0) | 2025.05.20 |
---|---|
파이썬 메타프로그래밍 마스터하기: 원하는 모든 것을 제어하는 방법 (0) | 2025.05.06 |
블룸 필터(Bloom Filter) 완벽 해부: 원리, 장단점, 파이썬(Python) 코드까지! (0) | 2025.05.06 |
ASGI 깊이 알기: 파이썬 비동기 웹 앱 통신 규약 파헤치기! (FastAPI, Uvicorn 연관성 포함) (1) | 2025.05.06 |
파이썬 리스트 정렬의 숨겨진 비밀: 팀소트(Timsort)는 왜 빠를까요? (0) | 2025.05.05 |