본문 바로가기
Artificial Intelligence/60. Python

[PYTHON] *args와 **kwargs를 사용한 유연한 데코레이터 설계 방법 5가지와 실무 해결 차이

by Papa Martino V 2026. 4. 2.
728x90

*args와 **kwargs
*args와 **kwargs

 

 

안녕하세요, 여러분! 파이썬을 사용하다 보면 기존의 코드를 수정하지 않고 기능을 추가하거나 변경하고 싶은 순간이 있습니다. 이때 마법처럼 등장하는 것이 바로 데코레이터(Decorator)입니다. 데코레이터는 함수를 인자로 받아 다른 함수를 반환하는 고차 함수(Higher-Order Function)로, 코드의 재사용성을 높이고 깔끔하게 유지하는 데 매우 유용합니다. 하지만 단순히 기본 데코레이터를 만드는 법을 아는 것만으로는 부족할 때가 있습니다. 만약 데코레이터를 적용하려는 함수들이 각기 다른 개수의 인자를 받거나, 아예 인자를 받지 않거나, 키워드 인자만 받는 등 형태가 제각각이라면 어떻게 해야 할까요? 각 함수마다 다른 데코레이터를 만들어야 할까요? 아닙니다. 오늘 이 글에서는 파이썬의 강력한 기능인 가변 인자(*args, **kwargs)를 활용하여 어떤 형태의 함수에도 유연하게 대응할 수 있는 만능 데코레이터를 설계하는 방법을 상세히 알아보겠습니다. 5가지 실무 예제를 통해 즉시 적용 가능한 지식을 공유하고, 일반 데코레이터와의 차이점 및 문제 해결 방식을 표로 비교하여 명확하게 이해할 수 있도록 도와드리겠습니다.

1. 데코레이터의 기본 개념과 한계

먼저, 데코레이터가 무엇인지 아주 간단하게 복습하고 넘어갑시다. 데코레이터는 기본적으로 함수를 인자로 받아 새로운 함수(보통 내부 함수로 정의됨)를 반환하는 함수입니다.


def my_decorator(func):
    def wrapper():
        print("함수 호출 전")
        func()
        print("함수 호출 후")
    return wrapper

@my_decorator
def say_hello():
    print("안녕하세요!")

say_hello()

이 코드는 아주 잘 동작합니다. 출력은 다음과 같을 것입니다:


함수 호출 전
안녕하세요!
함수 호출 후
직면하는 문제

하지만 만약 say_hello 함수가 인자를 받아야 한다면 어떻게 될까요?


@my_decorator
def say_hello_to(name):
    print(f"{name}님, 안녕하세요!")

# say_hello_to("철수") # TypeError: wrapper() takes 0 positional arguments but 1 was given

오류가 발생합니다. @my_decoratorsay_hello_to 함수를 인자가 없는 wrapper 함수로 대체했기 때문입니다. say_hello_to("철수")를 호출하는 것은 실제로 wrapper("철수")를 호출하는 것과 같고, wrapper는 인자를 받지 않도록 정의되어 있습니다.

이것이 바로 일반적인 데코레이터의 한계입니다. 데코레이터가 적용될 함수의 서명(Signature, 인자의 개수와 종류)을 미리 알아야만 올바른 wrapper 함수를 정의할 수 있습니다. 수많은 다른 함수에 재사용하려면 이 문제를 해결해야 합니다.

2. *args**kwargs로 만능 Wrapper 만들기

이 문제를 해결하는 마법의 열쇠가 바로 *args**kwargs입니다.

  • *args (Positional Arguments): 함수가 받을 수 있는 임의의 개수의 위치 인자를 튜플 형태로 수집합니다.
  • **kwargs (Keyword Arguments): 함수가 받을 수 있는 임의의 개수의 키워드 인자를 딕셔너리 형태로 수집합니다.

이 두 가지를 wrapper 함수의 매개변수로 정의하고, 다시 원본 함수(func)를 호출할 때 언패킹(Unpacking)하여 넘겨주면, 어떤 형태의 인자라도 그대로 원본 함수에 전달할 수 있습니다.

유연한 데코레이터의 기본 구조는 다음과 같습니다:


def universal_decorator(func):
    def wrapper(*args, **kwargs):
        # 함수 호출 전 수행할 작업 (예: 로깅, 권한 확인 등)
        print("함수 호출 전 - universal wrapper")
        
        # *args와 **kwargs를 사용하여 원본 함수 호출
        result = func(*args, **kwargs)
        
        # 함수 호출 후 수행할 작업
        print("함수 호출 후 - universal wrapper")
        
        return result
    return wrapper

이제 *args는 함수에 전달된 모든 위치 인자를 튜플로 받고, **kwargs는 모든 키워드 인자를 딕셔너리로 받습니다. func(*args, **kwargs) 호출 시, *** 연산자는 이 튜플과 딕셔너리를 다시 개별 인자로 언패킹하여 원본 함수에 정확히 전달합니다.

3. 일반 데코레이터 vs *args/**kwargs 데코레이터 비교

두 방법의 차이점을 표로 정리하여 확실히 이해해 봅시다.

비교 항목 일반 데코레이터 *args/**kwargs 활용 데코레이터
인자 지원 범위 고정된 개수의 인자만 지원. 서명이 다르면 매번 새로운 데코레이터 필요. 어떠한 형태의 인자(개수, 종류 불문)도 유연하게 지원 가능.
재사용성 낮음. 특정 함수 그룹에만 한정됨. 매우 높음. 프로젝트 전반의 다양한 함수에 적용 가능.
코드 복잡도 단순함. 이해하기 쉬움. 가변 인자 개념 이해 필요. 언패킹 과정이 추가됨.
유지보수성 대상 함수의 서명이 변경되면 데코레이터도 수정해야 할 가능성 있음. 대상 함수의 서명 변경에 영향을 받지 않음. 데코레이터 논리에만 집중 가능.
문제 해결 방식 특정 서명에 맞춘 wrapper 정의 가변 인자로 수집 후 원본 함수에 그대로 전달(Proxy)
주요 사용 처 인자가 없거나 고정된 특정 유틸리티 함수 로깅, 성능 측정, 권한 확인, 트랜잭션 관리 등 범용적인 부가 기능 추가

4. 실무에 바로 적용 가능한 5가지 유연한 데코레이터 설계 예제

이제 *args**kwargs를 활용하여 실무에서 바로 사용할 수 있는 5가지 유연한 데코레이터 설계 예제를 살펴보겠습니다.

예제 1: 만능 로깅 데코레이터 (All-purpose Logging Decorator)

가장 흔하게 사용되는 예제입니다. 어떤 함수가 호출되었고, 어떤 인자가 전달되었으며, 결과는 무엇인지 로깅합니다.


import functools

def log_execution(func):
    @functools.wraps(func) # 원본 함수의 메타데이터 보존을 위해 필수!
    def wrapper(*args, **kwargs):
        print(f"[LOG] '{func.__name__}' 함수 호출 시작")
        print(f"[LOG] 위치 인자(args): {args}")
        print(f"[LOG] 키워드 인자(kwargs): {kwargs}")
        
        try:
            result = func(*args, **kwargs)
            print(f"[LOG] '{func.__name__}' 함수 호출 성공, 결과: {result}")
            return result
        except Exception as e:
            print(f"[LOG] '{func.__name__}' 함수 호출 실패, 에러: {e}")
            raise # 에러를 다시 발생시켜 상위에서 처리하도록 함
        finally:
            print(f"[LOG] '{func.__name__}' 함수 호출 종료")
    return wrapper

# 인자가 없는 함수에 적용
@log_execution
def simple_greet():
    return "안녕하세요!"

# 위치 인자가 있는 함수에 적용
@log_execution
def add_numbers(a, b, c=0):
    return a + b + c

# 키워드 인자가 있는 함수에 적용
@log_execution
def get_user_profile(user_id, **options):
    print(f"User ID: {user_id}, Options: {options}")
    return {"id": user_id, "options": options}

print("\n--- 호출 1 ---")
simple_greet()

print("\n--- 호출 2 ---")
add_numbers(10, 20)

print("\n--- 호출 3 ---")
get_user_profile("admin", theme="dark", lang="ko")

simple_greet()args=(), kwargs={}로 호출됩니다. add_numbers(10, 20)args=(10, 20), kwargs={}로 호출됩니다. get_user_profileargs=("admin",), kwargs={'theme': 'dark', 'lang': 'ko'}로 호출됩니다. 단 하나의 데코레이터로 이 모든 형태의 함수 호출을 완벽하게 로깅할 수 있습니다.

예제 2: 인자 유효성 검사 데코레이터 (Argument Validation Decorator)

특정 조건에 맞지 않는 인자가 전달될 경우 함수를 실행하기 전에 차단합니다.


import functools

def validate_non_negative(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # 모든 위치 인자 검사
        for arg in args:
            if isinstance(arg, (int, float)) and arg < 0:
                raise ValueError(f"'{func.__name__}'의 모든 인자는 양수여야 합니다. (전달된 값: {arg})")
        
        # 모든 키워드 인자 검사
        for key, value in kwargs.items():
            if isinstance(value, (int, float)) and value < 0:
                raise ValueError(f"'{func.__name__}'의 인자 '{key}'는 양수여야 합니다. (전달된 값: {value})")
                
        return func(*args, **kwargs)
    return wrapper

@validate_non_negative
def calculate_area(width, height):
    return width * height

@validate_non_negative
def set_temperature(temp, unit="C"):
    print(f"온도 설정: {temp}{unit}")

print(f"넓이: {calculate_area(10, 5)}")

# calculate_area(-10, 5) # ValueError 발생

print(f"넓이: {calculate_area(width=10, height=20)}")

# calculate_area(width=10, height=-20) # ValueError 발생

set_temperature(temp=25, unit="F")

# set_temperature(temp=-15, unit="F") # ValueError 발생

이 데코레이터는 *args**kwargs를 순회하며 숫자형 인자가 음수인지 확인합니다. 어떤 함수에 적용하든 숫자 인자가 음수라면 ValueError를 발생시켜 함수 본문이 실행되지 않도록 보호합니다.

예제 3: 성능 측정 데코레이터 (Performance Measure Decorator)

함수의 실행 시간을 측정하여 성능을 모니터링합니다. 웹 요청 처리나 무거운 연산 함수에 유용합니다.


import functools
import time

def timer_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.perf_counter()
        result = func(*args, **kwargs)
        end_time = time.perf_counter()
        execution_time = end_time - start_time
        print(f"'{func.__name__}' 실행 시간: {execution_time:.6f}초")
        return result
    return wrapper

@timer_decorator
def heavy_computation():
    sum = 0
    for i in range(1_000_000):
        sum += i
    return sum

@timer_decorator
def api_request(endpoint, payload=None):
    print(f"API 요청 중... {endpoint}")
    time.sleep(1) # API 요청 시뮬레이션
    return {"status": 200, "data": "dummy"}

heavy_computation()
api_request("/api/v1/users", payload={"id": 100})

각 함수에 `*args`와 `**kwargs`가 어떻게 전달되는지와 상관없이, 데코레이터는 시작 시간과 종료 시간을 기록하여 실행 시간을 계산하고 출력합니다.

예제 4: 트랜잭션 관리 데코레이터 (Transaction Management Decorator)

데이터베이스 작업과 같이 여러 단계로 이루어진 작업을 하나의 트랜잭션으로 묶어 처리합니다. 에러 발생 시 롤백(Rollback)을 수행합니다.


import functools

# 가상의 DB 연결 및 트랜잭션 함수
class DB:
    @staticmethod
    def connect():
        print("DB 연결")
    @staticmethod
    def begin():
        print("트랜잭션 시작")
    @staticmethod
    def commit():
        print("트랜잭션 커밋")
    @staticmethod
    def rollback():
        print("트랜잭션 롤백 (에러 발생)")
    @staticmethod
    def disconnect():
        print("DB 연결 해제")

def db_transactional(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        DB.connect()
        DB.begin()
        try:
            result = func(*args, **kwargs)
            DB.commit()
            return result
        except Exception as e:
            DB.rollback()
            raise e # 에러를 다시 발생시켜 상위로 전달
        finally:
            DB.disconnect()
    return wrapper

@db_transactional
def create_user(username, email, age):
    print(f"사용자 생성: {username}, {email}, {age}")
    # 에러 발생 시뮬레이션
    # if age < 0: raise ValueError("나이는 양수여야 합니다.")

@db_transactional
def update_profile(user_id, **profile_data):
    print(f"사용자 프로필 업데이트 ({user_id}): {profile_data}")

create_user("chulsoo", "chulsoo@example.com", 30)

print("\n")

update_profile("chulsoo", age=31, nickname="new_chulsoo")

# create_user("chulsoo", "chulsoo@example.com", -5) # 에러 발생 시뮬레이션

이 데코레이터는 데이터베이스 작업을 수행하는 모든 함수에 적용할 수 있습니다. create_user는 위치 인자 3개를, update_profile은 위치 인자 1개와 키워드 인자 2개를 받지만, 데코레이터는 *args**kwargs를 통해 원본 함수에 이를 그대로 전달하고 트랜잭션 관리에만 집중합니다.

예제 5: 지연 실행 데코레이터 (Lazy Execution Decorator)

함수의 실행을 요청 즉시 수행하지 않고, 명시적으로 실행할 때까지 지연시킵니다. 인자가 준비될 때까지 함수를 실행하지 않으려는 경우 유용합니다.


import functools

class LazyResult:
    def __init__(self, func, *args, **kwargs):
        self.func = func
        self.args = args
        self.kwargs = kwargs
        self.result = None
        self.executed = False

    def get_result(self):
        if not self.executed:
            print(f"'{self.func.__name__}' 함수 실제 실행")
            self.result = self.func(*self.args, **self.kwargs)
            self.executed = True
        return self.result

def lazy_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"'{func.__name__}' 함수 실행 지연")
        # 함수를 실행하지 않고 LazyResult 객체를 반환
        return LazyResult(func, *args, **kwargs)
    return wrapper

@lazy_decorator
def multiply(a, b):
    return a * b

lazy_multiply_result = multiply(10, 5)
print("함수 호출은 했지만 아직 실행 안 됨")

print(f"실행 결과: {lazy_multiply_result.get_result()}")

이 예제는 multiply 함수의 실행을 지연시킵니다. 데코레이터는 LazyResult 객체를 반환하며, 이 객체는 나중에 호출될 때까지 실행되지 않습니다. LazyResult 객체는 원본 함수(func)와 호출 시점의 인자(args, kwargs)를 저장합니다. get_result() 메서드는 저장된 인자를 사용하여 함수를 실제 실행하고 결과를 반환합니다. 이 데코레이터는 어떤 형태의 함수에도 적용할 수 있습니다.

5. 핵심 요약 및 마무리

오늘 우리는 파이썬 데코레이터 설계에서 가장 중요한 개념 중 하나인 유연성(Flexibility)을 확보하는 방법을 상세히 배웠습니다. *args**kwargs를 활용하여 모든 형태의 함수 서명을 수용하는 만능 데코레이터를 설계하는 것은 실무 코드의 품질을 결정짓는 핵심 기술입니다.

주요 학습 포인트

  • 일반 데코레이터는 고정된 인자만 지원하여 재사용성이 낮습니다.
  • wrapper 함수에 *args**kwargs를 매개변수로 정의하면 모든 위치 인자와 키워드 인자를 수집할 수 있습니다.
  • 수집한 인자를 원본 함수에 다시 *** 연산자로 언패킹하여 전달하면, 어떤 형태의 함수도 유연하게 지원하는 만능 데코레이터가 됩니다.
  • 실무에서 로깅, 성능 측정, 인자 유효성 검사, 트랜잭션 관리, 지연 실행 등 범용적인 부가 기능 구현에 필수적입니다.
  • functools.wraps를 함께 사용하여 원본 함수의 메타데이터를 보존하는 것을 잊지 말아야 합니다.
728x90