파이썬 데코레이터 완벽 이해: 실행 시간 측정과 로깅 래퍼
파이썬 데코레이터의 동작 원리부터 functools.wraps, 실행 시간 측정, 로깅 래퍼, 인자형 데코레이터와 적용 순서까지 예제로 정리합니다.

파이썬 데코레이터는 기존 함수의 코드를 직접 고치지 않고 실행 전후에 로깅, 실행 시간 측정, 권한 확인 같은 공통 동작을 덧붙이는 문법입니다. 핵심은 함수를 인자로 받아 새 함수를 반환하는 래퍼이며, @decorator는 함수를 데코레이터에 넣어 다시 대입하는 표현을 간결하게 쓴 것입니다.
핵심 요약
@timer는work = timer(work)와 같은 의미입니다.- 범용 래퍼는
*args, **kwargs로 인자를 받고 원래 함수의 반환값을 돌려줘야 합니다. @functools.wraps로 함수 이름과 문서 문자열 등 메타데이터를 보존합니다.- 경과 시간 측정에는 시스템 시각보다
time.perf_counter()가 적합합니다. - 로깅과 측정은 예외가 발생해도 기록되도록
try/finally구조를 고려합니다.
데코레이터 동작 원리
파이썬에서 함수는 변수에 담거나 다른 함수의 인자로 전달할 수 있는 객체입니다. 데코레이터는 이 특성을 이용합니다. 아래 두 표현은 사실상 같은 작업을 합니다.
def trace(func):
def wrapper():
print("호출 시작")
result = func()
print("호출 종료")
return result
return wrapper
@trace
def hello():
return "안녕하세요"
# 데코레이터 문법을 풀어 쓰면 다음과 같습니다.
# hello = trace(hello)
프로그램이 함수를 정의할 때 trace가 원래 hello를 받아 wrapper를 반환합니다. 이후 hello()를 호출하면 실제로는 래퍼가 실행되고, 그 안에서 원래 함수가 호출됩니다. 이 구조를 이해하면 데코레이터를 외워서 쓰는 대신 실행 흐름을 추적할 수 있습니다.
안전한 래퍼 기본형
실무 함수는 위치 인자와 키워드 인자를 다양하게 받습니다. 특정 함수에만 맞춘 래퍼가 되지 않도록 *args와 **kwargs를 전달하고, 원래 결과도 반드시 반환하는 편이 안전합니다. 또한 functools.wraps를 적용하면 디버깅·문서화 도구가 원래 함수의 이름과 설명을 확인할 수 있습니다.
from functools import wraps
def trace(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"{func.__name__} 호출")
result = func(*args, **kwargs)
return result
return wrapper
| 구성 요소 | 역할 | 빠뜨렸을 때 문제 |
|---|---|---|
*args, **kwargs |
다양한 호출 인자 전달 | 인자가 있는 함수에서 오류 |
return result |
원래 반환값 보존 | 호출 결과가 None으로 변경 |
@wraps(func) |
이름·문서·래핑 관계 보존 | 디버깅과 자동 문서화가 어려움 |
실행 시간 측정 데코레이터
함수의 처리 시간을 비교하려면 단조 증가하는 고해상도 성능 카운터인 time.perf_counter()를 사용할 수 있습니다. 시스템 시각이 조정되더라도 경과 시간 계산에 적합하며, 측정 단위는 초입니다.
from functools import wraps
from time import perf_counter
def measure_time(func):
@wraps(func)
def wrapper(*args, **kwargs):
started = perf_counter()
try:
return func(*args, **kwargs)
finally:
elapsed = perf_counter() - started
print(f"{func.__name__}: {elapsed:.4f}초")
return wrapper
finally를 사용했기 때문에 원래 함수에서 예외가 발생해도 측정값은 출력되고, 예외는 그대로 호출자에게 전달됩니다. 데코레이터가 오류를 임의로 삼켜 버리면 장애 원인을 찾기 어려워지므로, 복구 정책이 없다면 예외를 숨기지 않는 것이 좋습니다.
로깅 데코레이터
운영 코드에서는 print보다 표준 logging 모듈을 사용하는 편이 낫습니다. 로그 레벨, 출력 위치, 형식을 애플리케이션 설정에서 통제할 수 있기 때문입니다. 아래 예제는 호출 시작, 성공, 실패를 구분하며 예외 발생 시 스택 추적도 남깁니다.
import logging
from functools import wraps
logger = logging.getLogger(__name__)
def log_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
logger.info("%s 호출 시작", func.__name__)
try:
result = func(*args, **kwargs)
except Exception:
logger.exception("%s 호출 실패", func.__name__)
raise
logger.info("%s 호출 완료", func.__name__)
return result
return wrapper
인자와 반환값을 로그에 남길 때는 비밀번호, API 키, 주민등록번호 같은 민감정보가 섞이지 않았는지 먼저 확인해야 합니다. 범용 데코레이터가 모든 인자를 그대로 기록하게 만들기보다는 필요한 식별자만 골라 남기는 방식이 안전합니다.
인자를 받는 데코레이터 만들기
@retry(max_attempts=3)처럼 데코레이터 자체에 옵션을 주려면 함수가 한 단계 더 필요합니다. 바깥 함수는 설정값을 받고, 가운데 함수는 꾸밀 함수를 받으며, 안쪽 래퍼가 실제 호출을 담당합니다.
from functools import wraps
def prefix(label):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"[{label}] {func.__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
@prefix("BATCH")
def run_job(job_id):
return f"{job_id} 처리 완료"
읽는 순서는 바깥에서 안쪽입니다. 먼저 prefix("BATCH")가 실행되어 decorator를 만들고, 그 함수가 run_job을 받아 최종 래퍼를 반환합니다. 재시도 기능을 만들 경우에는 무조건 반복하지 말고, 재시도할 예외 종류와 대기 방식, 최대 횟수를 명확히 정해야 합니다.
여러 데코레이터의 적용 순서
데코레이터를 여러 개 쌓으면 함수 정의 시에는 아래쪽부터 적용되고, 호출 시에는 바깥쪽 래퍼부터 실행됩니다.
@outer
@inner
def work():
pass
# work = outer(inner(work))
따라서 outer가 호출 전체를 감싸고 그 안에서 inner가 실행됩니다. 실행 시간 측정과 재시도를 함께 쓴다면 순서에 따라 ‘전체 재시도 시간’을 재는지, ‘각 시도 시간’을 재는지가 달라집니다.
| 배치 | 측정되는 범위 | 적합한 질문 |
|---|---|---|
@timer가 재시도 바깥 |
대기 시간을 포함한 전체 실행 | 사용자가 기다린 총시간은? |
@timer가 재시도 안쪽 |
각 시도별 실행 | 개별 요청은 얼마나 걸렸나? |
실수 방지 체크리스트
- 원래 함수의 반환값을 래퍼에서 다시 반환했는지 확인합니다.
- 위치·키워드 인자를 모두 전달하는지 확인합니다.
@wraps를 적용했는지 확인합니다.- 예외를 기록한 뒤 의도 없이 삼키지 않는지 확인합니다.
- 로그에 인증정보와 개인정보가 포함되지 않는지 확인합니다.
- 데코레이터를 여러 개 쓸 때 적용 순서를 코드 리뷰에서 점검합니다.
- 동기 래퍼로 비동기 함수를 감싸고 있지 않은지 확인합니다.
- 실행 시간은
perf_counter()로 측정하고 단위를 표시합니다.
자주 묻는 질문
1. 데코레이터와 일반 함수 호출은 무엇이 다른가요?
일반 호출은 그 순간 결과를 얻지만, 데코레이터는 함수 정의 시 원래 함수를 다른 호출 가능 객체로 감싸 이후 호출 방식을 바꿉니다.
2. 데코레이터에 반드시 @ 문법을 써야 하나요?
아닙니다. func = decorator(func)처럼 직접 대입해도 같은 구조를 만들 수 있습니다.
3. functools.wraps는 왜 필요한가요?
래퍼가 원래 함수의 이름, 문서 문자열, 주석 정보와 __wrapped__ 관계를 이어받게 해 디버깅과 도구 연동을 돕습니다.
4. 실행 시간은 왜 time.time() 대신 perf_counter()로 재나요?
perf_counter()는 짧은 구간의 경과 시간을 측정하도록 제공되는 고해상도 성능 카운터이기 때문입니다.
5. 래퍼에서 반환값을 생략해도 되나요?
원래 함수가 반환값을 사용하지 않을 때만 겉으로 문제가 드러나지 않습니다. 범용 데코레이터라면 결과를 반환해야 함수의 계약을 보존할 수 있습니다.
6. 로깅 데코레이터에서 모든 인자를 출력해도 되나요?
권장하지 않습니다. 토큰, 비밀번호, 개인정보가 포함될 수 있으므로 허용한 필드만 선별하거나 마스킹해야 합니다.
7. 데코레이터 안에서 예외를 처리해도 되나요?
가능하지만 명확한 복구 정책이 없다면 로그만 남기고 raise로 다시 전달하는 편이 안전합니다.
8. 메서드에도 같은 데코레이터를 쓸 수 있나요?
네. self나 cls도 위치 인자로 전달되므로 *args, **kwargs 형태의 래퍼라면 일반적으로 함께 사용할 수 있습니다.
9. 비동기 함수에도 일반 데코레이터를 적용할 수 있나요?
메타데이터를 감싸는 것은 가능하지만 실제 실행을 측정하거나 예외를 처리하려면 async def wrapper에서 원래 함수를 await해야 합니다.
10. 클래스 기반 데코레이터는 언제 쓰나요?
호출 횟수나 설정처럼 여러 호출 사이에 유지할 상태가 필요할 때 고려할 수 있습니다. 단순 로깅이나 측정은 함수 기반 구현이 더 읽기 쉬운 경우가 많습니다.
공식 출처
공식 문서 확인일: 2026년 9월 2일