데코레이터 활용 우수 사례
데코레이터를 언제, 어떤 목적으로 쓰면 좋은지 데코레이터가 특히 유용한 대표적인 상황들을 소개한다.
데코레이터가 좋은 선택이 되는 주요 경우
| 활용 사례 | 설명 | 효과 |
|---|---|---|
| 파라미터 변환 | • 복잡하고 반복적인 인자를 받는 레거시 함수나 프레임워크 함수에서, 데코레이터가 중간에서 인자를 가로채 가공한 뒤 깔끔한 도메인 객체(helper)로 변환해줌 • 파라미터가 어떻게 처리되는지 세부사항을 숨기면서 함수의 서명을 변경함 |
• 복잡한 함수에 더 깔끔한 인터페이스를 제공할 때 유용. 단, 의도를 명확히 해야 함 • 대규모 리팩토링 없이도 함수의 서명을 더 간단하고 직관적으로 변경할 수 있으며, 디자인 패턴에서의 어댑터(Adapter) 역할을 수행 |
| 코드 추적 (Tracing) | • 기존 코드를 전혀 수정하지 않고 외부에서 함수의 실행 경로와 상태를 모니터링하는 강력한 추상화 도구 | • 함수의 실행 경로 추적 (실행 함수 로깅) • 시스템 지표 모니터링 (CPU, 메모리 사용량 등) • 함수의 실행 시간 측정 • 실행 시점 및 전달된 파라미터 로깅 |
| 파라미터 유효성 검사 | • 함수의 인자 값이나 데이터 타입이 올바른지 투명하게 검사하는 데 사용됨 | • DbC(계약에 의한 디자인) 원칙에 따라 함수의 사전조건(Pre-condition)이나 사후조건(Post-condition)을 강제할 수 있어 코드의 안정성이 높아짐 |
| 재시도 로직 구현 | • retry 데코레이터처럼 구현 | • 네트워크 불안정이나 일시적인 서버 오류로 실패하는 작업에 대해, 데코레이터를 이용해 자동으로 재시도하는 로직을 손쉽게 붙일 수 있음 |
| 반복 작업을 데코레이터로 이동 | • 클래스나 함수에서 반복되는 작업을 데코레이터로 빼내어 단순화 | • DRY 원칙과 직접 관련 |
함수 서명 변경(파라미터 변환)
- 객체지향에서는 서로 다른 인터페이스를 가진 객체를 연결할 때 어댑터 패턴을 사용함
- 이와 비슷하게, 함수의 서명(파라미터 구조)을 변경하고 싶을 때 데코레이터를 사용할 수 있음
- 왜 필요한가?
- 레거시 코드나 프레임워크에는 복잡하고 긴 파라미터를 가진 함수가 많음
예시) 이 함수를 여러 곳에서 호출하다 보면, 매번 같은 보일러플레이트 코드가 반복되고 가독성이 떨어짐
def resolver_function(root, args, context, info):
helper = DomainObject(root, args, context, info)
...
helper.process()
- 데코레이터를 사용해서 복잡한 파라미터를 숨기고, 도메인 객체만 받도록 서명을 단순화할 수 있음
@DomainArgs
def resolver_function(helper):
helper.process()
- 동작
- 데코레이터가 원래의
(root, args, context, info)를 가로챈다. - 내부에서
DomainObject를 생성한다. - 데코레이팅된 함수에는 이미 만들어진
helper객체만 전달한다. - 반대로, 기존 코드가 객체를 받아서 여러 파라미터로 분해해 사용하는 경우에도 데코레이터를 중간 레이어로 사용해 변경을 최소화할 수 있다.
- 데코레이터가 원래의
데코레이터를 사용해서 함수를 더 간단하고 간결한 서명을 갖도록 만들자
- 주의할 점
- 명확한 의도를 가지고 사용할 때만 유용함
- 실수로 함수 서명을 이상하게 바꿔버리면 오히려 코드가 혼란스러워짐
파라미터 유효성 검사
- 데코레이터는 파라미터의 값이나 타입이 유효한지 검사하는 데 매우 적합
- Design by Contract(DbC) 원칙에 따라 사전조건(precondition) 이나 사후조건(postcondition) 을 강제할 수 있음
- 비슷한 검증 로직이 여러 함수에 반복될 때, 데코레이터로 한 곳에 모아두면 DRY 원칙을 지킬 수 있음
특히 다음과 같은 상황에서 자주 사용됨
- 필수 값이 있는지 검사
- 타입이 맞는지 검사
- 값의 범위가 유효한지 검사
- 특정 조건을 만족하는지 검사
데코레이터를 사용하면 검증 로직과 핵심 비즈니스 로직을 분리할 수 있어서
코드가 훨씬 깔끔해진다.
코드 추적
- 여기서 말하는 추적(tracing) 은 함수의 실행과 관련된 모니터링을 의미함
- 대표적인 활용 목적
- 함수의 실행 경로 추적 (어떤 함수가 호출되었는지 로깅)
- 함수 지표 모니터링 (CPU 사용량, 메모리 사용량 등)
- 함수의 실행 시간 측정
- 언제 함수가 실행되었고, 어떤 파라미터가 전달되었는지 로깅
이런 기능들은 대부분 데코레이터 형태로 제공되는 경우가 많음. 기존 비즈니스 코드를 전혀 수정하지 않고도 외부에서 기능을 붙일 수 있기 때문에 강력한 추상화 수단이 된다.
예시) 파라미터 유효성 검사 + 코드 추적 데코레이터
import time
from functools import wraps
# 1. 파라미터 유효성 검사 데코레이터
def validate_params(func):
@wraps(func)
def wrapper(*args, **kwargs):
if not args:
raise ValueError("인자가 필요합니다")
value = args[0]
if not isinstance(value, int):
raise TypeError("첫 번째 인자는 정수여야 합니다")
if value <= 0:
raise ValueError("첫 번째 인자는 0보다 커야 합니다")
print(f"✅ 유효성 검사 통과: {value}")
return func(*args, **kwargs)
return wrapper
# 2. 코드 추적 데코레이터
def trace(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"🚀 [{func.__name__}] 실행 시작")
print(f" 전달된 인자: args={args}, kwargs={kwargs}")
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"✅ [{func.__name__}] 실행 완료")
print(f" 반환값: {result}")
print(f" 실행 시간: {end - start:.4f}초")
print("-" * 40)
return result
return wrapper
# 3. 두 데코레이터를 함께 사용 (아래에서 위로 적용됨)
@trace
@validate_params
def process_number(n):
time.sleep(0.2) # 시간이 좀 걸리는 척
return n * 10
# 테스트
print(process_number(5))
# print(process_number(-3))
# print(process_number("abc"))
실행 결과)
🚀 [process_number] 실행 시작
전달된 인자: args=(5,), kwargs={}
✅ 유효성 검사 통과: 5
✅ [process_number] 실행 완료
반환값: 50
실행 시간: 0.2015초
----------------------------------------
50
참고) 적용 순서
@trace
@validate_params
def process_number(n):
...
process_number = trace(validate_params(process_number))- 이 경우 먼저 유효성 검사를 하고 → 그 다음에 추적(로깅/시간측정)이 실행됨. 순서를 바꾸고 싶으면 데코레이터 위치를 서로 바꾸면 된다.
📌 정리: 데코레이터는 “핵심 로직은 그대로 두고, 부가적인 관심사(검증, 로깅, 변환, 재시도 등)를 바깥에서 붙이는” 용도로 쓸 때 가장 효율적이다.
| 활용 목적 | 데코레이터의 역할 | 얻을 수 있는 이점 |
|---|---|---|
| 함수 서명 변경 | 복잡한 파라미터를 숨기고 간단한 인터페이스 제공 | 가독성 ↑, 레거시/프레임워크 코드와의 결합 최소화 |
| 파라미터 검증 | 유효성 검사를 투명하게 적용 | 관심사 분리, DbC 원칙 적용 가능 |
| 코드 추적 | 로깅, 시간 측정, 모니터링 | 기존 코드 수정 없이 기능 추가 |
| 재시도 / 반복 작업 | 횡단 관심사를 데코레이터로 이동 | DRY 원칙 준수, 클래스/함수 단순화 |
댓글