Home

Gabriel's Blog

<맨 위로>

파이썬 데코레이터를 이용한 유효기간 캐시 구현(2)

개요

이전에 api 요청 부담을 줄이기 위한 캐시 구현에 관한 글을 썼다. 원래 함수를 해치지 않고도 효율적으로 캐시를 구현하면서 외부에 저장도 할 수 있는 방법이었다.

그런데, Youtube API 관련한 작업을 하면서 이 캐시를 적용했을 때 오류가 발생했다.

원인은 리스폰스의 유효기한 때문이었다.


분석

streamingData{
    expiresInSeconds: 21540,
    formats: [
		{
			itag: 17
			url: "https://rr3---sn-3u-20nr.googlevideo.com/videoplayback?expire=1641911009..."
		},
	]
}

대충 이런 식으로 expiresInSeconds는 요청 시각 기준 남은 시간이, url 안에는 쿼리스트링으로 expire값에 만료 일자가 적혀 있었다. 일단 쿼리스트링을 분석하는 것보단 expiresInSeconds를 참조하는 게 더 쉬우므로 저 값을 이용해 만료일자를 계산하면 된다.

즉,

import time
expire = time.time() + int(data['streamingData']['expiresInSeconds'])-30

같은 식이다. 30초는 매너상 빼 줬다.


고민

생각해봐야 할 게 있다. 원함수의 리턴값으로부터 expire를 계산하는 식 은 함수에 의존하므로 cache 데코레이터를 정의하는 부분에 들어가면 안 된다. Nested Function으로 정의하는 건 어떨까? 예를 들면,

#원함수
@cached
def getInfo():
    def expire(raw):
        return raw['expireTime']
    return {'expireTime':100}

이고,

#캐싱
def cached(func):
    func.cache = {}
    @wraps(func)
    def wrapper(*args):
        tmp = func.cache.get(args)
        if tmp != None and tmp[0]>time.time():
            print("유효한 캐시가 존재합니다.")
            return tmp[1]
        else:
            print("캐시 정보를 불러오지 못했습니다. api를 요청합니다.")
            ret = func(*args)
            expire = func.expire(ret) #Here!
            func.cache[args] = (expire, ret)
            saveCache(func)
            return ret
    return wrapper

같은 식이다. (가독성을 위해 캐시를 외부에 저장하는 부분은 뺐다)

그러나 중첩 함수를 이와 같은 방식으로 사용할 수 없다는 사실을 깨닫고 큰 좌절에 빠졌다.

#원함수
@cached
def getInfo():
    def expire(raw):
        return raw['expireTime']
    getInfo.expire = expire #내부 함수를 스태틱 변수와 연결하는 코드
    return {'expireTime':100}
getInfo() #1회 호출을 통해 실제로 연결

같은 식으로 가능하긴 하지만…편법에 불과하고 또 매우 지저분하다.


해결

결국 생각해낸 것은 데코레이터 인자로 람다 함수를 전달 하는 방법이었다. 만료 기한을 계산하는 함수가 람다 함수로 표현되어야 한다는 조건이 붙는다. 자바스크립트가 생각나는 대목이다.

백 마디 설명보다 결과 코드를 보자.

#원함수
@cached(lambda raw: time.time() + int(raw['streamingData']['expiresInSeconds'])-30)
def getInfo():
    pass
def cached(expireFunc): #인자를 전달하기 위해 한 번 더 감싼다
    def decorator(func):
        func.cache = {}
        @wraps(func)
        def wrapper(*args):
            tmp = func.cache.get(args)
            if tmp != None and tmp[0]>time.time():
                print("유효한 캐시가 존재합니다.")
                return tmp[1]
            else:
                print("캐시 정보를 불러오지 못했습니다. api를 요청합니다.")
                ret = func(*args)
                expire = expireFunc(ret) #Here!
                func.cache[args] = (expire, ret)
                return ret
        return wrapper
    return decorator

좀 난해할 수도 있겠다. 데코레이터 부분을 이렇게 분리해서 생각해 보면 쉽게 이해할 수 있다.

#원함수
decorator = cached(lambda raw: time.time() + int(raw['streamingData']['expiresInSeconds'])-30)
@decorator
def getInfo():
    pass

람다 함수가 getInfo() 위에 있는 바로 저 decorator 함수에만 귀속되어 있는 느낌이다. 짱 신기하다.


개선

생각해 보니 캐시를 저장할 때 만료 기간을 함께 저장하지 않아도 될 것 같다. 괜히 복잡하게 짰네.

#캐싱
def cached(expireFunc):
    def decorator(func):
        func.cache = loadCache(func)
        @wraps(func)
        def wrapper(*args):
            tmp = func.cache.get(args)
            if tmp != None and expireFunc(tmp)>time.time(): #유효기간은 여기서 바로 구해서 비교
                print("유효한 캐시가 존재합니다.")
                return tmp
            else:
                print("캐시 정보가 존재하지 않거나 유효하지 않습니다. api를 요청합니다.")
                func.cache[args] = ret = func(*args)
                saveCache(func)
                return ret #원함수 리턴값 그대로 저장
        return wrapper
    return decorator

이쪽이 훨씬 단순하자. 진짜 왜 저렇게 했었을까.


여담

아주 머리 아픈 문제였다. 과정에 비해 결과는 놀라울 정도로 아름답다. 짱 기쁘다. 아이 좋아.

참고로 클래스 내부 함수에 사용할 때 self를 캐시 키값으로 저장하면 큰일 난다. 이때는

#캐싱
def cached(expireFunc):
    def decorator(func):
        func.cache = loadCache(func)
        @wraps(func)
        def wrapper(self, *args): #self는 따로
            tmp = func.cache.get(args)
            if tmp != None and expireFunc(tmp)>time.time():
                print("유효한 캐시가 존재합니다.")
                return tmp
            else:
                print("캐시 정보가 존재하지 않거나 유효하지 않습니다. api를 요청합니다.")
                func.cache[args] = ret = func(self, *args) #self는 따로
                saveCache(func)
                return ret
        return wrapper
    return decorator

이렇게 *args로부터 self를 분리해 주면 된다. 실제로 사용할 땐 이렇게 했다. 한큐에 해결하는 방법은 아직 못 찾았다.

여담 2

나중에 클래스 데코레이터를 이용해서 구현하는 방법도 생각해 봐야겠다. 다들 잘 자요.

그럼 다음에,
Gabriel-Dropout at 17:23

scribble