functools.lru_cache는 Python에서 한 줄의 데코레이터만으로 반복 계산을 제거할 수 있기 때문에, 성능 개선을 목적으로 자주 선택되는 비교적 쉽게 적용할 수 있는 최적화 수단이다.
하지만 실제 실무에서는 단순히 "빠르게 만든다"는 이유만으로 적용했다가, 메모리 사용량 증가나 예상치 못한 정합성 문제를 겪는 경우도 적지 않다고 한다. 때문에 lru_cache 의 판단 기준에 대해서 학습한 내용을 이 글에 다루고자 한다.
lru_cache의 역할lru_cache는 Python 표준 라이브러리 functools에 포함된 데코레이터로, 함수 호출 결과를 메모리에 저장해 두었다가 동일한 입력이 다시 들어오면 계산을 생략하는 구조를 제공한다.
중요한 점은, 이것이 알고리즘을 바꾸는 최적화가 아니라는 점이다. 계산 자체는 동일하며, 단지 이미 계산된 결과를 재사용할 수 있을 때만 실행 경로를 단축한다.
따라서 lru_cache는 다음과 같은 선택을 코드에 명시적으로 추가하는 것과 같다.
같은 입력이 다시 들어올 가능성이 높고, 그때 계산을 다시 하는 비용보다 메모리에 결과를 보관하는 비용이 더 저렴하다.
이 전제가 성립하지 않으면, lru_cache는 기대한 효과를 내지 못한다.
lru_cache는 함수가 호출될 때, 전달된 모든 positional argument와 keyword argument를 묶어 하나의 key로 사용한다. 이 key는 내부적으로 dictionary의 key로 사용되므로, 다음 조건을 만족해야 한다.
이 때문에 list, dict, DataFrame과 같은 mutable 객체는 그대로 인자로 사용할 수 없다.
또한 인자의 값이 조금이라도 다르면, 논리적으로 같은 의미라 하더라도 완전히 다른 cache entry로 취급된다.
lru_cache는 함수의 반환값을 그대로 저장한다. 복사본을 만들거나, 약한 참조를 사용하는 것이 아니다.
즉, 반환 객체가 크다면:
DataFrame, 대형 numpy array, 복잡한 dict를 반환하는 함수에 lru_cache를 적용할 경우, 이는 곧 메모리 상주 객체 수를 늘리는 결정이 된다.
maxsize는 단순히 "최대 몇 개"를 의미하지 않는다. 이는 다음을 의미한다.
즉, 호출 패턴이 국소적(locality)을 가질수록 효과가 커지고, 호출 패턴이 넓게 퍼져 있을수록 eviction이 잦아진다.
호출이 다음과 같은 형태라면:
A → B → C → A → B → C
LRU는 매우 효과적으로 동작한다. 반면,
A → B → C → D → E → F → ...
와 같은 패턴에서는 cache는 거의 도움이 되지 않는다.
lru_cache를 써야 하는 가단순히 "여러 번 호출될 수도 있다"는 추측이 아니라, 코드 구조상 반복 호출이 발생할 수밖에 없는 경우가 있다.
예를 들어:
이러한 경우에는 cache hit 비율이 높게 유지될 가능성이 크다.
lru_cache는 cache miss 시에는 아무런 이득이 없다. 따라서 miss가 발생했을 때의 비용이 충분히 커야 한다.
대표적인 예는 다음과 같다.
이 경우 hit 한 번이 가져오는 이득이 매우 크기 때문에, 비교적 적은 hit 비율도 의미를 가질 수 있다.
lru_cache는 외부 상태 변화를 감지하지 않는다. 따라서 다음과 같은 전제가 필요하다.
이 조건이 충족되지 않는다면, cache 무효화 전략이 없는 lru_cache는 적절한 도구가 아니다.
파일, DB, 환경 변수 등 외부 상태에 의존하는 함수는 인자만으로 결과가 결정되지 않는다.
이 경우 lru_cache는 실제 상태와 불일치하는 값을 반환할 수 있으며, 이는 디버깅이 매우 어려운 버그로 이어진다.
DataFrame을 반환하는 함수에 cache를 적용할 때 가장 흔히 발생하는 문제다.
이 경우 cache는 최적화가 아니라 메모리 사용에 부담이 된다.
인스턴스 메서드에 lru_cache를 적용하면 self가 key에 포함된다. 이는 다음을 의미한다.
이 동작을 명확히 이해하지 않은 상태에서 적용하면, 메모리 사용량을 예측하기 어렵다.
이 조건들은 lru_cache가 기대한 대로 동작하기에 충분한 근거를 제공한다. 이론적으론 맞을 지 몰라도 실제로 cache hit이 발생하는지 확인하지 않으면 원래의 의도와 달리 메모리만 소비할 수 있다.때문에 hit 비율을 확인해야 한다.
hits / (hits + misses)
이 값이 충분히 높다면, 해당 cache는 구조적으로 의미가 있다.
월요일에 출근 하자 마자 .. 다시 log 찍어보는걸로!_!