본문 바로가기

프로그래밍/Python

[Python] Decorator 입문 훑기

728x90
Python 데코레이터(decorator) 완전 가이드
Python · Decorator Guide

Python 데코레이터(decorator) 완전 가이드

이 글은 Python에서 자주 사용되는 데코레이터(decorator)를 이해하고, 직접 만들어서 활용할 수 있도록 개념부터 실전 예제까지 정리한 가이드입니다.

#python #decorator #함수형 #실전예제

1. 데코레이터란 무엇인가?

Python에서 데코레이터는 “함수나 메서드를 감싸서, 공통 동작을 추가해 주는 문법”입니다. 함수 정의 위에 @something 형태로 붙어있는 코드를 많이 보게 됩니다.

예를 들어:

@login_required
def my_page():
    ...

이런 코드는 실제로 my_page = login_required(my_page) 로 치환됩니다. 즉, my_page 함수는 이제 login_required가 감싼 새로운 함수가 됩니다.

2. 가장 단순한 데코레이터 만들어보기

“함수 시작/끝”을 출력하는 간단한 데코레이터를 만들어 봅시다.

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("➡ 함수 시작")
        result = func(*args, **kwargs)
        print("⬅ 함수 끝")
        return result
    return wrapper

@my_decorator
def hello():
    print("안녕!")

hello()

실행 순서는 다음과 같습니다.

  • @my_decoratorhello = my_decorator(hello) 로 대체
  • hello() 를 호출하면 실제로는 wrapper() 실행
  • wrapper 안에서 공통 동작 + 원래 함수(func) 호출

3. 왜 *args, **kwargs 를 쓸까?

데코레이터는 인자가 없는 함수, 인자가 여러 개인 함수, 키워드 인자를 쓰는 함수 등 다양한 형태를 모두 감쌀 수 있어야 합니다.

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

이렇게 정의해 두면 어떤 시그니처의 함수든 동일한 데코레이터를 재사용할 수 있습니다.

4. @wraps를 써야 하는 이유 (functools.wraps)

위에서 만든 데코레이터를 그대로 사용하면, hello.__name__"wrapper"로 나오는 문제점이 있습니다.

함수 이름, docstring 등 메타데이터를 보존하려면 functools.wraps를 사용하는 것이 좋습니다.

from functools import wraps

def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("➡ 함수 시작")
        result = func(*args, **kwargs)
        print("⬅ 함수 끝")
        return result
    return wrapper

이렇게 하면 hello.__name__, docstring 등의 정보가 원래 함수의 것을 그대로 유지합니다.

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

데코레이터 자체에도 옵션을 주고 싶다면, “데코레이터를 만드는 함수”를 한 번 더 감싸야 합니다.

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)
            print(f"[{level}] {func.__name__} 끝")
            return result
        return wrapper
    return decorator

@log("INFO")
def add(a, b):
    return a + b

add(3, 4)

@log("INFO") 를 쓰면 log("INFO")가 먼저 실행되어 데코레이터를 만들고, 그 데코레이터가 다시 원래 함수를 감싸는 구조입니다.

6. 자주 쓰이는 내장 데코레이터

6-1. @staticmethod

인스턴스와 무관한 유틸리티 메서드를 클래스 내부에 정의할 때 사용합니다.

class MathUtil:
    @staticmethod
    def add(a, b):
        return a + b

MathUtil.add(1, 2)

6-2. @classmethod

첫 번째 인자로 cls를 받아, 클래스 자체를 다루는 메서드를 만들 때 사용합니다.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @classmethod
    def from_string(cls, text):
        name, age = text.split(",")
        return cls(name, int(age))

p = Person.from_string("Alice,30")

6-3. @property

메서드를 “속성”처럼 사용할 수 있게 해주는 데코레이터입니다.

class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    @property
    def area(self):
        return self.width * self.height

rect = Rectangle(3, 4)
print(rect.area)  # 함수 호출이 아니라 속성 접근처럼 사용

6-4. @lru_cache

같은 인자로 반복 호출되는 함수의 결과를 캐싱하여 성능을 개선할 때 사용합니다.

from functools import lru_cache

@lru_cache(maxsize=128)
def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)

fib(30)

6-5. @dataclass

데이터를 담는 클래스를 간단하게 정의할 수 있게 해줍니다.

from dataclasses import dataclass

@dataclass
class User:
    name: str
    age: int

u = User("Alice", 30)
print(u)

7. 실전 패턴 예제

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

import time
from functools import wraps

def timeit(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} 실행 시간: {end - start:.4f}초")
        return result
    return wrapper

@timeit
def slow_func():
    time.sleep(1)

slow_func()

7-2. 로그인 체크 / 권한 체크

from functools import wraps

def login_required(func):
    @wraps(func)
    def wrapper(user, *args, **kwargs):
        if not user.get("is_authenticated"):
            print("로그인이 필요합니다.")
            return None
        return func(user, *args, **kwargs)
    return wrapper

@login_required
def my_page(user):
    print(f"{user['name']}님의 마이페이지입니다.")

my_page({"name": "홍길동", "is_authenticated": True})

8. 데코레이터 여러 개 동시에 사용하기

@dec1
@dec2
def func():
    ...

적용 순서는:

func = dec1(dec2(func))

아래에 있는 데코레이터가 먼저 적용된다는 점을 기억해 두면 좋습니다.

9. 데코레이터 사용 시 주의할 점

  • @wraps를 써서 원래 함수의 이름/메타데이터를 보존하기
  • 너무 많은 책임을 데코레이터 한 곳에 몰지 않기 (가독성 저하)
  • 상태를 가지는 복잡한 데코레이터는 클래스나 클로저 설계를 신중히 하기
데코레이터는 코드 재사용성과 가독성을 높이는 강력한 도구입니다. 공통 패턴이 자꾸 반복된다고 느껴진다면, “이걸 데코레이터로 빼볼 수 있을까?” 한 번 고민해 보는 습관을 추천합니다.