포스트

[Python 100일 챌린지] Day 61 - 데코레이터 기초

[Python 100일 챌린지] Day 61 - 데코레이터 기초

데코레이터는 파이썬의 슈퍼파워예요! 🦸‍♂️

지금까지 만든 함수들, 일일이 로그 찍고 시간 재고 했죠? 데코레이터를 쓰면 단 한 줄(@timer)로 모든 함수에 자동 적용 가능해요! Flask, Django 같은 웹 프레임워크도 데코레이터 투성이랍니다. 😊

Phase 3에서 배운 함수가 기초라면, 오늘은 “함수를 다루는 함수”를 배워요. 처음엔 조금 어려울 수 있지만, 한 번 이해하면 코드가 훨씬 깔끔해집니다! 💪

(30분 완독 ⭐⭐⭐)

🎯 오늘의 학습 목표

📚 사전 지식


🎯 학습 목표 1: 데코레이터의 개념 이해하기

한 줄 설명

데코레이터 = 함수에 기능을 덧붙이는 포장지 📦

선물 상자에 리본 붙이는 것처럼, 함수에 기능을 덧붙여요!

1.1 데코레이터가 필요한 이유

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 상황: 여러 함수의 실행 시간을 측정하고 싶어요

# ❌ 나쁜 방법: 모든 함수에 일일이 코드 추가
def calculate1():
    start = time.time()
    # 원래 코드...
    result = sum(range(1000000))
    print(f"시간: {time.time() - start}")
    return result

def calculate2():
    start = time.time()
    # 원래 코드...
    result = sum(range(2000000))
    print(f"시간: {time.time() - start}")
    return result

# ✅ 좋은 방법: 데코레이터 한 번만 작성!
@timer  # 이 한 줄만 추가하면 끝!
def calculate1():
    return sum(range(1000000))

@timer  # 이 한 줄만 추가하면 끝!
def calculate2():
    return sum(range(2000000))

💡 실생활 비유: 스마트폰 케이스 같아요! 핸드폰(함수)을 바꾸지 않고, 케이스(데코레이터)만 씌워서 기능을 추가하죠.

1.2 함수는 일급 객체(First-Class Object)

파이썬에서는 함수를 변수처럼 다룰 수 있어요!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 함수를 변수에 담기
def greet(name):
    return f"Hello, {name}!"

say_hi = greet  # 함수를 변수에 저장!
print(say_hi("Alice"))  # Hello, Alice!

# 함수를 인자로 전달
def call_func(func, name):
    return func(name)

result = call_func(greet, "Bob")
print(result)  # Hello, Bob!

# 함수가 함수를 반환
def get_greeter():
    def greet(name):
        return f"Hi, {name}!"
    return greet  # 함수를 리턴!

greeter = get_greeter()
print(greeter("Charlie"))  # Hi, Charlie!

💡 핵심: 함수를 값처럼 다룰 수 있어야 데코레이터를 이해할 수 있어요!

1.3 클로저(Closure) 복습

데코레이터는 클로저를 활용해요.

1
2
3
4
5
6
7
8
9
10
def outer(x):
    """외부 함수"""
    def inner(y):
        """내부 함수 - outer의 x를 기억해요!"""
        return x + y
    return inner

add_5 = outer(5)  # x=5를 기억하는 함수 생성
print(add_5(3))   # 8 (5 + 3)
print(add_5(10))  # 15 (5 + 10)

💡 클로저: 함수가 만들어질 때의 환경(변수)을 기억하는 함수예요!


🎯 학습 목표 2: 함수 데코레이터 작성하기

2.1 기본 데코레이터

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def my_decorator(func):
    """가장 기본적인 데코레이터"""
    def wrapper():
        print("🎬 함수 실행 전")
        func()
        print("✅ 함수 실행 후")
    return wrapper

# 데코레이터 적용 방법 1: 수동으로
def say_hello():
    print("Hello!")

say_hello = my_decorator(say_hello)  # 포장하기
say_hello()
# 출력:
# 🎬 함수 실행 전
# Hello!
# ✅ 함수 실행 후

# 데코레이터 적용 방법 2: @ 문법 (권장!) ⭐
@my_decorator
def say_goodbye():
    print("Goodbye!")

say_goodbye()
# 출력:
# 🎬 함수 실행 전
# Goodbye!
# ✅ 함수 실행 후

💡 @ 문법: @my_decoratorfunc = my_decorator(func)와 같아요!

2.2 매개변수가 있는 함수 데코레이팅

실제 함수들은 대부분 매개변수가 있죠? 이렇게 해요:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def my_decorator(func):
    def wrapper(*args, **kwargs):  # 모든 인자를 받아요!
        print(f"📞 함수 호출: {func.__name__}")
        print(f"📝 인자: {args}, {kwargs}")

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

        print(f"💡 결과: {result}")
        return result
    return wrapper

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

@my_decorator
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

# 사용해보기
add(3, 5)
# 출력:
# 📞 함수 호출: add
# 📝 인자: (3, 5), {}
# 💡 결과: 8

greet("Alice", greeting="Hi")
# 출력:
# 📞 함수 호출: greet
# 📝 인자: ('Alice',), {'greeting': 'Hi'}
# 💡 결과: Hi, Alice!

💡 *args, **kwargs: 어떤 인자든 받을 수 있는 만능 패턴이에요!

2.3 실전: 실행 시간 측정 데코레이터

진짜 유용한 예제예요!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import time

def timer(func):
    """함수 실행 시간을 측정하는 데코레이터 ⏱️"""
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()

        print(f"⏱️  {func.__name__} 실행 시간: {end_time - start_time:.4f}")
        return result
    return wrapper

@timer
def slow_function():
    """시간이 걸리는 함수"""
    time.sleep(1)
    return "완료!"

@timer
def calculate_sum(n):
    """1부터 n까지의 합"""
    return sum(range(1, n + 1))

# 사용
slow_function()
# 출력: ⏱️  slow_function 실행 시간: 1.0012초

result = calculate_sum(1000000)
print(f"결과: {result}")
# 출력:
# ⏱️  calculate_sum 실행 시간: 0.0234초
# 결과: 500000500000

🎯 학습 목표 3: functools.wraps 사용하기

3.1 데코레이터의 문제점

1
2
3
4
5
6
7
8
9
10
11
12
13
def my_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def greet(name):
    """인사를 출력하는 함수"""
    return f"Hello, {name}!"

# 문제 발생! ❌
print(greet.__name__)  # wrapper (원래는 greet여야 해요)
print(greet.__doc__)   # None (docstring이 사라졌어요!)

💡 문제: 데코레이터가 함수의 정보(이름, 설명)를 덮어써버려요!

3.2 functools.wraps로 해결

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from functools import wraps

def my_decorator(func):
    @wraps(func)  # 이 한 줄이면 해결! ✨
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def greet(name):
    """인사를 출력하는 함수"""
    return f"Hello, {name}!"

# 해결! ✅
print(greet.__name__)  # greet (정상!)
print(greet.__doc__)   # 인사를 출력하는 함수 (정상!)

3.3 올바른 데코레이터 템플릿 📝

앞으로 데코레이터 만들 때는 항상 이 템플릿을 사용하세요!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from functools import wraps

def my_decorator(func):
    """데코레이터 설명"""
    @wraps(func)  # 필수!
    def wrapper(*args, **kwargs):
        # 함수 실행 전 작업
        print(f"[Before] {func.__name__} 호출")

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

        # 함수 실행 후 작업
        print(f"[After] {func.__name__} 완료")

        return result
    return wrapper

🎯 학습 목표 4: 실전 데코레이터 예제

예제 1: 로깅 데코레이터 📝

모든 함수 호출을 기록하고 싶을 때 사용해요!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from functools import wraps
import logging

logging.basicConfig(level=logging.INFO)

def log_function_call(func):
    """함수 호출을 로깅하는 데코레이터"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        logging.info(f"📞 함수 '{func.__name__}' 호출됨")
        logging.info(f"  📝 인자: {args}, {kwargs}")

        try:
            result = func(*args, **kwargs)
            logging.info(f"  ✅ 결과: {result}")
            return result
        except Exception as e:
            logging.error(f"  ❌ 오류 발생: {e}")
            raise

    return wrapper

@log_function_call
def divide(a, b):
    """나눗셈을 수행하는 함수"""
    return a / b

# 사용
divide(10, 2)
# INFO:root:📞 함수 'divide' 호출됨
# INFO:root:  📝 인자: (10, 2), {}
# INFO:root:  ✅ 결과: 5.0

divide(10, 0)
# INFO:root:📞 함수 'divide' 호출됨
# INFO:root:  📝 인자: (10, 0), {}
# ERROR:root:  ❌ 오류 발생: division by zero

예제 2: 재시도 데코레이터 🔄

네트워크 요청처럼 실패할 수 있는 작업에 유용해요!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from functools import wraps
import time

def retry(max_attempts=3, delay=1):
    """실패 시 재시도하는 데코레이터"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            attempts = 0

            while attempts < max_attempts:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    attempts += 1
                    if attempts >= max_attempts:
                        print(f"{max_attempts}번 시도 후 실패")
                        raise

                    print(f"⚠️  시도 {attempts} 실패: {e}")
                    print(f"   {delay}초 후 재시도...")
                    time.sleep(delay)

        return wrapper
    return decorator

@retry(max_attempts=3, delay=1)
def unreliable_function():
    """가끔 실패하는 함수 (API 호출 등)"""
    import random
    if random.random() < 0.7:
        raise Exception("일시적 오류 발생")
    return "성공!"

# 사용
result = unreliable_function()
print(f"✅ 결과: {result}")

예제 3: 캐시 데코레이터 💾

한 번 계산한 결과를 저장해서 재사용해요!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from functools import wraps

def memoize(func):
    """함수 결과를 캐싱하는 데코레이터"""
    cache = {}

    @wraps(func)
    def wrapper(*args):
        if args in cache:
            print(f"💾 캐시에서 가져옴: {args}")
            return cache[args]

        print(f"🔄 계산 중: {args}")
        result = func(*args)
        cache[args] = result
        return result

    return wrapper

@memoize
def fibonacci(n):
    """피보나치 수를 계산하는 함수"""
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# 사용
print(fibonacci(5))
# 출력:
# 🔄 계산 중: (5,)
# 🔄 계산 중: (4,)
# 🔄 계산 중: (3,)
# 🔄 계산 중: (2,)
# 🔄 계산 중: (1,)
# 🔄 계산 중: (0,)
# 💾 캐시에서 가져옴: (1,)
# 💾 캐시에서 가져옴: (2,)
# 💾 캐시에서 가져옴: (3,)
# 5

💡 효과: 피보나치(100)을 계산할 때, 캐시 없으면 몇 분 걸리는데, 캐시 있으면 1초도 안 걸려요!

예제 4: 권한 확인 데코레이터 🔐

웹 애플리케이션에서 자주 쓰는 패턴이에요!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from functools import wraps

# 현재 사용자 (실제로는 세션이나 DB에서 가져와요)
current_user = {"username": "alice", "role": "admin"}

def require_role(role):
    """특정 역할을 요구하는 데코레이터"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            if current_user.get("role") != role:
                raise PermissionError(
                    f"❌ 이 함수는 '{role}' 권한이 필요해요. "
                    f"현재 권한: {current_user.get('role')}"
                )
            return func(*args, **kwargs)
        return wrapper
    return decorator

@require_role("admin")
def delete_user(user_id):
    """사용자를 삭제하는 함수 (관리자 전용)"""
    return f"✅ 사용자 {user_id} 삭제됨"

@require_role("user")
def view_profile():
    """프로필 보기 (일반 사용자)"""
    return "프로필 정보"

# 사용
print(delete_user(123))  # ✅ 성공 (admin 권한)

# 권한 변경
current_user["role"] = "user"
print(delete_user(123))  # ❌ PermissionError 발생

💡 오늘의 핵심 요약

  1. 데코레이터 = 함수에 기능을 추가하는 포장지 📦
    • 원본 함수를 수정하지 않고 기능 추가
  2. 기본 구조 (템플릿):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    from functools import wraps
    
    def my_decorator(func):
        @wraps(func)  # 필수!
        def wrapper(*args, **kwargs):
            # 전처리
            result = func(*args, **kwargs)
            # 후처리
            return result
        return wrapper
    
  3. 실전 활용:
    • 🔍 로깅: 함수 호출 기록
    • ⏱️ 타이머: 실행 시간 측정
    • 💾 캐싱: 결과 저장
    • 🔐 권한 확인: 접근 제어
  4. 꼭 기억하세요:
    • 항상 @wraps(func) 사용해요!
    • *args, **kwargs로 모든 인자 처리해요!

🧪 연습 문제

문제 1: 디버그 데코레이터

함수의 입력과 출력을 보기 쉽게 출력하는 debug 데코레이터를 작성하세요.

1
2
3
4
5
6
7
8
9
@debug
def add(a, b):
    return a + b

add(3, 5)
# 출력:
# 🔍 함수: add
# 📥 입력: a=3, b=5
# 📤 출력: 8
💡 힌트

단계별 힌트:

  1. from functools import wraps 임포트
  2. inspect.signature()로 함수 시그니처 가져오기
  3. 입력 인자를 예쁘게 출력하기
  4. 함수 실행 후 결과 출력하기

체크 포인트:

  • @wraps(func) 사용했나요?
  • *args, **kwargs 처리했나요?
  • ✅ 이모지로 보기 좋게 꾸몄나요? 😊
정답 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from functools import wraps
import inspect

def debug(func):
    """함수의 입력과 출력을 출력하는 데코레이터"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        # 함수 시그니처 가져오기
        sig = inspect.signature(func)
        bound_args = sig.bind(*args, **kwargs)
        bound_args.apply_defaults()

        print(f"🔍 함수: {func.__name__}")
        print("📥 입력:", ", ".join(
            f"{k}={v}" for k, v in bound_args.arguments.items()
        ))

        result = func(*args, **kwargs)
        print(f"📤 출력: {result}")

        return result

    return wrapper

# 테스트
@debug
def add(a, b):
    return a + b

@debug
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

add(3, 5)
greet("Alice")

문제 2: 호출 횟수 제한 데코레이터

함수를 최대 N번만 호출할 수 있도록 제한하는 데코레이터를 작성하세요.

💡 힌트

단계별 힌트:

  1. count 변수로 호출 횟수 추적
  2. nonlocal count로 외부 변수 수정
  3. count >= max_calls면 예외 발생
  4. 매번 count 증가
정답 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from functools import wraps

def limit_calls(max_calls):
    """함수 호출 횟수를 제한하는 데코레이터"""
    def decorator(func):
        count = 0

        @wraps(func)
        def wrapper(*args, **kwargs):
            nonlocal count

            if count >= max_calls:
                raise Exception(
                    f"{func.__name__}는 최대 {max_calls}번만 호출 가능해요!"
                )

            count += 1
            print(f"📞 호출 횟수: {count}/{max_calls}")
            return func(*args, **kwargs)

        return wrapper
    return decorator

# 테스트
@limit_calls(3)
def say_hello():
    print("Hello!")

say_hello()  # 호출 1/3
say_hello()  # 호출 2/3
say_hello()  # 호출 3/3
say_hello()  # Exception 발생!

📚 이전 학습

Day 60: Phase 6 실전 프로젝트 - 웹 크롤러 ⭐⭐⭐

어제는 Phase 6 마무리 프로젝트로 웹 크롤러를 만들었어요!

📚 다음 학습

Day 62: 데코레이터 심화 ⭐⭐⭐

내일은 매개변수가 있는 데코레이터, 클래스 데코레이터, @property 등 고급 기법을 배워요!


“고급 개념도 차근차근 배우면 어렵지 않아요!” 🚀

Day 61/100 Phase 7: 고급 파이썬 개념 #100DaysOfPython
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.