Home

Gabriel's Blog

<맨 위로>

파이썬 데코레이터를 이용한 캐시 구현

개요

웹 크롤링 관련 프로그램을 만들면서 디버깅 과정에서 서버 api에 데이터를 반복해서 요청하곤 한다. 이러다 ip 밴 당하는 게 아닐까 싶을 정도로 수십번 같은 데이터를 요청할 때마다 마음이 아프기 때문에 이를 해결할 방법을 찾아봤다.


파이썬에 캐시 기능이 있다고?

다음 코드를 보자.

from functools import lru_cache

@lru_cache(maxsize=128)
def fibo(n):
    if n < 2:
        return n
    return fibo(n - 1) + fibo(n - 2)

print(fibo(1000))

@lru_cache()라는 데코레이터를 추가해 준 것 만으로도 자동으로 캐싱의 되는 것이다. 짱 신기하네.

이름에서도 볼 수 있듯 LRU방식으로, 가장 오래 사용하지 않은 부분을 교체하도록 설계되어 있다. 최대 캐시 사이즈인 maxsizeNone으로 설정하면 제한 없이 저장하기도 한다.

일반적으로 캐시를 사용하기 위한 제한 조건과 권장 사항이 몇 가지 있다.

제한 조건으로,

  • 같은 인자 구성에서 항상 같은 리턴값이 나올 것
  • 캐시 결과를 검색하는 시간이 함수 실행 시간보다 짧을 것

그리고 권장 사항으로,

  • 함수의 단일 실행 시간이 오래 걸릴 것
  • 함수가 자주 호출될 것

하지만 어림도 없지

내가 원하는 기능은 맞지만 외부 파일로 저장할 방법이 없었다. 프로그램 끝나면 그걸로 땡. 캐시를 외부에 저장하고 자동으로 동기화하는 기능을 직접 만들기로 했다. 느리긴 하지만.

아래는 완성한 코드다.

from functools import wraps
import pickle
import os

def cached(func):
    func.cache = loadCache(func)
    @wraps(func)
    def wrapper(*args):
        try:
            tmp = func.cache[args]
            print("유효한 캐시가 존재합니다.")
            return tmp
        except KeyError:
            print("캐시 정보를 불러오지 못했습니다. api를 요청합니다.")
            func.cache[args] = result = func(*args)
            saveCache(func)
            return result   
    return wrapper

def saveCache(func):
    with open(f"Cache_{func.__name__}.pkl", 'wb') as f:
        pickle.dump(func.cache, f)

def loadCache(func):
    filename = f"Cache_{func.__name__}.pkl"
    if os.path.isfile(filename):
        with open(filename, 'rb') as f:
            return pickle.load(f)
    else:
        with open(filename, 'wb') as f:
            pickle.dump({}, f)
        return {}

@cached데코레이터로 적용할 수 있다. 간단히 원리를 설명하자면,

  • 데코레이터가 붙은 순간 해당 함수의 캐시 파일을 불러오고(Cache_함수명.pkl) 없으면 만든다.

  • 함수 호출시 캐시에 없는 인자일 경우 cache스태틱 변수에 실행결과 저장, 외부 캐시 파일과 동기화.

  • 캐시에 있는 인자일 경우 바로 리턴. 외부 캐시 파일과 동기화할 필요 X.

한편 딕셔너리의 저장을 위해 pickle 모듈을 사용했다. 그런데 캐시 동기화를 수행할 때마다 파일을 새로 덮어쓰기 때문에 파일 크기가 커질수록 느려질 가능성이 있다. 이후 관련 이슈가 발생하면 txt파일로 저장하던지 해서 해결할 수 있을 듯하다.


결론

앞으로 api 요청을 하면서 자주 쓰게 될 것 같다. 사실 이 용도 외에는 쓸데 없기도 하지만…

그럼 다음에,
Gabriel-Dropout at 10:03

scribble