본문 바로가기
👩🏻‍💻Computer Science/clean-code

Chapter5. 데코레이터를 사용한 코드 개선: 개념과 특징, 함수 데코레이터

by devhertz 2026. 8. 23.

Chapter 5. 데코레이터를 사용한 코드 개선

파이썬의 데코레이터

📌 목표

  • 파이썬에서 데코레이터가 동작하는 방식을 이해한다.
  • 함수와 클래스에 적용되는 데코레이터를 구현하는 방법을 배운다.
  • 일반적인 실수를 피하여 데코레이터를 효과적으로 구현하는 방법을 배운다.
  • 데코레이터를 활용한 코드 중복 회피 (DRY 원칙 준수)
  • 데코레이터를 활용한 관심사의 분리
  • 좋은 데코레이터 사례
  • 데코레이터가 좋은 선택이 될 수 있는 일반적인 상황, 관용구, 패턴

개념 및 원리

  • 파이썬에서 함수는 일반적인 객체이므로 변수에 할당하거나 파라미터로 전달할 수 있으며, 다른 함수에서 반환할 수도 있음.
  • 데코레이터: 함수를 인자로 받아서 새로운 함수를 반환하는 함수
    • 동작: 데코레이터는 기존 함수를 인자로 받아 내부에서 어떤 변환을 거친 후 수정된 새로운 형태의 함수를 반환하는 방식으로 동작함. 이는 수학의 합성함수 g(f(x))의 동작 방식과 유사.
    • 용어: 변형을 담당하는 함수데코레이터(decorator)라 하고, 데코레이터가 적용된 함수데코레이팅된(decorated) 함수 또는 래핑된(wrapped) 객체라고 부름.
    • 구문적 설탕 (Syntactic Sugar): 데코레이터 구문(@)은 데코레이터 기호 이후에 나오는 함수를 데코레이터의 첫 번째 파라미터로 전달하고 그 결과값을 반환하게 해주는 문법적 설탕일 뿐. 타이핑 수고를 덜고 코드를 읽기 쉽게 만들어 줌.
def original(...):
    ...

original = modifier(original)
  • 문제점
    • 함수를 동일한 이름으로 다시 할당하는 것에 주의.
    • 혼란스럽고 오류가 발생하기 쉽고 번거롭다. (재할당을 잊어버리거나, 함수 정의가 멀리 떨어져 있는 경우 특히 문제될 수 있음)
@modifier
def original(...):
    ...
  • 데코레이터는 데코레이터 이후에 나오는 것을 첫 번째 파라미터로 받고, 데코레이터의 결과 값을 반환하게 하는 문법적 설탕(Syntax Sugar)일 뿐
  • @modifier는 내부적으로 original = modifier(original)과 완전히 동일하게 동작함

특징

  • 가독성 향상
    • 데코레이터 도입 전에는 original = modifier(original)처럼 함수를 재할당해야 해서 함수 정의와 재할당 위치가 멀어질 경우 코드를 읽기 어렵게 만들고 오류를 유발했지만 데코레이터 도입 후에는 함수 정의 바로 위에 위치해 가독성을 크게 향상시킴.
  • 다양한 적용 범위와 활용
    • 함수뿐만 아니라 메서드, 제너레이터, 클래스 등 다양한 종류의 객체에 적용할 수 있고
    • 파라미터 유효성 검사, 사전조건 검사, 함수 결과 캐싱, 서명 변경, 기능 전체 재정의 등 어떠한 종류의 로직에도 적용할 수 있음.

예제1) 가장 단순한 형태: 함수를 인자로 받아 내부에서 새로운 함수를 정의하고 반환

def simple_logger(func):
    def wrapper():
        print("--- 함수 실행 시작 ---")
        func()  # 원본 함수 호출
        print("--- 함수 실행 종료 ---")
    return wrapper

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

say_hello()
  • @simple_logger 구문은 파이썬 내부적으로 say_hello = simple_logger(say_hello)로 치환되어 실행됨. 즉, 기존 함수가 wrapper 함수로 통째로 덮어씌워짐.

예제2) 동적 인자 처리 (*args, **kwargs): 실제 환경에서는 함수마다 파라미터 개수가 다르기 때문에, 데코레이터가 어떤 함수든 범용적으로 감쌀 수 있도록 가변 인자를 사용해야 함.

def trace_execution(func):
    def wrapper(*args, **kwargs):
        print(f"호출됨: {func.__name__}, 인자: {args}, {kwargs}")
        result = func(*args, **kwargs)
        print(f"결과: {result}")
        return result
    return wrapper

@trace_execution
def add_numbers(a, b):
    return a + b

add_numbers(3, b=5)
  • 전달받은 인자들을 언패킹하여 원본 func에 그대로 전달하고, 리턴값을 받아서 다시 클라이언트에게 돌려줌.

예제3) 메타데이터 보존(functools.wraps): 데코레이터를 씌우면 기존 함수의 이름(__name__)과 문서(__doc__)가 wrapper로 덮어씌워져 디버깅이 힘들어지는 치명적인 문제가 발생함. 이를 방지하기 위해 표준 라이브러리를 사용함.

from functools import wraps

def safe_decorator(func):
    @wraps(func)  # 원본 함수의 메타데이터를 wrapper로 복사함
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@safe_decorator
def process_data():
    """데이터를 처리하는 핵심 함수입니다."""
    pass

print(process_data.__name__)  # @wraps가 없으면 'wrapper'가 출력됨
print(process_data.__doc__)

예제4) 인자를 받는 데코레이터: 데코레이터 자체에 설정값(예: 재시도 횟수, 권한 레벨)을 동적으로 넘기고 싶다면 3중 중첩 함수를 만들어야 함.

from functools import wraps

def require_permission(role):
    # 실제 데코레이터를 반환하는 외부 함수
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            if role != "admin":
                raise PermissionError("관리자 권한이 필요함")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@require_permission(role="user")
def delete_database():
    print("데이터베이스 삭제 완료")

# delete_database() # 실행 시 PermissionError 발생
  • @require_permission(role="user")가 먼저 평가되어 decorator 함수를 반환하고, 반환된 decoratordelete_database 함수를 감싸는 순서로 동작함.

활용 패턴: API 응답 시간 측정

import time
import logging
from functools import wraps

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def time_it(func):
    @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
        logger.info(f"[{func.__name__}] 실행 시간: {execution_time:.4f}초")
        
        return result
    return wrapper

@time_it
def fetch_heavy_data():
    time.sleep(1.2)  # 무거운 I/O 작업 시뮬레이션
    return "데이터 로드 완료"

fetch_heavy_data()

 

✴️ 주의 사항:  파이썬의 데코레이터와 객체지향 설계의 "데코레이터 디자인 패턴"과 혼동하지 말 것.

 

결론

일반적으로 데코레이터 구문을 사용하지 않고 기존 함수를 다시 할당하는 방식은 피하도록 하자. 

특징 설명
일급 객체 기반 함수/클래스도 객체이므로 데코레이터로 감쌀 수 있음
문법적 설탕 @decoratorfunc = decorator(func)
관심사 분리 핵심 로직과 부가 기능(재시도, 로깅, 캐싱 등)을 분리
DRY 준수 반복되는 코드를 데코레이터로 한 곳에 모음
가독성 향상 함수 정의와 변형이 한 곳에 모여 있음
유연성 함수, 메서드, 제너레이터, 클래스 모두에 적용 가능
주의점 데코레이터 디자인 패턴과 혼동하지 말 것 / 재할당 방식은 가급적 피할 것

 

함수 데코레이터

  • 파이썬에서 데코레이터를 사용해 기능변경하는 가장 간단한 방법: 함수에 적용하는 것
  • 함수에 데코레이터를 사용하면 어떤 종류의 로직이라도 적용할 수 있음
    • 파라미터의 유효성 검사
    • 사전조건 검사
    • 기능 전체를 새롭게 정의
    • 시그니처 변경
    • 원래 함수의 결과 캐싱 등

예제) 특정 예외 발생 시 지정된 횟수만큼 로직을 재시도하게 만드는 데코레이터

import logging

logger = logging.getLogger(__name__)

class ControlledException(Exception):
    """도메인에서 발생하는 일반적인 예외"""

def retry(operation):
    @wraps(operation)
    def wrapped(*args, **kwargs):
        last_raised = None
        RETRIES_LIMIT = 3
        for _ in range(RETRIES_LIMIT):
            try:
                return operation(*args, **kwargs)
            except ControlledException as e:
                logger.info("%s 재시도", operation.__qualname__)
                last_raised = e
        raise last_raised
    return wrapped

@retry
def run_operation(task):
    """실행중 예외가 발생할 것으로 예상되는 특정 작업을 실행"""
    return task.run()
  • @retry 구문:  run_operation = retry(run_operation)을 실행하는 것과 동일한 역할수행
  • 이러한 방식을 통해 timeout 등의 예외 발생 시 재시도하는 로직을 기존 코드 수정 없이 간편하게 덧붙일 수 있음

 

댓글