Python 데코레이터 활용: 실행 시간 측정부터 로그와 권한 확인까지

Ad Placeholder

앞에서 데코레이터의 기본 구조와 함수의 인자 및 반환값 처리, 그리고 인자를 가지는 데코레이터까지 살펴봤습니다.

데코레이터는 단순히 문법을 알아두는 것보다 어떤 상황에서 활용할 수 있는지 이해하는 것이 중요합니다.

실제 Python 코드에서는 다음과 같은 공통 기능을 데코레이터로 분리하는 경우가 많습니다.

  • 함수 실행 시간 측정
  • 로그 기록
  • 권한 확인
  • 실행 횟수 제한
  • 입력값 검증
  • 캐싱
  • 예외 처리
  • 함수 실행 전후의 공통 작업

이번 글에서는 이런 활용 방법을 간단한 예제와 함께 살펴보겠습니다.

데코레이터로 실행 시간 측정하기

데코레이터를 활용하면 특정 함수가 실행되는 데 얼마나 걸리는지 쉽게 측정할 수 있습니다.

Python의 time 모듈을 사용해보겠습니다.

import time

def measure_time(func):

    def wrapper(*args, **kwargs):
        start = time.time()

        result = func(*args, **kwargs)

        end = time.time()

        print(f"실행 시간: {end - start:.4f}초")

        return result

    return wrapper

이제 함수에 데코레이터를 적용합니다.

@measure_time
def calculate():
    time.sleep(1)
    return "계산 완료"

실행하면 다음과 비슷한 결과를 확인할 수 있습니다.

실행 시간: 1.0012초

함수 내부의 코드를 직접 수정하지 않았지만 데코레이터를 통해 실행 시간을 측정할 수 있습니다.

이 방식은 데이터 처리나 머신러닝 코드에서 특정 함수의 처리 속도를 확인할 때도 활용할 수 있습니다.

함수 실행 로그 남기기

데코레이터는 로그를 남기는 용도로도 사용할 수 있습니다.

def log_call(func):

    def wrapper(*args, **kwargs):
        print(f"{func.__name__} 함수 실행")

        result = func(*args, **kwargs)

        print(f"{func.__name__} 함수 종료")

        return result

    return wrapper

사용 방법은 다음과 같습니다.

@log_call
def greet(name):
    print(f"{name}님 안녕하세요")

실행합니다.

greet("Python")

결과:

greet 함수 실행
Python님 안녕하세요
greet 함수 종료

여러 함수에 같은 데코레이터를 적용할 수도 있습니다.

@log_call
def save_data():
    print("데이터 저장")


@log_call
def load_data():
    print("데이터 조회")

이렇게 하면 각각의 함수에 로그 기능을 직접 작성하지 않아도 됩니다.

함수 실행 횟수 제한하기

특정 함수가 무제한으로 실행되는 것을 막아야 하는 경우에도 데코레이터를 사용할 수 있습니다.

예를 들어 최대 3번까지만 실행할 수 있는 데코레이터를 만들어보겠습니다.

def limit_calls(max_calls):

    def decorator(func):
        count = 0

        def wrapper(*args, **kwargs):
            nonlocal count

            if count >= max_calls:
                print("함수 실행 횟수를 초과했습니다.")
                return None

            count += 1

            return func(*args, **kwargs)

        return wrapper

    return decorator

사용할 때는 다음과 같습니다.

@limit_calls(3)
def hello():
    print("Hello")

이제 함수를 네 번 호출해보겠습니다.

hello()
hello()
hello()
hello()

결과:

Hello
Hello
Hello
함수 실행 횟수를 초과했습니다.

여기서 nonlocal이 사용된 이유도 중요합니다.

nonlocal count

count는 decorator() 함수 안에서 만들어진 지역 변수입니다.

하지만 실제로 값을 변경하는 곳은 그 안쪽의 wrapper()입니다.

nonlocal을 사용하면 바깥쪽 함수의 count 변수를 변경할 수 있습니다.

권한 확인에 활용하기

웹 애플리케이션에서는 특정 기능을 실행하기 전에 사용자의 권한을 확인해야 하는 경우가 있습니다.

간단한 예제로 권한을 문자열로 전달받는다고 가정해보겠습니다.

def require_role(required_role):

    def decorator(func):

        def wrapper(user_role, *args, **kwargs):

            if user_role != required_role:
                print("권한이 없습니다.")
                return None

            return func(user_role, *args, **kwargs)

        return wrapper

    return decorator

사용할 수 있습니다.

@require_role("admin")
def delete_data(user_role):
    print("데이터를 삭제합니다.")

관리자 권한으로 호출하면:

delete_data("admin")

결과:

데이터를 삭제합니다.

일반 사용자라면:

delete_data("user")

결과:

권한이 없습니다.

실제 웹 프레임워크에서는 인증 시스템이나 미들웨어 등을 통해 더 복잡하게 구현하지만, 공통적인 권한 확인 로직을 함수와 분리한다는 아이디어는 동일합니다.

함수의 실행 전후에 작업 추가하기

데코레이터의 가장 기본적인 활용 방법은 함수 실행 전후에 공통 작업을 추가하는 것입니다.

def before_after(func):

    def wrapper(*args, **kwargs):
        print("함수 실행 전")

        result = func(*args, **kwargs)

        print("함수 실행 후")

        return result

    return wrapper

사용하면 다음과 같습니다.

@before_after
def process():
    print("데이터 처리 중...")

결과:

함수 실행 전
데이터 처리 중...
함수 실행 후

이런 구조는 로그 기록, 상태 확인, 리소스 준비와 정리 등 다양한 곳에서 사용할 수 있습니다.

여러 데코레이터를 하나의 함수에 적용하기

하나의 함수에 데코레이터를 여러 개 적용할 수도 있습니다.

예를 들어 다음과 같이 작성할 수 있습니다.

@log_call
@measure_time
def process():
    time.sleep(1)

여기서 순서를 이해하는 것이 중요합니다.

위 코드는 개념적으로 다음과 같습니다.

process = log_call(measure_time(process))

즉 아래에 있는 measure_time이 먼저 적용되고, 그 결과에 log_call이 적용됩니다.

구조를 그림으로 표현하면 다음과 같습니다.

process()
   ↓
log_call wrapper
   ↓
measure_time wrapper
   ↓
원래 process()

따라서 데코레이터를 여러 개 사용하는 경우에는 작성 순서와 실행 순서를 구분해서 생각해야 합니다.

functools.wraps를 사용하는 이유

지금까지 만든 데코레이터에는 한 가지 문제가 있습니다.

다음 코드를 실행해보겠습니다.

def decorator(func):

    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)

    return wrapper


@decorator
def greet():
    """인사하는 함수입니다."""
    print("안녕하세요")

그리고 함수 이름을 확인합니다.

print(greet.__name__)

원래 함수 이름인 greet가 아니라 다음과 같은 결과가 나옵니다.

wrapper

데코레이터가 greet를 wrapper로 감쌌기 때문입니다.

이때 Python의 functools.wraps를 사용할 수 있습니다.

from functools import wraps

def decorator(func):

    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)

    return wrapper

이제 다시 확인해보면,

print(greet.__name__)

결과는 다음과 같습니다.

greet

함수의 문서 문자열도 유지됩니다.

print(greet.__doc__)

결과:

인사하는 함수입니다.

따라서 실제 프로젝트에서 데코레이터를 작성한다면 다음과 같은 형태를 사용하는 것이 좋습니다.

from functools import wraps

def decorator(func):

    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result

    return wrapper

wraps()는 원래 함수의 이름과 문서 문자열 등 메타데이터를 wrapper에 보존하는 데 도움을 줍니다.

인자를 가지는 데코레이터에도 wraps 사용하기

앞에서 배운 인자를 가지는 데코레이터에서도 wraps를 사용할 수 있습니다.

예를 들어 로그 레벨을 지정하는 데코레이터를 만들어보겠습니다.

from functools import wraps

def log(level):

    def decorator(func):

        @wraps(func)
        def wrapper(*args, **kwargs):
            print(f"[{level}] {func.__name__} 실행")

            result = func(*args, **kwargs)

            return result

        return wrapper

    return decorator

사용 방법은 다음과 같습니다.

@log("INFO")
def save_data():
    """데이터를 저장합니다."""
    print("데이터 저장")

이제 함수의 이름과 문서 문자열을 확인해도 원래 함수의 정보가 유지됩니다.

print(save_data.__name__)
print(save_data.__doc__)

결과:

save_data
데이터를 저장합니다.

실제 프로젝트에서 재사용할 데코레이터를 만든다면 functools.wraps를 함께 사용하는 습관을 들이는 것이 좋습니다.

데코레이터를 사용할 때 주의할 점

데코레이터는 편리하지만 모든 기능을 데코레이터로 만드는 것이 좋은 것은 아닙니다.

1. 너무 복잡한 로직을 넣지 않기

데코레이터 내부에 지나치게 많은 로직을 넣으면 함수의 실제 동작을 파악하기 어려워질 수 있습니다.

공통적으로 반복되는 기능을 분리하는 용도로 사용하는 것이 좋습니다.

2. 데코레이터 적용 순서 확인하기

여러 데코레이터를 사용하는 경우 순서에 따라 실행 결과가 달라질 수 있습니다.

@decorator_a
@decorator_b
def function():
    ...

이는 다음과 같은 구조입니다.

function = decorator_a(decorator_b(function))

따라서 단순히 위에서부터 실행된다고 생각하면 안 됩니다.

3. 함수의 인자와 반환값을 유지하기

재사용할 데코레이터라면 일반적으로 다음 구조를 사용하는 것이 좋습니다.

def decorator(func):

    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result

    return wrapper

*args, **kwargs를 사용하면 다양한 형태의 함수에 적용하기 편하고, return result를 통해 원래 함수의 반환값도 유지할 수 있습니다.

데코레이터를 언제 사용하면 좋을까?

데코레이터를 사용할지 고민된다면 여러 함수에서 반복되는 공통 기능인지 생각해보면 좋습니다.

예를 들어 다음과 같은 기능이라면 데코레이터와 잘 맞습니다.

기능활용 예
로그함수 실행 기록
시간 측정실행 시간 확인
권한 확인특정 기능 접근 제한
실행 횟수 제한API 호출 제한
입력 검증함수 실행 전 값 확인
캐싱이전 결과 재사용
예외 처리공통 오류 처리
실행 전후 작업공통 초기화 및 정리

반대로 특정 함수 하나에서만 사용하는 복잡한 로직이라면 굳이 데코레이터로 만들 필요는 없습니다.

데코레이터의 핵심 정리

지금까지 배운 내용을 하나로 정리하면 다음과 같습니다.

일반적인 데코레이터:

@decorator
def function():
    ...

개념적으로:

function = decorator(function)

인자를 가지는 데코레이터:

@decorator(value)
def function():
    ...

개념적으로:

function = decorator(value)(function)

그리고 실제 함수 호출을 처리하는 wrapper()에서는 다음 구조를 사용하는 경우가 많습니다.

from functools import wraps

def decorator(value):

    def inner(func):

        @wraps(func)
        def wrapper(*args, **kwargs):
            result = func(*args, **kwargs)
            return result

        return wrapper

    return inner

이 구조를 이해하면 Python에서 자주 등장하는 다양한 데코레이터 코드를 읽는 데 큰 도움이 됩니다.

마무리

데코레이터는 처음에는 wrapper()와 중첩 함수 때문에 복잡하게 느껴질 수 있습니다.

하지만 핵심은 간단합니다.

기존 함수의 코드를 직접 수정하지 않고, 공통 기능을 바깥에서 추가하는 것이 데코레이터의 가장 중요한 역할입니다.

실행 시간 측정이나 로그 기록처럼 여러 함수에서 반복되는 기능을 데코레이터로 분리하면 코드 중복을 줄이고 각 함수의 핵심 로직도 더 깔끔하게 유지할 수 있습니다.

특히 실제 프로젝트에서는 *args, **kwargs, return, functools.wraps를 함께 사용하는 형태가 자주 등장하므로 이 네 가지를 하나의 패턴으로 익혀두면 좋습니다.

jiji

AI, Python, 생활정보와 정부 정책 정보를 쉽게 설명하는 정보 블로그입니다.

Ad Placeholder
Scroll to Top