본문 바로가기
👩🏻‍💻 Computer Science/clean-code(ft.python)

Chapter5. 고급 데코레이터(코루틴(coroutine)을 위한 데코레이터, 데코레이터를 위한 확장 구문)

by devhertz 2026. 9. 20.

코루틴(coroutine)을 위한 데코레이터

  • 코루틴(Coroutine): 비동기(Asynchronous) 작업을 위한 특별한 함수
    • 일반 함수는 호출되면 끝까지 실행된 뒤 종료되지만, 코루틴은 실행을 중간에 멈췄다가(await) 나중에 다시 이어서 실행할 수 있는 특별한 함수
    • 파이썬에서는 async def 키워드로 정의. 내부에서 await를 만나면 제어권을 양도하고 대기 상태로 들어가며, 그 동안 CPU나 이벤트 루프가 다른 작업을 처리할 수 있도록 함
      • 예시 상황) 쇼핑몰 서버에서 사용자 주문을 처리할 때, 외부 결제모듈 API 응답을 기다리는 n초 동안 멈춰있지 않고, 대기 시간 동안 다른 사용자의 주문이나 조회 요청을 처리함. 이로 인해 적은 자원으로도 수많은 요청을 동시에 처리할 수 있음.
def 일반함수():
    return "결과"

async def 코루틴함수():
    return "결과"
  • async def로 만든 함수를 호출하면 바로 실행되지 않고 대신 코루틴 객체가 생성된다.
  • 실제로 실행하려면 await를 붙여야 한다.
async def hello():
    print("안녕")
    return "완료"

# 이렇게 하면 실행 X
coro = hello()          # 코루틴 객체만 만들어짐
print(coro)             # <coroutine object hello at 0x...>

# 이렇게 해야 실행 O
import asyncio
result = asyncio.run(hello())   # 또는 다른 코루틴 안에서 await hello()
  • 코루틴을 위한 데코레이터
    • 일반 함수 데코레이터: def로 래핑하고 그냥 호출
    • 코루틴 데코레이터: async def로 래핑하고 await로 호출해야 함
  • 함수와 코루틴 모두 지원하고 싶을 때
    • 가장 이상적인 방법은 하나의 데코레이터로 둘 다 지원하는 것.
    • 이를 위해 디스패처 래퍼를 만들 수 있다(데코레이터를 위한 파사드를 만드는 것과 비슷).
      • 디스패처(Dispatcher): 입력된 대상이 함수인지 코루틴인지 판단해서 적절한 래퍼를 선택해주는 역할
      • 파사드(Façade): 복잡한 내부 구현을 숨기고 사용자에게 단순한 인터페이스(ex.@timing 하나로)를 제공하는 디자인 패턴

예제) 고정 값 사용

X, Y = 1, 2

def decorator(callable):
    """고정된 X와 Y 값으로 <callable> 호출"""
    @wraps(callable)
    def wrapped():
        return callable(X, Y)
    return wrapped

@decorator
def func(x, y):
    return x + y

@decorator
async def coro(x, y):
    return x + y
  • 파라미터를 무시하고 고정 값을 사용하기 때문에 일반 함수와 코루틴 둘 다 잘 동작 하지만 코루틴을 받았을 때 await 없이 호출하면 코루틴 객체만 생성되고 실제 실행은 되지 않음 (나중에 await coro()를 해야 결과가 나옴)

 

 

해결법) inspect 모듈을 이용한 동적 디스패치 (Dispatcher): 동기 함수와 코루틴을 모두 지원하는 실행 시간 측정 데코레이터

대상 함수가 일반 함수인지 코루틴인지 검사(inspect)해서 코루틴인 경우 내부에서 await를 사용하는 비동기 래퍼를 반환하는 방식

import time
import inspect
from functools import wraps

def timing(callable):
    @wraps(callable)
    def wrapped(*args, **kwargs):
        start = time.time()
        result = callable(*args, **kwargs)
        latency = time.time() - start
        return {"latency": latency, "result": result}

    @wraps(callable)
    async def wrapped_coro(*args, **kwargs):
        start = time.time()
        result = await callable(*args, **kwargs)    # 반드시 await
        latency = time.time() - start
        return {"latency": latency, "result": result}

    # 대상이 코루틴 함수인지 판별하여 분기
    if inspect.iscoroutinefunction(callable):
        return wrapped_coro
    return wrapped

# 사용 예    
@timing
def sync_task():
    time.sleep(0.5)
    return "동기 완료"

@timing
async def async_task():
    await asyncio.sleep(0.5)
    return "비동기 완료"

print(sync_task())
asyncio.run(async_task())
  • 왜 이렇게 해야 할까?
    • wrapped_coro가 없으면 두 가지 문제가 생긴다.
      • await 없이 코루틴을 호출하면 작업이 끝날 때까지 기다리지 않는다.
      • 반환값의 result 키에 실제 결과가 아니라 코루틴 객체가 들어간다.
      • → 나중에 await 없이 사용하면 에러 발생
    • 결과물로 실제 데이터가 아니라 실행되지 않은 코루틴 객체가 담기게 되어, 나중에 이 값을 사용하려고 할 때 에러남
결론

데코레이터에서 받은 객체(decorated object)는 같은 종류의 객체로 교체해야 한다.

  • 함수를 받았으면 → 함수로 반환
  • 코루틴을 받았으면 → 코루틴으로 반환

 

 

데코레이터를 위한 확장 구문

  • 파이썬 3.9 이전에는 @ 뒤에 올 수 있는 표현식(expression)의 범위가 매우 제한적이어서 복잡한 표현식을 데코레이터로 직접 사용할 수 없었다.
  • PEP 614 도입 이후 제약이 해제되면서 이제 모든 함수 표현식을 데코레이터 자리에 사용할 수 있게 됨.

 

활용 예시) 로그를 남기는 데코레이터를 람다로 바로 작성

def _log(f, *args, **kwargs):
    print(f"함수 이름: {f.__qualname__!r}, 파라미터: {args=} 와 {kwargs=}")
    return f(*args, **kwargs)

@(lambda f: lambda *args, **kwargs: _log(f, *args, **kwargs))
def func(x):
    return x + 1
>>> func(3)
함수 이름: 'func', 파라미터: args=(3,) 와 kwargs={}
  • 주의할 점
    • 코드를 짧게 줄일 수 있다는 장점이 있지만, 너무 복잡한 로직을 람다나 축약된 표현식으로 데코레이터 자리에 넣으면 가독성이 심각하게 떨어질 수 있다.
    • 데코레이터 표현식을 읽기 어렵다면, 차라리 두 개 이상의 일반 함수로 나누어 알기 쉽게 분리하는 것이 좋다.

댓글