🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Python 데코레이터 완벽 가이드 - 슬라이드 1/13
A

AI Generated

2025. 10. 29. · 25 Views

Python 데코레이터 완벽 가이드

Python 데코레이터는 함수나 클래스의 동작을 수정하는 강력한 도구입니다. 코드 재사용성을 높이고, 로깅, 인증, 성능 측정 등 다양한 기능을 쉽게 추가할 수 있습니다. 실무에서 자주 사용되는 핵심 개념을 단계별로 배워보세요.


목차

  1. 데코레이터 기본 개념
  2. 함수 데코레이터 만들기
  3. 여러 데코레이터 중첩하기
  4. 인자를 받는 데코레이터
  5. 클래스 기반 데코레이터
  6. functools.wraps 활용
  7. 실행 시간 측정 데코레이터
  8. 로깅 데코레이터
  9. 인증 검사 데코레이터
  10. 재시도 데코레이터

1. 데코레이터 기본 개념

시작하며

여러분이 여러 함수에 동일한 기능을 추가해야 할 때 어떻게 하시나요? 예를 들어, 10개의 함수마다 실행 전후에 로그를 남기거나, 실행 시간을 측정해야 한다면 모든 함수에 같은 코드를 복사해서 붙여넣으시나요?

이런 방식은 코드 중복을 만들고, 나중에 수정이 필요할 때 모든 곳을 찾아다니며 고쳐야 하는 악몽을 만듭니다. 유지보수는 점점 어려워지고, 버그가 발생할 확률도 높아집니다.

바로 이럴 때 필요한 것이 데코레이터입니다. 데코레이터를 사용하면 함수의 원본 코드를 수정하지 않고도 새로운 기능을 추가할 수 있습니다.

개요

간단히 말해서, 데코레이터는 다른 함수를 감싸서(wrap) 그 함수의 동작을 수정하거나 확장하는 함수입니다. 실무에서 데코레이터는 횡단 관심사(cross-cutting concerns)를 처리하는 데 매우 유용합니다.

로깅, 성능 측정, 캐싱, 인증 검사 같은 기능들은 여러 함수에서 공통으로 필요한데, 데코레이터를 사용하면 이를 한 곳에서 관리할 수 있습니다. 기존에는 각 함수 내부에 로그 코드를 직접 작성했다면, 이제는 @log_decorator 한 줄만 추가하면 됩니다.

코드가 깔끔해지고, 비즈니스 로직과 부가 기능이 분리됩니다. 데코레이터의 핵심 특징은 세 가지입니다.

첫째, 함수를 인자로 받습니다. 둘째, 새로운 함수를 반환합니다.

셋째, @기호를 사용해 간편하게 적용할 수 있습니다. 이러한 특징들이 코드의 재사용성과 가독성을 동시에 높여줍니다.

코드 예제

# 기본 데코레이터 구조
def my_decorator(func):
    # 원본 함수를 감싸는 wrapper 함수 정의
    def wrapper():
        print("함수 실행 전")
        func()  # 원본 함수 호출
        print("함수 실행 후")
    return wrapper  # wrapper 함수 반환

# 데코레이터 적용
@my_decorator
def say_hello():
    print("Hello!")

# 함수 호출
say_hello()

설명

이것이 하는 일: 데코레이터는 원본 함수를 수정하지 않고 그 앞뒤에 추가 동작을 삽입합니다. 마치 선물을 포장지로 감싸듯이, 함수를 다른 함수로 감싸는 것입니다.

첫 번째로, my_decorator 함수는 func라는 함수를 인자로 받습니다. 이것이 우리가 꾸며주고 싶은 원본 함수입니다.

데코레이터는 이 함수를 직접 수정하지 않고, 그대로 보관하고 있습니다. 그 다음으로, 내부에 wrapper라는 새로운 함수를 정의합니다.

이 wrapper 함수가 실제로 실행될 함수입니다. wrapper는 원본 함수(func)를 호출하기 전에 "함수 실행 전"을 출력하고, 호출 후에 "함수 실행 후"를 출력합니다.

원본 함수의 앞뒤에 우리가 원하는 동작을 추가하는 것입니다. 마지막으로, my_decorator는 이 wrapper 함수를 반환합니다.

@my_decorator를 사용하면, say_hello = my_decorator(say_hello)와 동일한 작업이 일어납니다. 즉, say_hello라는 이름이 이제 wrapper 함수를 가리키게 됩니다.

여러분이 이 코드를 실행하면 "함수 실행 전", "Hello!", "함수 실행 후"가 순서대로 출력됩니다. 원본 say_hello 함수는 단순히 "Hello!"만 출력하지만, 데코레이터 덕분에 추가 기능이 생긴 것입니다.

이를 통해 코드 중복을 줄이고, 관심사를 분리하며, 유지보수성을 크게 향상시킬 수 있습니다.

실전 팁

💡 데코레이터는 함수를 반환한다는 점을 기억하세요. @decorator는 실제로 func = decorator(func)와 같은 의미입니다.

💡 여러 함수에 같은 기능을 추가해야 할 때 데코레이터를 고려하세요. 코드 중복을 크게 줄일 수 있습니다.

💡 데코레이터를 사용하면 비즈니스 로직과 부가 기능(로깅, 인증 등)을 깔끔하게 분리할 수 있습니다.

💡 디버깅 시 데코레이터가 적용된 함수는 실제로 wrapper 함수라는 점을 명심하세요. 함수의 이름이나 docstring이 달라질 수 있습니다.


2. 함수 데코레이터 만들기

시작하며

여러분이 API 서버를 개발하는데, 모든 엔드포인트에서 요청과 응답을 로그로 남겨야 한다는 요구사항을 받았습니다. 50개의 엔드포인트마다 똑같은 로그 코드를 복사해서 붙여넣으시겠습니까?

이런 반복 작업은 개발자의 시간을 낭비하고, 나중에 로그 형식을 바꾸려면 50군데를 다 수정해야 합니다. 누락되는 부분이 생기면 일관성도 깨집니다.

직접 데코레이터를 만들면 이 문제를 한 번에 해결할 수 있습니다. 한 곳에서 로직을 정의하고, 필요한 함수에 @데코레이터만 붙이면 됩니다.

개요

간단히 말해서, 실용적인 데코레이터를 만들려면 원본 함수의 인자를 처리하고, 반환값을 전달하며, 예외를 처리하는 방법을 알아야 합니다. 실무에서는 인자가 없는 단순한 함수보다는 다양한 매개변수를 받는 복잡한 함수를 다루게 됩니다.

데코레이터는 이런 함수들도 문제없이 감싸야 합니다. *args와 **kwargs를 사용하면 어떤 형태의 인자든 받을 수 있습니다.

기존에는 특정 함수 시그니처에만 맞는 데코레이터를 만들었다면, 이제는 모든 함수에 적용 가능한 범용 데코레이터를 만들 수 있습니다. 코드의 재사용성이 극대화됩니다.

핵심은 세 가지입니다. *args와 **kwargs로 모든 인자를 받고, 원본 함수의 반환값을 그대로 전달하며, 필요한 경우 예외도 처리합니다.

이렇게 하면 어떤 함수에도 안전하게 적용할 수 있는 튼튼한 데코레이터가 됩니다.

코드 예제

def log_function_call(func):
    """함수 호출을 로깅하는 데코레이터"""
    def wrapper(*args, **kwargs):
        # 함수 이름과 인자를 로그로 출력
        print(f"[LOG] 함수 '{func.__name__}' 호출")
        print(f"[LOG] 인자: args={args}, kwargs={kwargs}")

        # 원본 함수 실행
        result = func(*args, **kwargs)

        # 반환값 로그 출력
        print(f"[LOG] 반환값: {result}")
        return result  # 반환값을 호출자에게 전달
    return wrapper

@log_function_call
def add(a, b):
    return a + b

print(add(3, 5))

설명

이것이 하는 일: 이 데코레이터는 함수가 호출될 때 함수 이름, 전달된 인자, 그리고 반환값을 자동으로 로그에 기록합니다. 개발자가 직접 로그 코드를 작성할 필요가 없습니다.

첫 번째로, wrapper 함수는 *args와 **kwargs를 매개변수로 받습니다. *args는 위치 인자(positional arguments)를 튜플로, **kwargs는 키워드 인자(keyword arguments)를 딕셔너리로 받습니다.

이렇게 하면 원본 함수가 어떤 형태의 인자를 받든 상관없이 모두 처리할 수 있습니다. 그 다음으로, 함수 실행 전에 로그를 출력합니다.

func.__name__으로 함수 이름을 가져오고, args와 kwargs를 출력합니다. 그리고 result = func(*args, **kwargs)로 원본 함수를 호출하면서 받은 인자들을 그대로 전달합니다.

별표(*)는 언패킹 연산자로, 튜플과 딕셔너리를 풀어서 전달합니다. 마지막으로, 원본 함수의 반환값을 result 변수에 저장하고, 이를 로그로 출력한 후 return result로 호출자에게 반환합니다.

이 단계가 매우 중요합니다. 반환값을 전달하지 않으면 원본 함수의 동작이 깨지기 때문입니다.

여러분이 add(3, 5)를 호출하면 로그가 자동으로 출력되고, 정확히 8이라는 결과를 받게 됩니다. 이 데코레이터는 add뿐만 아니라 multiply, divide 등 어떤 함수에도 적용할 수 있습니다.

디버깅할 때 특히 유용하며, 함수의 실행 흐름을 추적하는 데 큰 도움이 됩니다.

실전 팁

💡 항상 *args와 **kwargs를 사용하여 범용 데코레이터를 만드세요. 특정 인자 개수에 의존하면 재사용성이 떨어집니다.

💡 원본 함수의 반환값을 반드시 return으로 전달하세요. 이를 놓치면 함수가 항상 None을 반환하는 버그가 생깁니다.

💡 func.__name__을 사용하면 디버깅 시 어떤 함수가 실행되는지 쉽게 확인할 수 있습니다.

💡 실무에서는 print 대신 logging 모듈을 사용하세요. 로그 레벨을 조절하고 파일로 저장할 수 있습니다.

💡 데코레이터 내부에서 예외가 발생할 수 있다면 try-except로 감싸서 원본 함수의 예외가 가려지지 않도록 하세요.


3. 여러 데코레이터 중첩하기

시작하며

여러분이 함수에 로깅도 추가하고, 실행 시간도 측정하고, 인증도 확인해야 한다면 어떻게 하시겠습니까? 하나의 거대한 데코레이터를 만들어야 할까요?

하나의 데코레이터에 모든 기능을 넣으면 코드가 복잡해지고, 각 기능을 독립적으로 사용할 수 없게 됩니다. 재사용성이 떨어지고 유지보수가 어려워집니다.

Python은 여러 데코레이터를 중첩(stack)해서 사용할 수 있습니다. 각각 독립적인 데코레이터를 만들고, 필요한 만큼 쌓아올리면 됩니다.

개요

간단히 말해서, 데코레이터 중첩은 여러 개의 @데코레이터를 연속으로 쓰는 것이며, 아래에서 위로 순서대로 적용됩니다. 실무에서는 하나의 함수에 여러 관심사가 존재합니다.

웹 API의 경우 인증, 권한 확인, 로깅, 캐싱, 성능 측정 등이 필요할 수 있습니다. 각각을 독립적인 데코레이터로 만들면 필요에 따라 조합할 수 있습니다.

기존에는 모든 기능이 뒤섞인 복잡한 함수를 만들었다면, 이제는 각 기능을 레고 블록처럼 조립할 수 있습니다. 어떤 API는 인증만, 어떤 API는 인증과 로깅을, 또 다른 API는 세 가지 모두 적용하는 식으로 유연하게 구성할 수 있습니다.

중요한 점은 두 가지입니다. 첫째, 데코레이터는 아래에서 위로 적용됩니다.

둘째, 각 데코레이터는 이전 데코레이터의 결과(함수)를 감쌉니다. 이 순서를 이해하면 복잡한 조합도 예측 가능하게 사용할 수 있습니다.

코드 예제

def uppercase_decorator(func):
    """결과를 대문자로 변환"""
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper

def exclamation_decorator(func):
    """결과에 느낌표 추가"""
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result + "!!!"
    return wrapper

# 데코레이터 중첩: 아래부터 위로 적용됨
@exclamation_decorator  # 2번째: 느낌표 추가
@uppercase_decorator     # 1번째: 대문자 변환
def greet(name):
    return f"hello {name}"

print(greet("python"))  # 출력: HELLO PYTHON!!!

설명

이것이 하는 일: 데코레이터를 여러 개 쌓으면 각 데코레이터가 순차적으로 원본 함수를 감싸서, 여러 기능을 조합할 수 있습니다. 첫 번째로, 가장 아래에 있는 @uppercase_decorator가 먼저 적용됩니다.

greet = uppercase_decorator(greet)가 실행되어, greet는 이제 대문자로 변환하는 wrapper 함수가 됩니다. 이 시점에서 greet("python")을 호출하면 "HELLO PYTHON"이 반환됩니다.

그 다음으로, @exclamation_decorator가 적용됩니다. 이미 변환된 greet 함수(사실은 uppercase의 wrapper)를 받아서 또 한 번 감쌉니다.

greet = exclamation_decorator(greet)가 실행되는 것입니다. 이제 greet는 exclamation의 wrapper 함수가 되었고, 내부에는 uppercase의 wrapper가 있습니다.

최종적으로 greet("python")을 호출하면 이런 흐름으로 진행됩니다. 먼저 exclamation의 wrapper가 실행되고, 그 안에서 uppercase의 wrapper를 호출합니다.

uppercase의 wrapper는 원본 greet를 호출하여 "hello python"을 받고 이를 "HELLO PYTHON"으로 변환합니다. 이 결과가 exclamation으로 돌아가서 "HELLO PYTHON!!!"이 됩니다.

여러분이 이 패턴을 사용하면 각 데코레이터를 독립적으로 개발하고 테스트할 수 있습니다. uppercase_decorator는 대문자 변환만 책임지고, exclamation_decorator는 느낌표만 책임집니다.

단일 책임 원칙(SRP)을 지키면서도 강력한 조합을 만들 수 있습니다. 실무에서는 @login_required, @admin_only, @rate_limit 같은 데코레이터들을 조합하여 복잡한 권한 체계를 간결하게 표현할 수 있습니다.

실전 팁

💡 데코레이터 순서가 중요합니다. 아래에서 위로 적용된다는 점을 항상 기억하세요.

💡 각 데코레이터를 독립적으로 만들어 재사용성을 높이세요. 하나의 거대한 데코레이터보다 작은 데코레이터 여러 개가 낫습니다.

💡 디버깅할 때는 데코레이터를 하나씩 제거해가며 문제를 격리하세요.

💡 성능에 민감한 경우, 데코레이터를 너무 많이 중첩하면 함수 호출 오버헤드가 증가할 수 있습니다.

💡 실무에서는 인증 → 권한 확인 → 로깅 → 비즈니스 로직 순서로 데코레이터를 배치하는 것이 일반적입니다.


4. 인자를 받는 데코레이터

시작하며

여러분이 로그 레벨을 조절할 수 있는 로깅 데코레이터를 만들어야 한다면 어떻게 하시겠습니까? 어떤 함수는 DEBUG 레벨로, 어떤 함수는 INFO 레벨로 로그를 남기고 싶습니다.

지금까지 배운 데코레이터는 인자를 받을 수 없습니다. 각 레벨마다 별도의 데코레이터를 만들어야 할까요?

그러면 코드 중복이 심해집니다. 데코레이터 팩토리(Decorator Factory) 패턴을 사용하면 데코레이터에 인자를 전달할 수 있습니다.

한 번의 구현으로 다양한 설정을 지원하는 유연한 데코레이터를 만들 수 있습니다.

개요

간단히 말해서, 인자를 받는 데코레이터는 실제로 데코레이터를 반환하는 함수입니다. 함수를 3단계로 중첩합니다.

실무에서는 설정 가능한 데코레이터가 필수입니다. 재시도 횟수를 지정하거나, 타임아웃 시간을 설정하거나, 캐시 유효 시간을 조절하는 등의 경우가 많습니다.

매번 새로운 데코레이터를 만드는 것은 비효율적입니다. 기존에는 하드코딩된 값으로 데코레이터를 만들었다면, 이제는 매개변수로 동작을 제어할 수 있습니다.

@retry(times=3)과 @retry(times=5)를 다르게 사용할 수 있습니다. 핵심은 3단계 구조입니다.

첫째, 인자를 받는 가장 바깥 함수. 둘째, 실제 데코레이터 함수.

셋째, wrapper 함수. 이 구조를 이해하면 어떤 설정도 데코레이터로 만들 수 있습니다.

코드 예제

def repeat(times):
    """함수를 여러 번 반복 실행하는 데코레이터"""
    # 실제 데코레이터를 반환
    def decorator(func):
        def wrapper(*args, **kwargs):
            # times 만큼 함수를 반복 실행
            for i in range(times):
                print(f"[실행 {i+1}/{times}]")
                result = func(*args, **kwargs)
            # 마지막 실행 결과만 반환
            return result
        return wrapper
    return decorator

# 인자를 전달하여 데코레이터 생성
@repeat(times=3)
def say_hi():
    print("안녕하세요!")

say_hi()

설명

이것이 하는 일: 이 패턴은 데코레이터에 설정값을 전달하여 동작을 커스터마이즈합니다. 데코레이터를 반환하는 팩토리 함수를 만드는 것입니다.

첫 번째로, repeat(times)는 가장 바깥 함수로 times라는 인자를 받습니다. 이 함수는 데코레이터를 반환합니다.

@repeat(times=3)이 실행되면 repeat(3)이 호출되고, 그 결과로 decorator 함수가 반환됩니다. 중요한 점은 times 값이 클로저(closure)로 캡처되어 내부 함수들이 접근할 수 있다는 것입니다.

그 다음으로, decorator(func)는 실제 데코레이터입니다. 꾸며질 함수를 받아서 wrapper를 반환합니다.

이것은 우리가 이전에 배운 일반 데코레이터와 동일한 구조입니다. @repeat(times=3)에서 반환된 decorator가 say_hi 함수에 적용되는 것입니다.

마지막으로, wrapper(*args, **kwargs)는 실제로 실행될 함수입니다. 여기서 for 루프를 times 번 돌면서 원본 함수를 반복 실행합니다.

times 변수는 클로저 덕분에 접근 가능합니다. 마지막 실행의 결과만 return하여 호출자에게 전달합니다.

여러분이 say_hi()를 호출하면 "안녕하세요!"가 3번 출력됩니다. times=5로 바꾸면 5번 출력됩니다.

같은 코드로 다양한 동작을 구현할 수 있습니다. 실무에서는 @cache(ttl=300), @rate_limit(calls=100, period=60), @timeout(seconds=30) 같은 설정 가능한 데코레이터를 만들 때 이 패턴을 사용합니다.

실전 팁

💡 3단계 구조를 명확히 이해하세요: 인자 받기 → 데코레이터 반환 → wrapper 반환.

💡 클로저 덕분에 가장 바깥 함수의 인자들을 내부에서 사용할 수 있습니다.

💡 @repeat(3)처럼 괄호를 꼭 써야 합니다. @repeat만 쓰면 함수 자체가 전달되어 오류가 발생합니다.

💡 기본값을 제공하려면 def repeat(times=1): 처럼 기본 인자를 설정하세요.

💡 복잡한 설정이 필요하면 딕셔너리나 **kwargs를 사용하여 여러 옵션을 받을 수 있습니다.


5. 클래스 기반 데코레이터

시작하며

여러분이 데코레이터가 상태(state)를 유지해야 한다면 어떻게 하시겠습니까? 예를 들어 함수가 몇 번 호출되었는지 카운트하거나, 이전 호출 결과를 캐싱해야 할 때 말입니다.

함수 기반 데코레이터로도 가능하지만, 클로저 변수를 사용해야 하고 코드가 복잡해집니다. 여러 메서드로 기능을 분리하기도 어렵습니다.

클래스 기반 데코레이터를 사용하면 객체지향 프로그래밍의 이점을 살릴 수 있습니다. 상태를 인스턴스 변수로 관리하고, 여러 메서드로 로직을 깔끔하게 분리할 수 있습니다.

개요

간단히 말해서, 클래스 기반 데코레이터는 __init__과 call 메서드를 구현한 클래스입니다. 인스턴스 자체가 호출 가능한 객체가 됩니다.

실무에서는 복잡한 로직을 가진 데코레이터가 필요할 때가 있습니다. 통계 수집, 캐싱, 속도 제한(rate limiting) 같은 기능은 내부 상태를 관리해야 합니다.

클래스를 사용하면 이런 상태를 self 변수로 깔끔하게 관리할 수 있습니다. 기존에는 함수 내부에 nonlocal 변수를 사용했다면, 이제는 self.count 같은 인스턴스 변수를 사용합니다.

코드가 더 읽기 쉽고 객체지향적입니다. 핵심은 두 가지입니다.

__init__에서 원본 함수를 받아 저장하고, __call__에서 실제 호출을 처리합니다. 클래스의 인스턴스가 함수처럼 동작하는 것입니다.

코드 예제

class CountCalls:
    """함수 호출 횟수를 세는 데코레이터 클래스"""
    def __init__(self, func):
        self.func = func  # 원본 함수 저장
        self.count = 0    # 호출 횟수 초기화

    def __call__(self, *args, **kwargs):
        # 호출될 때마다 카운트 증가
        self.count += 1
        print(f"[호출 횟수: {self.count}번]")
        # 원본 함수 실행
        return self.func(*args, **kwargs)

@CountCalls
def process_data():
    print("데이터 처리 중...")

# 여러 번 호출
process_data()
process_data()
print(f"총 호출 횟수: {process_data.count}")

설명

이것이 하는 일: 클래스 기반 데코레이터는 인스턴스 변수에 상태를 저장하여, 호출 간에 정보를 유지할 수 있습니다. 첫 번째로, init 메서드가 실행됩니다.

@CountCalls가 적용되면 CountCalls(process_data)가 호출되고, 원본 함수가 self.func에 저장됩니다. 동시에 self.count를 0으로 초기화합니다.

이제 process_data라는 이름은 CountCalls의 인스턴스를 가리킵니다. 그 다음으로, process_data()를 호출하면 실제로는 call 메서드가 실행됩니다.

Python에서 __call__을 구현한 객체는 함수처럼 호출할 수 있습니다. call 내부에서 self.count를 1 증가시키고, 현재 카운트를 출력한 후, self.func(*args, **kwargs)로 원본 함수를 실행합니다.

중요한 점은 self.count가 인스턴스 변수이기 때문에 여러 번 호출해도 값이 유지된다는 것입니다. 첫 번째 호출에서 count가 1이 되고, 두 번째 호출에서는 2가 됩니다.

함수 기반 데코레이터에서는 nonlocal을 사용해야 하지만, 클래스에서는 자연스럽게 self를 사용합니다. 여러분이 이 패턴을 사용하면 훨씬 복잡한 상태 관리도 가능합니다.

예를 들어 마지막 10개의 호출 결과를 리스트로 저장하거나, 호출 시간을 추적하거나, 캐시 딕셔너리를 관리할 수 있습니다. process_data.count처럼 외부에서 상태에 접근할 수도 있어서, 모니터링이나 디버깅에도 유용합니다.

실전 팁

💡 상태를 유지해야 하는 데코레이터는 클래스로 만드는 것이 더 깔끔합니다.

💡 call 메서드 덕분에 클래스 인스턴스가 함수처럼 동작합니다.

💡 인스턴스 변수로 카운터, 캐시, 통계 등 다양한 정보를 저장할 수 있습니다.

💡 여러 메서드로 로직을 분리하여 복잡한 데코레이터도 관리하기 쉽습니다.

💡 functools.update_wrapper를 사용하여 원본 함수의 메타데이터를 복사하는 것을 잊지 마세요(다음 카드에서 설명).


6. functools.wraps 활용

시작하며

여러분이 데코레이터를 적용한 함수의 이름을 확인했는데, "wrapper"라고 나온다면 당황스럽지 않으신가요? 디버깅할 때 함수 이름이나 docstring이 사라지면 큰 문제입니다.

데코레이터를 사용하면 원본 함수가 wrapper 함수로 대체되기 때문에, 함수의 메타데이터(이름, docstring, 모듈 등)가 손실됩니다. help() 함수로 문서를 확인하거나, 로그에 함수 이름을 남길 때 혼란이 생깁니다.

functools.wraps는 이 문제를 해결합니다. 원본 함수의 메타데이터를 wrapper 함수에 복사하여, 데코레이터를 사용해도 함수의 정체성이 유지됩니다.

개요

간단히 말해서, functools.wraps는 데코레이터 안의 wrapper 함수를 꾸며주는 데코레이터입니다. 메타데이터를 보존하는 역할을 합니다.

실무에서는 함수의 메타데이터가 매우 중요합니다. API 문서가 자동 생성되거나, 로깅 시스템이 함수 이름을 사용하거나, IDE가 타입 힌트를 보여줄 때 메타데이터가 필요합니다.

wraps를 사용하지 않으면 이런 도구들이 제대로 동작하지 않습니다. 기존에는 데코레이터를 쓰면 원본 함수의 정보가 사라졌다면, 이제는 @wraps(func) 한 줄만 추가하면 모든 것이 해결됩니다.

name, doc, module, annotations 등이 자동으로 복사됩니다. 핵심은 @wraps(func)를 wrapper 함수에 적용하는 것입니다.

이것만으로 원본 함수의 모든 메타데이터가 보존되며, 디버깅과 문서화가 훨씬 쉬워집니다.

코드 예제

from functools import wraps

def my_decorator(func):
    @wraps(func)  # 원본 함수의 메타데이터를 wrapper에 복사
    def wrapper(*args, **kwargs):
        """Wrapper 함수의 docstring"""
        print(f"{func.__name__} 함수 실행 중...")
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def calculate(x, y):
    """두 수를 더하는 함수입니다."""
    return x + y

# 메타데이터 확인
print(f"함수 이름: {calculate.__name__}")  # calculate
print(f"Docstring: {calculate.__doc__}")   # 두 수를 더하는...
print(calculate(3, 4))

설명

이것이 하는 일: @wraps(func)는 wrapper 함수에 원본 함수의 name, doc, module 같은 속성들을 복사합니다. 첫 번째로, from functools import wraps로 wraps를 임포트합니다.

이것은 Python 표준 라이브러리에 포함된 유틸리티입니다. wraps 자체도 데코레이터이며, 원본 함수의 정보를 복사하는 역할을 합니다.

그 다음으로, @wraps(func)를 wrapper 함수 위에 적용합니다. 이것은 wrapper = wraps(func)(wrapper)와 동일합니다.

wraps(func)는 func의 메타데이터를 복사하는 데코레이터를 반환하고, 이것이 wrapper에 적용됩니다. 결과적으로 wrapper.__name__이 "wrapper" 대신 "calculate"가 됩니다.

wraps가 없다면 calculate.__name__은 "wrapper"가 되고, calculate.__doc__은 "Wrapper 함수의 docstring"이 됩니다. 하지만 @wraps(func)를 사용하면 calculate.__name__은 "calculate", calculate.__doc__은 "두 수를 더하는 함수입니다."가 됩니다.

원본 함수의 정체성이 유지되는 것입니다. 여러분이 이것을 사용하면 여러 이점이 있습니다.

help(calculate)를 호출하면 원본 docstring이 표시되고, 로그에 함수 이름이 정확히 기록되며, IDE의 자동완성과 타입 체크가 제대로 작동합니다. 특히 FastAPI나 Flask 같은 프레임워크는 함수의 메타데이터를 사용해 문서를 생성하므로, wraps를 빼먹으면 API 문서가 엉망이 됩니다.

데코레이터를 만들 때 @wraps(func)를 항상 포함시키는 습관을 들이세요.

실전 팁

💡 데코레이터를 만들 때 항상 @wraps(func)를 사용하세요. 이것은 사실상 필수입니다.

💡 wraps는 name, doc, module, qualname, annotations, __dict__를 복사합니다.

💡 디버깅 시 함수 이름이 정확해야 스택 트레이스를 읽기 쉽습니다.

💡 API 문서 자동 생성 도구들은 함수의 docstring을 사용하므로 wraps가 필수적입니다.

💡 클래스 기반 데코레이터에서는 functools.update_wrapper(self, func)를 __init__에서 호출하세요.


7. 실행 시간 측정 데코레이터

시작하며

여러분이 애플리케이션이 느리다는 보고를 받았습니다. 어떤 함수가 병목인지 찾으려면 각 함수의 실행 시간을 측정해야 합니다.

모든 함수에 time.time()을 일일이 추가하시겠습니까? 수동으로 시간을 측정하면 코드가 지저분해지고, 측정 코드를 깜빡 잊거나 나중에 제거하는 것도 번거롭습니다.

프로파일링이 끝나면 모든 코드를 다시 정리해야 합니다. 실행 시간 측정 데코레이터를 만들면 @measure_time 한 줄로 어떤 함수든 프로파일링할 수 있습니다.

필요할 때 추가하고, 끝나면 한 줄만 삭제하면 됩니다.

개요

간단히 말해서, 실행 시간 측정 데코레이터는 함수 실행 전후의 시간 차이를 계산하여 출력합니다. 실무에서 성능 최적화는 항상 측정부터 시작됩니다.

추측만으로는 진짜 병목을 찾을 수 없습니다. 시간 측정 데코레이터를 주요 함수들에 적용하면, 어디서 시간이 소요되는지 한눈에 파악할 수 있습니다.

기존에는 각 함수마다 start_time = time.time()과 end_time = time.time()을 작성했다면, 이제는 데코레이터 하나로 모든 함수를 통일된 방식으로 측정할 수 있습니다. 측정 형식도 일관되게 유지됩니다.

핵심은 time.perf_counter()를 사용하는 것입니다. time.time()보다 정확하며, 함수 실행 전후의 값을 빼면 경과 시간이 나옵니다.

이를 로그로 출력하거나 저장할 수 있습니다.

코드 예제

import time
from functools import wraps

def measure_time(func):
    """함수 실행 시간을 측정하는 데코레이터"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()  # 시작 시간 기록

        result = func(*args, **kwargs)  # 함수 실행

        end = time.perf_counter()  # 종료 시간 기록
        elapsed = end - start  # 경과 시간 계산

        print(f"[{func.__name__}] 실행 시간: {elapsed:.4f}초")
        return result
    return wrapper

@measure_time
def slow_function():
    time.sleep(1)  # 1초 대기
    return "완료"

print(slow_function())

설명

이것이 하는 일: 이 데코레이터는 함수가 시작되기 직전의 시간과 종료된 직후의 시간을 기록하여, 실제 실행에 걸린 시간을 계산합니다. 첫 번째로, time.perf_counter()로 시작 시간을 기록합니다.

perf_counter()는 가장 정확한 시간 측정 함수로, 시스템 시간 변경의 영향을 받지 않습니다. time.time()은 시스템 시간을 반환하기 때문에, 시스템 시간이 변경되면 측정이 부정확해질 수 있습니다.

그 다음으로, 원본 함수를 실행하고 결과를 result 변수에 저장합니다. 이 부분이 실제로 시간을 측정하려는 코드입니다.

함수가 실행되는 동안 시간이 흐르고, 이것이 우리가 측정하려는 값입니다. 함수 실행이 끝나면 즉시 end = time.perf_counter()로 종료 시간을 기록합니다.

elapsed = end - start로 경과 시간을 초 단위로 계산합니다. 그리고 f-string의 {elapsed:.4f}로 소수점 4자리까지 출력합니다.

마지막으로 result를 반환하여 원본 함수의 동작을 유지합니다. 여러분이 이것을 사용하면 어떤 함수가 느린지 즉시 알 수 있습니다.

예를 들어 데이터베이스 쿼리 함수에 적용하면 쿼리 성능을 추적할 수 있고, API 호출 함수에 적용하면 네트워크 지연을 측정할 수 있습니다. 실무에서는 측정 결과를 파일에 저장하거나, 모니터링 시스템에 전송하여 지속적으로 성능을 추적합니다.

성능 개선 전후를 비교하는 데도 매우 유용합니다.

실전 팁

💡 time.perf_counter()는 time.time()보다 정확하고 안정적입니다.

💡 실무에서는 로그 레벨을 조절하여 개발 환경에서만 시간을 측정하도록 설정하세요.

💡 매우 빠른 함수는 여러 번 실행하여 평균을 내는 것이 더 정확합니다.

💡 시간 측정 자체도 약간의 오버헤드가 있으니, 프로덕션에서는 필요한 경우에만 사용하세요.

💡 데코레이터에 임계값을 추가하여 특정 시간 이상 걸릴 때만 경고를 출력할 수 있습니다.


8. 로깅 데코레이터

시작하며

여러분이 복잡한 버그를 디버깅하는데, 어떤 함수가 어떤 순서로 호출되는지 추적이 안 된다면 어떻게 하시겠습니까? print()를 여기저기 추가하다 보면 나중에 지우는 것도 일입니다.

수동 로깅은 일관성이 없고, 로그 포맷이 제각각이며, 나중에 정리하기도 어렵습니다. 특히 여러 개발자가 작업하면 로그 스타일이 통일되지 않습니다.

로깅 데코레이터를 사용하면 함수 호출, 인자, 반환값, 예외를 자동으로 기록할 수 있습니다. Python의 logging 모듈과 결합하면 로그 레벨, 포맷, 저장 위치까지 통합 관리할 수 있습니다.

개요

간단히 말해서, 로깅 데코레이터는 함수의 실행 흐름을 자동으로 기록하여 디버깅과 모니터링을 쉽게 만듭니다. 실무에서는 체계적인 로깅이 필수입니다.

프로덕션 환경에서는 print()를 사용할 수 없고, logging 모듈로 적절한 레벨(DEBUG, INFO, WARNING, ERROR)로 기록해야 합니다. 데코레이터를 사용하면 이런 로깅을 표준화할 수 있습니다.

기존에는 각 함수마다 logging.info()를 수동으로 추가했다면, 이제는 @log_calls 하나로 함수 호출, 인자, 결과를 자동으로 기록할 수 있습니다. 예외가 발생해도 자동으로 ERROR 레벨로 로깅됩니다.

핵심은 logging 모듈을 사용하는 것입니다. 로그 레벨을 설정하고, 포맷을 지정하고, 파일에 저장할 수 있습니다.

데코레이터는 이런 설정을 따르면서 일관된 형식으로 로그를 남깁니다.

코드 예제

import logging
from functools import wraps

# 로거 설정
logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

def log_calls(func):
    """함수 호출을 로깅하는 데코레이터"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        # 함수 호출 정보 로깅
        logger.debug(f"호출: {func.__name__}(args={args}, kwargs={kwargs})")

        try:
            result = func(*args, **kwargs)
            # 성공 로깅
            logger.debug(f"반환: {func.__name__} -> {result}")
            return result
        except Exception as e:
            # 예외 로깅
            logger.error(f"예외 발생: {func.__name__} - {e}", exc_info=True)
            raise  # 예외를 다시 발생시킴
    return wrapper

@log_calls
def divide(a, b):
    return a / b

print(divide(10, 2))
print(divide(10, 0))  # 예외 발생

설명

이것이 하는 일: 이 데코레이터는 Python의 logging 모듈을 사용하여 함수의 실행 흐름을 체계적으로 기록하고, 예외도 자동으로 포착합니다. 첫 번째로, logging.basicConfig()로 로거를 설정합니다.

level=logging.DEBUG는 DEBUG 이상의 모든 로그를 출력하라는 의미입니다. format에는 시간, 로그 레벨, 메시지를 포함시켰습니다.

logger = logging.getLogger(name)으로 현재 모듈의 로거를 가져옵니다. 그 다음으로, wrapper 함수 내부에서 logger.debug()로 함수 호출 정보를 기록합니다.

함수 이름과 전달된 인자를 포함시켜, 나중에 어떤 값으로 함수가 호출되었는지 확인할 수 있습니다. 이것은 DEBUG 레벨이므로 개발 중에만 보이고, 프로덕션에서는 숨길 수 있습니다.

try-except 블록으로 함수 실행을 감쌉니다. 정상 실행되면 반환값을 logger.debug()로 기록합니다.

하지만 예외가 발생하면 except 블록이 실행되고, logger.error()로 ERROR 레벨로 기록합니다. exc_info=True를 추가하면 스택 트레이스도 함께 기록되어 디버깅이 쉬워집니다.

마지막으로 raise로 예외를 다시 발생시켜, 호출자가 예외를 처리할 수 있게 합니다. 여러분이 divide(10, 2)를 호출하면 "호출: divide(args=(10, 2), kwargs={})"와 "반환: divide -> 5.0" 같은 로그가 출력됩니다.

divide(10, 0)을 호출하면 ZeroDivisionError가 발생하고, 자동으로 ERROR 레벨로 스택 트레이스와 함께 기록됩니다. 이런 로그들은 파일로 저장하여 나중에 분석할 수 있고, 프로덕션 환경에서 버그를 추적하는 데 매우 유용합니다.

실전 팁

💡 logging 모듈을 사용하면 로그 레벨을 조절하여 개발/프로덕션 환경을 구분할 수 있습니다.

💡 exc_info=True를 사용하면 스택 트레이스가 함께 기록되어 예외 원인을 쉽게 찾을 수 있습니다.

💡 민감한 정보(비밀번호, API 키 등)는 로그에 남기지 않도록 주의하세요.

💡 로그 파일 크기가 커지지 않도록 RotatingFileHandler를 사용하여 자동으로 로테이션하세요.

💡 실무에서는 로그를 중앙 집중식 시스템(ELK, Datadog 등)으로 전송하여 통합 모니터링합니다.


9. 인증 검사 데코레이터

시작하며

여러분이 웹 애플리케이션을 개발하는데, 특정 함수는 로그인한 사용자만 실행할 수 있어야 합니다. 모든 함수마다 "if not user.is_authenticated: raise Unauthorized" 같은 코드를 반복하시겠습니까?

인증 로직을 모든 함수에 복사하면 코드 중복이 심해지고, 인증 방식이 바뀌면 모든 곳을 수정해야 합니다. 보안과 관련된 코드는 일관성이 매우 중요한데, 수동으로 관리하면 실수하기 쉽습니다.

인증 검사 데코레이터를 만들면 @require_auth 한 줄로 함수를 보호할 수 있습니다. 인증 로직이 한 곳에 집중되어 유지보수가 쉽고, 빠뜨릴 위험도 줄어듭니다.

개요

간단히 말해서, 인증 검사 데코레이터는 함수 실행 전에 사용자의 권한을 확인하고, 권한이 없으면 예외를 발생시킵니다. 실무에서 웹 API나 웹 애플리케이션은 엔드포인트마다 다른 권한이 필요합니다.

일부는 누구나 접근 가능하고, 일부는 로그인 필요하며, 일부는 관리자만 가능합니다. 데코레이터를 사용하면 이런 권한 체계를 선언적으로 표현할 수 있습니다.

기존에는 각 함수 시작 부분에 권한 확인 코드를 작성했다면, 이제는 @require_auth, @require_admin 같은 데코레이터로 권한을 명시합니다. 코드를 읽는 사람이 함수의 권한 요구사항을 한눈에 알 수 있습니다.

핵심은 함수 실행 전에 조건을 검사하고, 조건이 맞지 않으면 예외를 발생시키는 것입니다. 조건이 맞으면 원본 함수를 실행하고, 아니면 실행조차 하지 않습니다.

코드 예제

from functools import wraps

class User:
    """사용자 클래스 예시"""
    def __init__(self, name, is_authenticated):
        self.name = name
        self.is_authenticated = is_authenticated

# 전역 변수로 현재 사용자 (실제로는 세션이나 컨텍스트에서 가져옴)
current_user = User("Guest", False)

def require_auth(func):
    """인증된 사용자만 실행 가능하도록 하는 데코레이터"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        if not current_user.is_authenticated:
            raise PermissionError(f"{func.__name__}은(는) 로그인이 필요합니다.")
        print(f"[인증 성공] {current_user.name}님이 {func.__name__} 실행")
        return func(*args, **kwargs)
    return wrapper

@require_auth
def view_profile():
    return "프로필 페이지"

# 인증되지 않은 사용자
try:
    view_profile()
except PermissionError as e:
    print(f"오류: {e}")

# 인증된 사용자
current_user = User("Alice", True)
print(view_profile())

설명

이것이 하는 일: 이 데코레이터는 함수가 실행되기 전에 사용자의 인증 상태를 검사하고, 인증되지 않았으면 PermissionError를 발생시켜 함수 실행을 막습니다. 첫 번째로, current_user 전역 변수를 사용하여 현재 사용자 정보를 저장합니다.

실제 웹 애플리케이션에서는 Flask의 g 객체나 FastAPI의 Depends, Django의 request.user처럼 프레임워크가 제공하는 컨텍스트를 사용합니다. 여기서는 간단히 전역 변수로 표현했습니다.

그 다음으로, wrapper 함수 내부에서 if not current_user.is_authenticated로 인증 상태를 확인합니다. is_authenticated가 False이면 PermissionError 예외를 발생시키고, 함수는 실행되지 않습니다.

이것이 핵심입니다. 권한이 없는 사용자는 함수에 접근조차 할 수 없습니다.

인증이 확인되면 성공 메시지를 출력하고 func(*args, **kwargs)로 원본 함수를 실행합니다. 사용자 이름과 함수 이름을 로그로 남겨서, 누가 어떤 기능을 사용했는지 추적할 수 있습니다.

이는 보안 감사(audit)에 중요합니다. 여러분이 이 패턴을 확장하면 더 복잡한 권한 체계를 만들 수 있습니다.

@require_role("admin")처럼 인자를 받아서 역할을 확인하거나, @require_permission("write")처럼 세밀한 권한을 체크할 수 있습니다. Flask, Django, FastAPI 같은 프레임워크들은 모두 이런 방식의 인증 데코레이터를 제공합니다.

여러 데코레이터를 중첩하여 @require_auth와 @require_admin을 함께 사용할 수도 있습니다.

실전 팁

💡 실제 웹 애플리케이션에서는 프레임워크의 컨텍스트 시스템을 사용하여 현재 사용자를 가져오세요.

💡 인증 실패 시 적절한 예외를 발생시켜 호출자가 처리할 수 있도록 하세요.

💡 인증 성공/실패를 로그로 남겨 보안 감사에 활용하세요.

💡 @require_role(role)처럼 인자를 받아 더 세밀한 권한 제어를 구현할 수 있습니다.

💡 여러 데코레이터를 중첩하여 인증, 권한, 로깅을 동시에 적용하세요.


10. 재시도 데코레이터

시작하며

여러분이 외부 API를 호출하는데, 가끔 네트워크 오류로 실패합니다. 매번 try-except로 감싸고 루프를 돌면서 재시도 로직을 작성하시겠습니까?

네트워크 호출, 데이터베이스 쿼리, 파일 I/O는 일시적인 오류가 발생할 수 있습니다. 모든 함수에 재시도 로직을 복사하면 코드가 복잡해지고, 재시도 횟수나 대기 시간을 바꾸려면 여러 곳을 수정해야 합니다.

재시도 데코레이터를 만들면 @retry(times=3, delay=1) 한 줄로 자동 재시도 기능을 추가할 수 있습니다. 일시적인 오류를 우아하게 처리하여 시스템의 안정성을 높일 수 있습니다.

개요

간단히 말해서, 재시도 데코레이터는 함수가 예외를 발생시키면 지정된 횟수만큼 자동으로 다시 실행합니다. 실무에서는 외부 시스템과의 통신이 불안정할 수 있습니다.

마이크로서비스 환경에서는 일시적인 네트워크 지연이나 서비스 재시작 때문에 요청이 실패할 수 있습니다. 재시도 로직을 추가하면 이런 일시적 오류를 자동으로 복구할 수 있습니다.

기존에는 각 API 호출마다 for 루프와 try-except를 작성했다면, 이제는 데코레이터로 통일된 재시도 전략을 적용할 수 있습니다. 재시도 횟수, 대기 시간, 재시도할 예외 타입까지 설정할 수 있습니다.

핵심은 for 루프로 지정된 횟수만큼 시도하고, 실패하면 time.sleep()으로 대기한 후 다시 시도하는 것입니다. 마지막 시도에서도 실패하면 예외를 그대로 발생시킵니다.

코드 예제

import time
from functools import wraps

def retry(times=3, delay=1, exceptions=(Exception,)):
    """실패 시 자동으로 재시도하는 데코레이터

    Args:
        times: 최대 시도 횟수
        delay: 재시도 간 대기 시간(초)
        exceptions: 재시도할 예외 타입들
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    if attempt == times:  # 마지막 시도
                        print(f"[실패] {times}번 시도 후 실패: {e}")
                        raise  # 예외를 다시 발생
                    print(f"[재시도 {attempt}/{times}] 오류: {e}, {delay}초 후 재시도...")
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(times=3, delay=1)
def unstable_api_call():
    """불안정한 API 시뮬레이션"""
    import random
    if random.random() < 0.7:  # 70% 확률로 실패
        raise ConnectionError("네트워크 오류")
    return "성공!"

print(unstable_api_call())

설명

이것이 하는 일: 이 데코레이터는 함수 실행 중 예외가 발생하면 지정된 횟수만큼 자동으로 재시도하고, 재시도 사이에 대기 시간을 둡니다. 첫 번째로, retry 함수는 times, delay, exceptions 세 개의 인자를 받습니다.

times는 최대 시도 횟수, delay는 재시도 간 대기 시간(초), exceptions는 재시도할 예외 타입의 튜플입니다. 기본값으로 모든 예외(Exception)를 재시도합니다.

이 값들은 클로저로 캡처되어 내부 함수들이 사용할 수 있습니다. 그 다음으로, wrapper 함수에서 for attempt in range(1, times + 1)로 지정된 횟수만큼 루프를 돕니다.

range(1, 4)는 1, 2, 3을 생성하므로 최대 3번 시도합니다. try 블록에서 func(*args, **kwargs)를 실행하고, 성공하면 즉시 return으로 결과를 반환하며 루프를 빠져나갑니다.

except 블록은 지정된 예외가 발생했을 때만 실행됩니다. if attempt == times로 마지막 시도인지 확인합니다.

마지막 시도에서도 실패했다면 더 이상 재시도하지 않고 raise로 예외를 다시 발생시킵니다. 호출자가 최종 실패를 알아야 하기 때문입니다.

마지막이 아니라면 재시도 메시지를 출력하고 time.sleep(delay)로 지정된 시간만큼 대기한 후 다음 루프로 넘어갑니다. 여러분이 unstable_api_call()을 실행하면, 70% 확률로 ConnectionError가 발생합니다.

데코레이터가 자동으로 1초 대기 후 재시도하고, 성공할 때까지 최대 3번 시도합니다. 실무에서는 외부 API 호출, 데이터베이스 연결, 파일 다운로드 같은 불안정한 작업에 이 패턴을 적용합니다.

exponential backoff(지수 백오프)를 구현하여 delay를 점점 늘리면 더욱 효과적입니다. 예를 들어 1초, 2초, 4초 순으로 대기하면 서버에 부담을 줄일 수 있습니다.

실전 팁

💡 모든 예외를 재시도하지 말고, 일시적인 오류(네트워크, 타임아웃 등)만 재시도하세요.

💡 재시도 간 대기 시간을 두어 서버에 부담을 주지 않도록 하세요.

💡 exponential backoff를 구현하면 재시도 간격을 점점 늘려 더 안정적입니다.

💡 무한 재시도는 위험합니다. 최대 횟수를 반드시 설정하세요.

💡 재시도 로그를 남겨 얼마나 자주 실패하는지 모니터링하고, 근본 원인을 해결하세요.


#Python#Decorators#Functions#AdvancedFeatures#Metaprogramming

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.
이전4/4
다음