데코레이터는 Python에서 자주 사용되는 기능으로, 이를 적절히 활용하면 코드의 생산성과 가독성을 크게 향상시킬 수 있습니다.
데코레이터 소개
초기 문제 설정
어떤 회사에 A, B, C 세 개의 비즈니스 부서와 S라는 기반 서비스 부서가 있다고 가정해보겠습니다. 현재 S 부서는 다른 부서들이 호출할 수 있는 두 개의 함수를 제공하고 있습니다.
def f1():
print('f1 호출됨')
def f2():
print('f2 호출됨')
초기에는 이 함수들을 호출하는 데 문제가 없었지만, 회사의 사업이 성장하면서 S 부서는 함수 호출 전 권한 검증이 필요하게 되었습니다. 즉, 권한이 있는 경우에만 함수를 호출할 수 있도록 하고자 합니다. 이러한 요구사항을 어떻게 해결할 수 있을까요?
가능한 솔루션
- ABC 부서들이 함수를 호출하기 전에 직접 권한을 검증하도록 요청합니다.
- S 부서가 제공하는 모든 함수 내부에서 권한 검증을 먼저 수행한 후 실제 작업을 실행합니다.
문제점
- 첫 번째 방법은 권한 검증 로직을 호출 측에 노출시키며, 여러 부서가 존재할 경우 각 부서마다 일관된 구현을 보장하기 어렵습니다.
- 두 번째 방법은 새로운 함수가 추가될 때마다 권한 검증 로직을 반복적으로 삽입해야 하므로 코드의 유지보수성이 저하됩니다.
이러한 문제를 해결하기 위해 데코레이터를 사용할 수 있습니다.
데코레이터 예시
아래는 데코레이터를 이용한 간단한 예입니다.
def 권한_검증(func):
def wrapper():
print("...권한 검증 중...")
func()
return wrapper
@권한_검증
def f1():
print('f1 호출됨')
@권한_검증
def f2():
print('f2 호출됨')
f1()
f2()
위 코드의 출력 결과는 다음과 같습니다.
...권한 검증 중...
f1 호출됨
...권한 검증 중...
f2 호출됨
코드 분석:
권한_검증함수는 클로저 형태로 정의되어 있으며, 내부 함수wrapper가 원본 함수(func)를 호출하기 전에 권한 검증을 수행합니다.@권한_검증문법은f1 = 권한_검증(f1)과 같은 효과를 가지며,f1함수가 호출될 때마다 권한 검증이 자동으로 이루어집니다.
데코레이터 작동 원리
데코레이터는 기본적으로 클로저와 유사한 개념으로 작동합니다. 아래는 데코레이터의 실행 시점을 확인하는 예제입니다.
def 데코레이터(func):
print("...데코레이터 적용 시작...")
def wrapper():
print("...권한 검증 중...")
func()
return wrapper
@데코레이터
def test():
print('test')
test()
출력 결과:
...데코레이터 적용 시작...
...권한 검증 중...
test
위 코드에서 볼 수 있듯이, @데코레이터 문이 해석될 때 해당 함수가 즉시 데코레이터에 의해 감싸집니다.
여러 데코레이터 적용
두 개 이상의 데코레이터를 하나의 함수에 적용할 수도 있습니다.
def 굵게(func):
print("----a----")
def wrapper():
print("----1----")
return "<b>" + func() + "</b>"
return wrapper
def 기울게(func):
print("----b----")
def wrapper():
print("----2----")
return "<i>" + func() + "</i>"
return wrapper
@굵게
@기울게
def test():
print("----c----")
print("----3----")
return '안녕하세요'
print(test())
출력 결과:
----b----
----a----
----1----
----2----
----c----
----3----
<b><i>안녕하세요</i></b>
여기서는 데코레이터가 가장 안쪽부터 바깥쪽 순서로 적용되며, 호출 시에는 외곽부터 내부 순서로 실행됩니다.
파라미터를 갖는 함수 장식
파라미터를 받는 함수에도 데코레이터를 적용할 수 있습니다.
def 인사_데코레이터(func):
def wrapper(name):
print("인사말 데코레이터 호출됨")
func(name)
return wrapper
@인사_데코레이터
def hello(name):
print(f'안녕하세요 {name}')
hello('홍길동')
결과:
인사말 데코레이터 호출됨
안녕하세요 홍길동
여러 파라미터나 가변 파라미터를 처리하는 경우도 가능합니다.
def 덧셈_데코레이터(func):
def wrapper(*args, **kwargs):
print("덧셈 데코레이터 호출됨")
return func(*args, **kwargs)
return wrapper
@덧셈_데코레이터
def add(a, b):
return a + b
print(add(3, 5))
결과:
덧셈 데코레이터 호출됨
8
리턴 값을 처리하는 데코레이터
리턴 값을 갖는 함수에도 데코레이터를 적용할 수 있습니다.
def 리턴값_처리(func):
def wrapper():
result = func()
return f"결과: {result}"
return wrapper
@리턴값_처리
def test():
return "테스트"
print(test())
결과:
결과: 테스트
파라미터를 갖는 데코레이터
데코레이터 자체도 파라미터를 받을 수 있습니다.
def 로그_레벨(level="INFO"):
def decorator(func):
def wrapper():
print(f"[{level}] 로그: 함수 실행")
func()
return wrapper
return decorator
@로그_레벨(level="DEBUG")
def test_log():
print("테스트 로그")
test_log()
결과:
[DEBUG] 로그: 함수 실행
테스트 로그
클래스 기반 데코레이터
함수뿐만 아니라 클래스도 데코레이터로 사용할 수 있습니다.
class TestDecorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print("데코레이터 내부 기능 실행")
return self.func(*args, **kwargs)
@TestDecorator
def test():
print("테스트 함수 실행")
test()
결과:
데코레이터 내부 기능 실행
테스트 함수 실행
wraps 사용
functools.wraps는 데코레이터를 사용했을 때 원본 함수의 메타데이터를 보존하는 데 유용합니다.
from functools import wraps
def 데코레이터(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("데코레이터 실행")
return func(*args, **kwargs)
return wrapper
@데코레이터
def test():
"""테스트 함수 설명"""
pass
print(test.__doc__)
결과:
테스트 함수 설명
wraps는 원본 함수의 이름, 문서 문자열 등을 보존하여 더 깔끔한 코드를 작성할 수 있게 해줍니다.