Learning
레슨 9 / 11·3개 토픽

데코레이터와 컨텍스트 매니저

함수 데코레이터

데코레이터는 함수를 인자로 받아 새로운 함수를 반환하는 고차 함수입니다. @decorator 구문으로 기존 함수에 기능을 추가할 수 있습니다. functools.wraps를 사용하면 원래 함수의 메타데이터(이름, 독스트링 등)를 보존할 수 있습니다.

python
import functools
import time

# 기본 데코레이터
def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"{func.__name__} 실행 시간: {elapsed:.4f}초")
        return result
    return wrapper

@timer
def slow_function():
    """느린 작업을 시뮬레이션합니다."""
    time.sleep(1)
    return "완료"

result = slow_function()  # slow_function 실행 시간: 1.00xx초
print(slow_function.__name__)  # slow_function (wraps 덕분)

# 인자를 받는 데코레이터
def retry(max_attempts=3):
    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:
                    print(f"시도 {attempt}/{max_attempts} 실패: {e}")
            raise Exception(f"{max_attempts}회 시도 모두 실패")
        return wrapper
    return decorator

@retry(max_attempts=3)
def unstable_api_call():
    import random
    if random.random() < 0.7:
        raise ConnectionError("연결 실패")
    return "성공"

클래스 데코레이터: @property, @staticmethod, @classmethod

python
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("반지름은 양수여야 합니다")
        self._radius = value

    @property
    def area(self):
        import math
        return math.pi * self._radius ** 2

    @staticmethod
    def is_valid_radius(value):
        """인스턴스 없이 호출 가능"""
        return isinstance(value, (int, float)) and value > 0

    @classmethod
    def from_diameter(cls, diameter):
        """지름으로 Circle 인스턴스 생성"""
        return cls(diameter / 2)

c = Circle(5)
print(c.radius)          # 5 (property getter)
print(c.area)            # 78.5398... (계산된 속성)
c.radius = 10            # property setter 호출

print(Circle.is_valid_radius(-3))  # False (staticmethod)
c2 = Circle.from_diameter(20)      # classmethod로 생성
print(c2.radius)                   # 10.0

컨텍스트 매니저: with 문

컨텍스트 매니저는 리소스의 획득과 해제를 자동으로 관리합니다. with 문을 사용하면 예외가 발생해도 리소스가 안전하게 해제됩니다. __enter____exit__ 메서드를 구현하거나, contextlib.contextmanager 데코레이터를 사용할 수 있습니다.

python
# 기본 with 문 (파일)
with open("data.txt", "w") as f:
    f.write("Hello, World!")  # 블록 종료 시 자동 close

# 클래스 기반 컨텍스트 매니저
class DatabaseConnection:
    def __init__(self, host):
        self.host = host
        self.connected = False

    def __enter__(self):
        print(f"{self.host}에 연결")
        self.connected = True
        return self  # as 변수에 바인딩

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f"{self.host} 연결 해제")
        self.connected = False
        return False  # True면 예외 억제

with DatabaseConnection("localhost") as db:
    print(f"연결 상태: {db.connected}")  # True
# 블록 종료 → __exit__ 자동 호출

# contextlib 데코레이터 방식
from contextlib import contextmanager

@contextmanager
def temp_directory():
    import tempfile, shutil, os
    dirpath = tempfile.mkdtemp()
    print(f"임시 디렉토리 생성: {dirpath}")
    try:
        yield dirpath  # yield 값이 as 변수에 바인딩
    finally:
        shutil.rmtree(dirpath)
        print("임시 디렉토리 삭제 완료")

with temp_directory() as tmpdir:
    filepath = tmpdir + "/test.txt"
    with open(filepath, "w") as f:
        f.write("임시 파일")
  • @decorator — 함수를 감싸서 기능을 추가하는 구문
  • functools.wraps — 데코레이터 적용 시 원래 함수의 메타데이터 보존
  • @property — 메서드를 속성처럼 접근 가능하게 만듦
  • @staticmethod — 인스턴스/클래스에 의존하지 않는 유틸리티 메서드
  • @classmethod — 클래스 자체를 첫 인자로 받아 팩토리 메서드 등에 활용
  • __enter__ / __exit__ — 클래스 기반 컨텍스트 매니저의 핵심 메서드
  • contextlib.contextmanager — 제너레이터로 간편하게 컨텍스트 매니저 생성
💡

데코레이터를 중첩하면 아래에서 위로 적용됩니다. @a @b def f():f = a(b(f))와 같습니다. 실무에서는 로깅, 캐싱, 인증, 재시도 등에 데코레이터를 널리 사용합니다.