본의 아니게 openai api 강의를 들으면서 파이썬을 공부하게 되었다. 본래 생각은 자바에서도 쓸 수 있는 restapi가 있을거라고 생각하며 강의를 샀는데 그게 아니었다.... 파이썬으로 된 라이브러리를 사용하는 형식이었다.
어쨋든 오늘은 강의를 들으면서 sqlite를 쓰는 부분에서 데코레이터 패턴을 사용해서 이 부분을 기록하려고 글을 작성하게 되었다.
먼저 데코레이터를 정의한 함수를 보면 아래와 같다.
def memoize_to_sqlite(filename: str = "cache.db"):
# 함수 내부 로직
def memoize(func):
def wrapped(*args):
# 함수 내부 로직
return result
return wrapped
return memoize
이 코드는 Python에서 데코레이터 패턴의 기본 구조를 나타낸다. 데코레이터는 일반적으로 함수를 수정하지 않고 추가 기능을 제공하고자 할 때 사용된다.
데코레이터의 구조를 자세히 살펴보자.
데코레이터 팩토리 (memoize_to_sqlite
):
memoize_to_sqlite
는 데코레이터 팩토리 함수이다. 이 함수는 데코레이터 함수인 memoize
를 반환한다. 데코레이터 팩토리는 필요한 설정(예: 데이터베이스 파일 이름)을 데코레이터에 전달할 수 있게 해준다.데코레이터 (memoize
):
memoize
는 실제 데코레이터 함수로, 데코레이션 대상이 되는 함수 func
를 인자로 받는다. 이 함수는 내부적으로 또 다른 함수 wrapped
를 정의하고 반환한다.래핑된 함수 (wrapped
):
wrapped
함수는 데코레이터가 적용된 원본 함수의 로직을 감싸는 래퍼(wrapper) 역할을 한다. wrapped
는 원본 함수 func
가 호출될 때 실행되며, *args
를 사용해 원본 함수에 전달된 모든 위치 인수를 받는다.위 데코레이터의 사용 예는 다음과 같다:
@memoize_to_sqlite(filename="embeddings.db")
@retry(
wait=wait_random_exponential(multiplier=1, max=30),
stop=stop_after_attempt(3),
retry=retry_if_exception_type(APIConnectionError) | retry_if_exception_type(APIError) | retry_if_exception_type(RateLimitError),
)
def get_embedding(text: str) -> List[float]:
# ... 기타 코드들 ...
return embeddings
여기서 @memoize_to_sqlite(filename="embeddings.db")
는 some_expensive_function
함수를 memoize_to_sqlite
로 데코레이트한다. 이렇게 하면 some_expensive_function
을 호출할 때마다 실제 함수 호출 대신 wrapped
함수가 호출되어 캐싱 로직 등 추가적인 기능이 수행된다.
데코레이터의 강력함은 기존 코드를 수정하지 않고도 함수의 기능을 확장할 수 있다는 데 있다. 그 결과, 코드의 재사용성과 유지보수성이 향상된다.
다시 돌아가서 위의 memoize_to_sqlite
를 호출하면 어떻게 내부의 memoize
가 호출되고 wrapped
가 호출되는지 알아보자.
*args
는 무엇인가먼저 wrapped
함수에서 쓰인 *args
가 무엇인지 알아봐야 한다. *args
는 파이썬에서 위치 인수를 가변적으로 받을 수 있게 하는 문법입니다. 함수에 얼마나 많은 인수가 전달될지 모를 때, 이들을 튜플로 묶어서 처리할 수 있다. wrapped
함수 내에서 *args
는 func
함수로 전달되는 모든 인수들을 받아들인다. 예를 들어, some_expensive_function(x, y)
함수가 있다면, 이 함수에 x
와 y
라는 인수가 전달될 때 wrapped
함수의 args
는 (x, y)
라는 튜플로 이들을 받는다.
memoize
함수는 데코레이터의 핵심 부분으로, 원본 함수 func
를 인자로 받는다. 이 함수는 내부적으로 wrapped
함수를 정의하고 이 wrapped
함수를 반환한다. return wrapped
라는 구문을 통해, memoize
데코레이터가 적용된 함수가 호출될 때 실제로 실행되는 것은 wrapped
함수이다.
데코레이터를 적용한 함수를 호출하면, 다음과 같은 일이 일어난다:
1. 원본 함수 func
를 memoize
에 전달한다.
2. memoize
는 wrapped
를 정의하고 반환한다.
3. 이제 원본 함수 대신 wrapped
함수가 func
의 자리를 차지하게 된다.
4. 데코레이터가 적용된 함수를 호출하면 사실상 wrapped
함수가 호출되며, 이 함수 내부에서 원본 함수 func
가 필요할 때만 호출된다.
@memoize_to_sqlite("cache.db")
def some_expensive_function(x):
return x * x
# 함수 호출
some_expensive_function(4)
위 예에서 some_expensive_function(4)
를 호출하면 실제로는 wrapped(4)
가 실행된다. wrapped
는 some_expensive_function
의 인수 4
를 받아 처리하고, 이 때의 로직에 따라 결과를 캐시하거나 캐시된 결과를 반환한다.
이런 식으로 데코레이터는 기존 함수의 기능을 변경하지 않으면서 추가적인 기능을 실행할 수 있는 강력한 수단을 제공한다. wrapped
는 실질적으로 원본 함수의 프록시 역할을 하며, 데코레이터는 이를 활용해 다양한 부가 기능을 구현할 수 있다.