💻 프로그래밍

Decorator

데코레이터

함수/클래스 동작을 수정하는 래퍼. Python @decorator 문법. 메타프로그래밍.

📖 상세 설명

데코레이터는 기존 함수나 클래스의 코드를 직접 수정하지 않고 새로운 기능을 추가하는 디자인 패턴입니다. 원본 코드를 감싸는(wrapping) 방식으로 동작하며, 로깅, 인증, 캐싱 등의 횡단 관심사(cross-cutting concerns)를 깔끔하게 분리할 수 있습니다.

Python에서 데코레이터는 @ 기호를 사용하는 문법적 설탕(syntactic sugar)으로, 함수를 인자로 받아 새로운 함수를 반환하는 고차 함수입니다. @decorator는 사실 func = decorator(func)와 동일한 의미이며, 2003년 PEP 318에서 도입되어 현재 Python의 핵심 기능이 되었습니다.

TypeScript에서 데코레이터는 클래스, 메서드, 프로퍼티, 파라미터에 메타데이터를 추가하거나 동작을 수정하는 특별한 선언입니다. 현재 TC39 Stage 3 제안 단계이며, Angular, NestJS 등 주요 프레임워크에서 의존성 주입과 라우팅에 필수적으로 사용됩니다.

실무에서 데코레이터는 AOP(관점 지향 프로그래밍)의 핵심 도구로, 비즈니스 로직과 부가 기능을 분리하여 코드 재사용성과 유지보수성을 크게 향상시킵니다. Flask의 @app.route, Django의 @login_required, NestJS의 @Controller 등이 대표적인 예시입니다.

💻 코드 예제

# 1. 기본 데코레이터 - 실행 시간 측정
import time
import functools

def timer(func):
    @functools.wraps(func)  # 원본 함수 메타데이터 보존
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__} 실행 시간: {end - start:.4f}초")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)
    return "완료"

slow_function()  # 출력: slow_function 실행 시간: 1.0012초

# 2. 인자를 받는 데코레이터 - 재시도 로직
def retry(max_attempts=3, delay=1):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts:
                        raise
                    print(f"시도 {attempt} 실패, {delay}초 후 재시도...")
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(max_attempts=3, delay=2)
def fetch_data(url):
    # API 호출 로직
    pass

# 3. 클래스 데코레이터 - 싱글톤 패턴
def singleton(cls):
    instances = {}
    @functools.wraps(cls)
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

@singleton
class Database:
    def __init__(self):
        print("DB 연결 생성")

db1 = Database()  # 출력: DB 연결 생성
db2 = Database()  # 출력 없음 (같은 인스턴스 반환)
print(db1 is db2)  # True

🗣️ 실무에서 이렇게 말하세요

💬 회의에서
"API 엔드포인트마다 인증 로직이 중복되고 있어요. @login_required 데코레이터를 만들어서 공통으로 적용하면 코드 중복을 줄이고 보안 정책 변경 시 한 곳만 수정하면 됩니다."
💬 면접에서
"데코레이터 패턴은 OCP(개방-폐쇄 원칙)을 잘 구현하는 방법입니다. 기존 함수를 수정하지 않고 확장할 수 있고, Python에서는 functools.wraps를 사용해 원본 함수의 __name__, __doc__ 같은 메타데이터를 보존해야 디버깅이 쉬워집니다."
💬 코드 리뷰에서
"이 데코레이터에서 functools.wraps가 빠져있네요. 이렇게 되면 decorated 함수의 __name__이 'wrapper'로 나와서 로깅이나 디버깅할 때 헷갈릴 수 있어요. @functools.wraps(func) 추가해주세요."

⚠️ 흔한 실수 & 주의사항

functools.wraps 누락

@functools.wraps(func)를 빠뜨리면 원본 함수의 __name__, __doc__, __annotations__이 손실됩니다. 디버깅 시 함수 이름이 모두 'wrapper'로 나타나 추적이 어려워집니다.

데코레이터 순서 착각

여러 데코레이터가 쌓이면 아래에서 위로 적용됩니다. @a @b @c def func()는 a(b(c(func)))와 같습니다. 순서에 따라 동작이 달라질 수 있으니 의도한 순서인지 확인하세요.

인자 있는 데코레이터는 3중 중첩

@decorator(arg)처럼 인자를 받으려면 함수를 한 단계 더 감싸야 합니다. decorator(arg) -> actual_decorator(func) -> wrapper(*args) 구조로 3중 중첩이 필요합니다.

🔗 관련 용어

📚 더 배우기