사내에서 사용하는 함수 중 @redis_cache라는 함수가 있다. 함수의 FQCN과 input을 기억해놓고 캐싱하는 역할의 함수이다. 이 함수를 사용하는데, 자꾸만 TypeError가 발생했다. TypeError라고 해서 인자 타입이 잘못된 것인줄 알았는데 Python의 TypeError는 호출하려는 함수의 시그니처가 다를 경우 발생하기도 한다.
@redis_cache
def get_feeds(feed_pk: int) {
...
}
Python에서는 시그니처 검사시 타입을 검사하지 않고 인수의 수만 검사한다. 동적 언어기 때문에 식별자에 담긴 타입이 언제든지 변할 수 있고, 참조값을 저장하기 때문에 타입 검사가 의미 없기 때문이다. 실제로 feed_pk에서 타입 힌팅을 int로 주고 있지만 str 타입이 들어와도 무사히 돌아간다.
그렇다면 위 함수에서 문제는 무엇일까? 바로 @redis_cache를 선언하기만 했지, 호출하지 않았기 때문이다. 다음과 같이 변경했더니 무사히 통과했다.
나는 Spring AOP를 사용할 때 처럼 너무 당연하게 함수를 호출하든 안 하든 동일하게 동작할 것이라 예상했는데 아니었다. 이 문제를 해결하면서 Python Decorator가 어떻게 동작하는지 알아봤고, Python Closure도 함께 공부했다.
Spring AOP과 Python Decorator
Spring AOP는 프록시 방식의 AOP로 구현되어 있다. Spring에서 원하는 객체를 사용할 때 Bean Context에서 Bean을 꺼내다 쓰는데, 이때 Bean을 꺼낼 때 프록시가 적용된 Bean을 호출하도록 한다.
Python의 Decorator도 비슷하다. 위 함수에서 get_feeds()를 호출하면 get_feeds()를 바로 호출할 것 같으나, 사실은 redis_cache()의 local 함수의 내부함수를 먼저 호출하고 그 내부에서 get_feeds()를 호출하는 식으로 동작한다. (그래서 오류가 발생했을 때도 traceback에 redis_cache.<local>.wrapper() 이런 식으로 메시지가 찍혀 있었다)
# 출처: https://medium.com/@dmytro.ch/pythons-decorators-vs-java-s-annotations-same-thing-2b1ef12e4dc5
def cached(func):
def wrapper(*args, **kwargs):
global cached_item
if func.__name__ not in cached_items:
cached_items[func.__name__] = func(*args, **kwargs)
return cached_items[func.__name__]
return wrapper
@cached
def intensive_task():
time.sleep(1.0)
return 10
근데 위 코드에서는 함수 호출해서 넘기지 않았는데?
사실 사내에서 사용하는 redis_cache의 내부에는 이런 식으로 코드가 되어 있다. generate_cache_key(func, args, kwargs, ...)를 보면 func를 넘기고 있다.
자바 개발할 때는 보기 힘든 문법이다. 위에서 TypeError가 발생한 이유는 내부에 cache_decorator, func_wrapper 이런식으로 내부 함수들이 존재하기 때문이다.
이걸 이해하려면 일급 객체 개념과 클로저에 대해 이해해야 한다. 파이썬의 모든 것은 객체라고 여겨지는데, 만약 a = redis_cache 이런 식으로 담으면 redis_cache라는 객체를 담은 것이다. 그 객체를 호출해서 함수처럼 사용할 수 있다. 이때 호출되는 것은 내부에 선언된 __call__ 함수이다.
이런 식으로 __call__이 정의된 객체를 Callable Object라고 부른다. 이러한 특성은 Closoure라는 것에서 비롯된다. 자바 사용할 때 내가 이해한 클로저는, 데이터를 Stack이 아닌 Heap에 담아서 다른 Scope에서도 공유해서 사용할 수 있도록 하는 것이었다.
위키백과에서는 일급 객체 언어에서 lexical scope에 네이밍 바인딩을하도록 만드는 기술이라고 하는데, lexical binding이 runtime stack에서 독립적으로 만드는 기술이라고 한다
너무 어렵게 말한 것 같은데, 쉽게 말하자면 상태를 heap에 저장해 놓고 참조 해제 전까지는 언제든지 가져다 쓸 수 있도록 만드는 기술이라고 생각할 수 있을 것 같다
이 특성으로 인해 함수를 만들어만 놓고 나중에 호출할 수 있다. 위에 있던 문제가 발생했던 코드 구조를 다시 보면 이렇다
함수 내부에 함수가 있고, 또 함수가 있는 구조다. 만약 @redis_cache 이렇게만 선언하고 호출하지 않았으면 redis_cache(foo) 이런 식으로 넘어가게 되는 것이다. 따라서 함수를 호출해서 내부에 있는 함수를 호출해주도록 해야 한다
위 사진에서 real force!가 찍히지 않은 이유는? 저 내부에서 함수 호출을 하지 않았기 때문이다. func을 받았지만 호출하지 않았기 때문이다
마치면서
위 문제 때문에 2시간 넘게 씨름한 것 같은데, 덕분에 예전 JS할 때 경험이 없었더라면 이해하지 못해 훨씬 힘들었을 것 같다. 그래도 이번 기회에 Python Callable Object라던가, decorator 원리 같은 것들을 공부했고 동적 언어에 대해 조금 더 이해할 수 있는 계기가 되었다. 아직은 자바가 훨씬 편하지만... 내공을 빨리 쌓아서 파이써닉한 코드를 짜고 싶다!