포스트

[Python 100일 챌린지] Day 62 - 데코레이터 심화

[Python 100일 챌린지] Day 62 - 데코레이터 심화

데코레이터를 한 단계 업그레이드해봐요! 🚀

어제 배운 기본 데코레이터, 이제 설정값을 받아서 동작을 조정하거나(@repeat(3)), 클래스로 구현하거나, 여러 개를 동시에 적용할 수 있어요! Django의 @login_required, Flask의 @app.route("/home") 같은 실무 데코레이터들이 바로 이런 고급 기법을 쓰고 있답니다. 😊

오늘 배우면 진짜 프로처럼 데코레이터를 다룰 수 있어요!

(35분 완독 ⭐⭐⭐)

🎯 오늘의 학습 목표

📚 사전 지식


🎯 학습 목표 1: 매개변수가 있는 데코레이터 작성하기

한 줄 설명

매개변수가 있는 데코레이터 = 설정 가능한 포장지 🎁⚙️

일반 데코레이터가 고정된 포장지라면, 매개변수가 있는 데코레이터는 “리본 3개 달기”, “리본 5개 달기” 같은 옵션을 줄 수 있어요!

1.1 왜 필요할까요?

1
2
3
4
5
6
7
8
# 같은 기능, 다른 설정
@repeat(times=3)
def say_hello():
    print("Hello!")

@repeat(times=5)
def say_goodbye():
    print("Goodbye!")

똑같은 반복 기능을 쓰지만, 횟수만 다르게 설정할 수 있어요! 🎯

💡 실생활 비유: 커피 주문할 때 “샷 추가”를 선택하는 것과 비슷해요. 같은 커피(함수)지만 설정(매개변수)만 달라지는 거죠!

1.2 3단계 구조 이해하기

매개변수가 있는 데코레이터는 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
from functools import wraps

def repeat(times):
    """함수를 N번 반복 실행하는 데코레이터"""
    # 1단계: 매개변수를 받는 함수
    def decorator(func):
        # 2단계: 원본 함수를 받는 함수
        @wraps(func)
        def wrapper(*args, **kwargs):
            # 3단계: 실제 실행되는 함수
            for i in range(times):
                print(f"[{i+1}/{times}]", end=" ")
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def greet(name):
    print(f"Hello, {name}!")

greet("Alice")
# 출력:
# [1/3] Hello, Alice!
# [2/3] Hello, Alice!
# [3/3] Hello, Alice!

왜 3단계일까요? 🤔

  • 1단계: times 같은 설정값을 먼저 받아요
  • 2단계: func(원본 함수)를 받아요
  • 3단계: 실제로 함수를 실행해요

1.3 템플릿 외워두기

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

def decorator_with_params(param1, param2):
    """매개변수가 있는 데코레이터 템플릿"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # param1, param2 사용 가능
            print(f"매개변수: {param1}, {param2}")
            result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@decorator_with_params("value1", "value2")
def my_function():
    print("함수 실행!")

이 템플릿만 외워두면 어떤 매개변수든 받을 수 있어요! 📝

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
from functools import wraps
import os

def run_if_env(env_var, expected_value):
    """환경 변수가 특정 값일 때만 함수 실행"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            actual_value = os.getenv(env_var)

            if actual_value == expected_value:
                print(f"{env_var}={actual_value}, 함수 실행")
                return func(*args, **kwargs)
            else:
                print(f"⏭️  {env_var}={actual_value}, 함수 건너뜀")
                return None

        return wrapper
    return decorator

@run_if_env("ENVIRONMENT", "production")
def send_email():
    print("📧 이메일 전송!")

# 테스트
os.environ["ENVIRONMENT"] = "development"
send_email()  # 건너뜀

os.environ["ENVIRONMENT"] = "production"
send_email()  # 실행됨

실무에서 이렇게 써요! 개발 환경에서는 이메일 안 보내고, 운영 환경에서만 보내게 할 때 유용해요! 🎯

1.5 실전 예제: 속도 제한 데코레이터

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
from functools import wraps
import time

def rate_limit(max_calls, period):
    """일정 시간 동안 최대 호출 횟수를 제한"""
    def decorator(func):
        calls = []

        @wraps(func)
        def wrapper(*args, **kwargs):
            now = time.time()

            # 오래된 호출 기록 제거
            calls[:] = [c for c in calls if now - c < period]

            if len(calls) >= max_calls:
                wait_time = period - (now - calls[0])
                print(f"{wait_time:.1f}초 후 다시 시도하세요")
                time.sleep(wait_time)

            calls.append(now)
            return func(*args, **kwargs)

        return wrapper
    return decorator

@rate_limit(max_calls=3, period=5)
def api_call():
    print("🌐 API 호출!")

# 테스트
for i in range(5):
    print(f"\n호출 {i+1}:")
    api_call()

실무에서 이렇게 써요! API 요청을 5초에 3번으로 제한해서 서버 부하를 줄여요! 🚦


🎯 학습 목표 2: 클래스 데코레이터 이해하기

한 줄 설명

클래스 데코레이터 = 상태를 기억하는 포장지 🧠📦

함수로 만든 데코레이터는 간단하지만, 클래스로 만들면 상태(count, cache 등)를 더 쉽게 관리할 수 있어요!

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
from functools import wraps

class CountCalls:
    """함수 호출 횟수를 세는 데코레이터 (클래스 버전)"""
    def __init__(self, func):
        wraps(func)(self)
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"호출 횟수: {self.count}")
        return self.func(*args, **kwargs)

@CountCalls
def say_hello():
    print("Hello!")

say_hello()  # 호출 횟수: 1
say_hello()  # 호출 횟수: 2
say_hello()  # 호출 횟수: 3

print(f"{say_hello.count}번 호출됨")

핵심 포인트!

  • __init__: 함수를 받아서 저장해요
  • __call__: 함수처럼 호출될 때 실행돼요
  • self.count: 상태를 인스턴스 변수로 저장해요! 🔢

💡 언제 클래스를 쓸까요? 호출 횟수, 캐시, 통계 같은 상태를 기억해야 할 때 클래스가 훨씬 편해요!

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
from functools import wraps

class Retry:
    """재시도 데코레이터 (클래스 버전)"""
    def __init__(self, max_attempts=3, delay=1):
        self.max_attempts = max_attempts
        self.delay = delay

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(self.max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == self.max_attempts - 1:
                        raise
                    print(f"시도 {attempt + 1} 실패, {self.delay}초 후 재시도")
                    import time
                    time.sleep(self.delay)
        return wrapper

@Retry(max_attempts=3, delay=0.5)
def unreliable_function():
    import random
    if random.random() < 0.7:
        raise Exception("일시적 오류")
    return "성공!"

result = unreliable_function()
print(result)

실무에서 이렇게 써요! 네트워크 요청이나 DB 연결처럼 가끔 실패할 수 있는 작업에서 자동으로 재시도해요! 🔄

2.3 클래스에 데코레이터 적용하기

함수뿐만 아니라 클래스 전체에도 데코레이터를 적용할 수 있어요!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def singleton(cls):
    """싱글톤 패턴을 구현하는 클래스 데코레이터"""
    instances = {}

    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("데이터베이스 연결 초기화")
        self.connection = "db_connection"

# 테스트
db1 = Database()  # 데이터베이스 연결 초기화
db2 = Database()  # 출력 없음 (이미 생성됨)

print(db1 is db2)  # True (같은 인스턴스)

싱글톤이 뭐예요? 🤔

  • 클래스의 인스턴스를 딱 하나만 만들어요
  • DB 연결, 설정 관리 같은 곳에서 유용해요!

💡 실생활 비유: 나라에 대통령이 한 명인 것처럼, 프로그램에 DB 연결도 하나만 있으면 돼요!


🎯 학습 목표 3: 데코레이터 체이닝 활용하기

한 줄 설명

데코레이터 체이닝 = 여러 포장지를 겹겹이 씌우기 📦📦📦

한 함수에 여러 데코레이터를 동시에 적용할 수 있어요!

3.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
38
from functools import wraps
import time

def timer(func):
    """실행 시간 측정"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"⏱️  실행 시간: {end - start:.4f}")
        return result
    return wrapper

def logger(func):
    """함수 호출 로깅"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"📝 함수 '{func.__name__}' 호출됨")
        result = func(*args, **kwargs)
        print(f"📝 함수 '{func.__name__}' 완료")
        return result
    return wrapper

# 데코레이터 체이닝 (아래에서 위로 적용됨)
@timer
@logger
def calculate():
    print("계산 중...")
    time.sleep(0.5)
    return 42

result = calculate()
# 출력:
# 📝 함수 'calculate' 호출됨
# 계산 중...
# 📝 함수 'calculate' 완료
# ⏱️  실행 시간: 0.5012초

3.2 데코레이터 적용 순서 이해하기

1
2
3
4
5
6
7
8
9
# 이것은
@decorator1
@decorator2
@decorator3
def func():
    pass

# 다음과 같아요
func = decorator1(decorator2(decorator3(func)))

아래에서 위로 적용돼요! 양파 껍질 벗기듯이, 가장 안쪽(decorator3)부터 바깥쪽(decorator1)으로요! 🧅

3.3 실전 예제: API 엔드포인트

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 authenticate(func):
    """인증 확인"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("🔐 인증 확인 중...")
        # 실제로는 토큰 검증 등
        return func(*args, **kwargs)
    return wrapper

def validate_input(func):
    """입력 검증"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("✅ 입력 검증 중...")
        # 실제로는 데이터 검증
        return func(*args, **kwargs)
    return wrapper

def log_api_call(func):
    """API 호출 로깅"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"📊 API 호출: {func.__name__}")
        result = func(*args, **kwargs)
        print(f"📊 API 응답: {result}")
        return result
    return wrapper

@log_api_call
@authenticate
@validate_input
def create_user(username, email):
    """사용자 생성 API"""
    return {"username": username, "email": email, "id": 123}

# 사용
user = create_user("alice", "[email protected]")

실무에서 이렇게 써요! 웹 API에서 로깅 → 인증 → 입력 검증을 순차적으로 처리해요! 🎯


🎯 학습 목표 4: 내장 데코레이터 마스터하기

한 줄 설명

내장 데코레이터 = 파이썬이 제공하는 필수 도구 🛠️

직접 만들지 않아도, 파이썬이 미리 만들어둔 강력한 데코레이터들이 있어요!

4.1 @property - 메서드를 속성처럼!

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
40
class Person:
    def __init__(self, name, age):
        self._name = name
        self._age = age

    @property
    def name(self):
        """이름 가져오기"""
        return self._name

    @property
    def age(self):
        """나이 가져오기"""
        return self._age

    @age.setter
    def age(self, value):
        """나이 설정하기"""
        if value < 0:
            raise ValueError("나이는 0 이상이어야 합니다")
        self._age = value

    @property
    def description(self):
        """계산된 속성"""
        return f"{self.name}님은 {self.age}세입니다"

# 사용
person = Person("Alice", 25)

# 메서드처럼 ()를 사용하지 않아요!
print(person.name)         # Alice
print(person.age)          # 25
print(person.description)  # Alice님은 25세입니다

# setter 사용
person.age = 26
print(person.age)  # 26

person.age = -1  # ValueError 발생!

왜 @property를 쓸까요? 🤔

  • 메서드인데 속성처럼 쓸 수 있어요 (괄호 안 씀)
  • 값을 검증할 수 있어요 (setter)
  • 계산된 값을 속성처럼 제공해요 (description)

💡 실생활 비유: 자판기에서 커피 버튼만 누르면 내부에서 자동으로 커피를 만들어주는 것처럼, person.age만 써도 내부에서 getter 메서드가 실행돼요!

4.2 @staticmethod와 @classmethod

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
40
41
42
class MathUtils:
    pi = 3.14159

    def __init__(self, value):
        self.value = value

    # 인스턴스 메서드 (self 필요)
    def add(self, n):
        return self.value + n

    # 정적 메서드 (self, cls 불필요)
    @staticmethod
    def multiply(a, b):
        """단순 계산, 클래스/인스턴스와 무관"""
        return a * b

    # 클래스 메서드 (cls 필요)
    @classmethod
    def from_string(cls, s):
        """대체 생성자"""
        value = int(s)
        return cls(value)

    @classmethod
    def get_pi(cls):
        """클래스 속성 접근"""
        return cls.pi

# 사용
# 인스턴스 메서드
obj = MathUtils(10)
print(obj.add(5))  # 15

# 정적 메서드 (인스턴스 없이 호출 가능)
print(MathUtils.multiply(3, 4))  # 12

# 클래스 메서드 (대체 생성자)
obj2 = MathUtils.from_string("20")
print(obj2.value)  # 20

# 클래스 메서드 (클래스 속성 접근)
print(MathUtils.get_pi())  # 3.14159

언제 뭘 쓸까요? 📚

  • 인스턴스 메서드: 인스턴스 데이터(self.value) 필요할 때
  • @staticmethod: 클래스와 관계없는 유틸리티 함수
  • @classmethod: 대체 생성자나 클래스 속성 접근할 때

4.3 @staticmethod vs @classmethod 비교

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
class Pizza:
    def __init__(self, ingredients):
        self.ingredients = ingredients

    @staticmethod
    def mix_ingredients(x, y):
        """정적 메서드: 클래스와 무관한 유틸리티 함수"""
        return x + y

    @classmethod
    def margherita(cls):
        """클래스 메서드: 특정 타입의 인스턴스 생성"""
        return cls(["모짜렐라", "토마토"])

    @classmethod
    def prosciutto(cls):
        """클래스 메서드: 다른 특정 타입의 인스턴스 생성"""
        return cls(["모짜렐라", "토마토", ""])

# 사용
Pizza.mix_ingredients("치즈", "토마토")  # 정적 메서드

pizza1 = Pizza.margherita()  # 클래스 메서드
pizza2 = Pizza.prosciutto()  # 클래스 메서드

print(pizza1.ingredients)  # ['모짜렐라', '토마토']
print(pizza2.ingredients)  # ['모짜렐라', '토마토', '햄']

@classmethod가 유용한 이유! 🍕

  • 다양한 방법으로 인스턴스를 만들 수 있어요 (대체 생성자)
  • Pizza.margherita()처럼 의미가 명확해요!

4.4 @cached_property (Python 3.8+)

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
from functools import cached_property
import time

class DataProcessor:
    def __init__(self, data):
        self.data = data

    @cached_property
    def processed_data(self):
        """한 번만 계산되고 캐시됨"""
        print("⏳ 데이터 처리 중... (시간이 오래 걸림)")
        time.sleep(1)
        return [x * 2 for x in self.data]

# 사용
processor = DataProcessor([1, 2, 3, 4, 5])

# 첫 번째 접근: 계산 수행
print(processor.processed_data)
# 출력: ⏳ 데이터 처리 중... (시간이 오래 걸림)
#       [2, 4, 6, 8, 10]

# 두 번째 접근: 캐시된 값 반환 (즉시)
print(processor.processed_data)
# 출력: [2, 4, 6, 8, 10]

언제 쓸까요? 🚀

  • 계산이 오래 걸리지만 결과가 변하지 않을 때
  • 한 번만 계산하고 결과를 저장해요!

💡 실생활 비유: 처음엔 직접 커피를 내려서 시간이 걸리지만, 다음부턴 보온병에 담아둔 커피를 바로 마시는 거예요!


💡 오늘의 핵심 요약

1. 매개변수가 있는 데코레이터

1
2
3
4
5
6
7
8
def decorator(param):
    def wrapper(func):
        @wraps(func)
        def inner(*args, **kwargs):
            # param 사용 가능
            return func(*args, **kwargs)
        return inner
    return wrapper

2. 클래스 데코레이터

  • __init__: 함수나 매개변수 받기
  • __call__: 실제 래핑된 함수 실행
  • 상태를 기억해야 할 때 유용해요!

3. 데코레이터 체이닝

  • 여러 데코레이터 적용 가능해요
  • 아래에서 위로 적용돼요!

4. 내장 데코레이터

  • @property: 속성처럼 사용하는 메서드
  • @staticmethod: 클래스와 무관한 메서드
  • @classmethod: 클래스 정보가 필요한 메서드
  • @cached_property: 결과를 캐싱하는 속성

🧪 연습 문제

문제 1: 실행 횟수 제한 데코레이터

함수를 최대 N번까지만 실행할 수 있는 max_calls 데코레이터를 작성하세요.

1
2
3
4
5
6
7
8
@max_calls(3)
def greet():
    print("Hello!")

greet()  # 실행
greet()  # 실행
greet()  # 실행
greet()  # "최대 호출 횟수 초과" 메시지
💡 힌트

단계별 힌트:

  1. 3단계 중첩 함수 구조를 만들어요
  2. nonlocal 키워드로 호출 횟수를 기록해요
  3. 호출 전에 횟수를 확인하고, 초과하면 메시지만 출력해요

핵심 키워드: nonlocal count, count >= n, count += 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
from functools import wraps

def max_calls(n):
    """함수를 최대 n번까지만 실행"""
    def decorator(func):
        count = 0

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

            if count >= n:
                print(f"❌ 최대 호출 횟수({n}번) 초과")
                return None

            count += 1
            print(f"[{count}/{n}]", end=" ")
            return func(*args, **kwargs)

        return wrapper
    return decorator

# 테스트
@max_calls(3)
def greet():
    print("Hello!")

for _ in range(5):
    greet()

출력:

1
2
3
4
5
[1/3] Hello!
[2/3] Hello!
[3/3] Hello!
❌ 최대 호출 횟수(3번) 초과
❌ 최대 호출 횟수(3번) 초과

문제 2: 계산 속성을 가진 클래스

@property를 사용하여 원의 반지름으로부터 넓이와 둘레를 계산하는 Circle 클래스를 작성하세요.

💡 힌트

단계별 힌트:

  1. @propertyarea, circumference를 만들어요
  2. @radius.setter로 반지름 검증(음수 방지)을 추가해요
  3. 원의 넓이 = π × r², 둘레 = 2 × π × r

핵심 키워드: @property, @radius.setter, math.pi

정답 코드
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
import math

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("반지름은 0 이상이어야 합니다")
        self._radius = value

    @property
    def area(self):
        """원의 넓이"""
        return math.pi * self._radius ** 2

    @property
    def circumference(self):
        """원의 둘레"""
        return 2 * math.pi * self._radius

    def __repr__(self):
        return f"Circle(radius={self.radius})"

# 테스트
circle = Circle(5)
print(circle)                    # Circle(radius=5)
print(f"넓이: {circle.area:.2f}")        # 넓이: 78.54
print(f"둘레: {circle.circumference:.2f}")  # 둘레: 31.42

circle.radius = 10
print(f"넓이: {circle.area:.2f}")        # 넓이: 314.16

출력:

1
2
3
4
Circle(radius=5)
넓이: 78.54
둘레: 31.42
넓이: 314.16

📝 오늘 배운 내용 정리

  1. 매개변수가 있는 데코레이터: 3단계 중첩으로 설정값을 받아요
  2. 클래스 데코레이터: 상태를 기억할 수 있어요
  3. 데코레이터 체이닝: 여러 데코레이터를 겹겹이 적용해요
  4. 내장 데코레이터: @property, @staticmethod, @classmethod, @cached_property

🔗 관련 자료


📚 이전 학습

Day 61: 데코레이터 기초 ⭐⭐⭐

어제는 데코레이터의 기본 개념과 함수 래핑, @wraps 사용법을 배웠어요!

📚 다음 학습

Day 63: 제너레이터 기초 ⭐⭐⭐

내일은 메모리 효율적인 데이터 처리를 위한 제너레이터를 배워요!


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

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