메뉴 닫기

파이썬 성능 최적화: 캐싱 기능을 완벽하게 활용하는 5가지 전략

파이썬 성능 최적화: 캐싱 기능을 완벽하게 활용하는 5가지 전략

💻 functools.lru_cache부터 cachetools까지, 파이썬 코드 가속화 비법

파이썬으로 개발을 하다 보면 처리 속도가 느려지는 병목 현상을 한 번쯤 경험하게 됩니다.

특히 데이터베이스 쿼리, 복잡한 계산, 외부 API 호출처럼 시간이 오래 걸리는 작업을 반복해야 할 때 성능 문제는 더욱 심각해지죠.

이런 문제를 해결하고 코드의 실행 속도를 획기적으로 높일 수 있는 가장 강력한 방법 중 하나가 바로 ‘캐싱(Caching)’입니다.

캐싱은 이미 계산된 결과를 임시 저장해두었다가 재사용함으로써 불필요한 재계산을 피하게 해줍니다.

함수 호출 한 번만으로 성능이 수십 배 향상되는 마법 같은 경험을 하게 될 수도 있습니다.

이번 글에서는 파이썬 개발자들이 가장 많이 사용하는 내장 캐싱 도구인 functools의 lru_cache부터, 더 세밀한 제어가 가능한 외부 라이브러리 cachetools의 활용법까지 캐싱 전략 전반을 깊이 있게 다뤄보겠습니다.

단순히 캐시를 적용하는 것을 넘어, 캐시 무효화나 키 정규화와 같은 고급 기법까지 숙지하여 파이썬 코드를 최고 성능으로 가속화하는 방법을 익혀보시길 바랍니다.



🚀 파이썬 성능 가속의 기본, functools.lru_cache 활용하기

파이썬에서 캐싱을 시작하는 가장 쉽고 강력한 방법은 바로 functools 모듈의 lru_cache 데코레이터를 사용하는 것입니다.

이 데코레이터는 함수의 결과를 저장하고, 동일한 인수가 들어왔을 때 함수를 다시 실행하는 대신 저장된 결과를 즉시 반환해줍니다.

lru_cache는 “Least Recently Used” 알고리즘을 사용하여 캐시 크기가 가득 찼을 때 가장 오랫동안 사용되지 않은 항목을 제거하며 메모리를 효율적으로 관리합니다.

이는 캐시의 크기를 무제한으로 늘리지 않으면서도 가장 자주 사용되는 데이터의 접근성을 높여줍니다.

@lru_cache 데코레이터 사용법과 maxsize 옵션

lru_cache를 적용하는 방법은 매우 간단합니다.

캐싱을 적용하고 싶은 함수 위에 데코레이터를 붙여주기만 하면 됩니다.

이때 가장 중요한 옵션은 maxsize입니다.

maxsize는 캐시에 저장할 수 있는 최대 항목 수를 지정하며, 이 값을 설정하지 않거나 None으로 설정하면 캐시 크기는 제한이 없어집니다.

일반적으로 캐시 크기는 메모리 사용량과 성능 향상 사이의 균형을 고려하여 적절히 지정하는 것이 중요합니다.

만약 maxsize를 3으로 설정하면, 최근에 호출된 3개의 고유한 인수 조합에 대한 결과만 저장됩니다.

CODE BLOCK
from functools import lru_cache
import time

# 캐시 크기를 128로 제한합니다. (기본값)
@lru_cache(maxsize=128)
def heavy_computation(n):
    # 실제로는 복잡한 계산이나 DB 쿼리가 들어가는 영역
    time.sleep(1) 
    return n * n

# 첫 번째 호출: 1초 소요
print(heavy_computation(5)) 

# 두 번째 호출: 캐시된 결과를 즉시 반환 (0초 소요)
print(heavy_computation(5)) 

# 새로운 인수로 호출: 1초 소요
print(heavy_computation(6)) 

# 캐시 통계 확인
print(heavy_computation.cache_info())
# CacheInfo(hits=1, misses=2, maxsize=128, currsize=2)

💡 TIP: lru_cache는 함수 인수가 해시 가능(hashable)해야만 작동합니다. 리스트(List)나 딕셔너리(Dictionary)처럼 변경 가능한(mutable) 인수를 직접 캐싱할 수는 없습니다. 불변(immutable) 인수인 튜플(Tuple) 등으로 변환하여 사용하는 것이 일반적인 해결책입니다.

캐시 통계 확인 및 무효화

lru_cache가 적용된 함수는 .cache_info() 메서드를 통해 캐시의 상태를 확인할 수 있습니다.

이는 캐시가 얼마나 잘 작동하고 있는지(hits, misses)를 분석하는 데 매우 유용합니다.

또한, 캐시에 저장된 모든 결과를 강제로 지워야 할 때는 .cache_clear() 메서드를 사용하여 캐시를 무효화할 수 있습니다.

이는 데이터가 변경되어 캐시된 결과가 더 이상 유효하지 않을 때 핵심적으로 사용됩니다.

lru_cache는 사용 편의성 덕분에 대부분의 파이썬 애플리케이션에서 일차적으로 고려되는 캐싱 도구입니다.

💾 캐시 크기 및 시간 제한 설정 (TTL/Size)을 위한 cachetools

functools.lru_cache는 사용하기 매우 편리하지만, 시간 기반 만료(Time-To-Live, TTL) 기능을 제공하지 않는다는 치명적인 단점이 있습니다.

데이터가 시간에 따라 변경될 가능성이 있는 경우, 무한정 캐시된 오래된 데이터를 사용하게 되어 심각한 오류를 유발할 수 있죠.

이때 TTL과 더 유연한 크기 제한 정책이 필요하다면, 외부 라이브러리인 cachetools을 사용하는 것이 필수적입니다.

cachetools는 LRU뿐만 아니라 LFU(Least Frequently Used), RR(Random Replacement) 등 다양한 캐싱 알고리즘을 지원하며, 특히 TTL 기반 캐시를 쉽게 구현할 수 있게 해줍니다.

TTL 캐시 (TTLCache) 구현하기

cachetools에서 시간 기반 만료를 구현할 때는 @cached decorator와 TTLCache 객체를 함께 사용합니다.

TTLCache를 초기화할 때 maxsize(최대 항목 수)와 ttl(만료 시간, 초 단위)을 인수로 전달해야 합니다.

특정 항목이 캐시에 저장된 후 지정된 ttl 시간이 지나면, 해당 항목은 자동으로 만료되어 다음 접근 시 함수가 재실행됩니다.

CODE BLOCK
# pip install cachetools 설치 필요
from cachetools import cached, TTLCache
import time

# 최대 100개의 항목, 60초(1분) TTL 캐시 생성
ttl_cache = TTLCache(maxsize=100, ttl=60)

@cached(ttl_cache)
def fetch_api_data(user_id):
    # 외부 API 호출 시뮬레이션 (1초 소요)
    time.sleep(1)
    print(f"API 호출: 사용자 {user_id} 데이터 가져옴")
    return f"Data for {user_id} @ {time.time()}"

# 첫 번째 호출
print(fetch_api_data(1)) 
# 캐시 적중 (Cache Hit)
print(fetch_api_data(1)) 

# 60초 후 호출 시 만료된 데이터는 무효화되고 재호출됨
# time.sleep(61) 후 호출하면 'API 호출' 메시지가 다시 출력됨

⚠️ 주의: TTL이 너무 짧으면 캐시 재계산 비용이 너무 자주 발생하고, 너무 길면 오래된 데이터를 제공하게 됩니다. TTL 값은 데이터의 변동 주기를 고려하여 신중하게 결정해야 합니다. 데이터가 자주 변하지 않는다면 TTL을 길게, 실시간성이 중요한 데이터라면 짧게 설정해야 합니다.

다양한 캐싱 알고리즘의 선택

cachetools는 LRUCache(Least Recently Used), LFUCache(Least Frequently Used), RRCache(Random Replacement Cache) 등 다양한 캐시 구현체를 제공합니다.

대부분의 경우 LRUCache가 좋은 성능을 보장하지만, 만약 특정 항목이 자주 요청되지만 최근에는 요청되지 않는 패턴이라면, 사용 빈도가 낮은 항목을 제거하는 LFU 캐시가 더 효율적일 수 있습니다.

프로젝트의 데이터 접근 패턴을 분석하여 가장 적합한 캐싱 알고리즘을 선택하는 것이 최적화의 핵심입니다.



🔑 캐시 효율 극대화를 위한 ‘키 정규화’ 전략

캐싱은 함수 호출 시 전달되는 인수를 ‘키(Key)’로 사용하여 이전에 계산된 결과(값)를 찾아냅니다.

하지만 겉보기에는 같아 보이지만 파이썬 내부적으로는 다르게 처리되는 인수의 경우, 캐시가 이를 ‘다른 호출’로 인식하여 캐시 미스(Cache Miss)가 발생하고 불필요한 재계산이 일어나게 됩니다.

이러한 비효율성을 제거하고 캐시 적중률(Cache Hit Ratio)을 극대화하기 위해 ‘키 정규화(Key Normalization)’ 전략이 필요합니다.

키 정규화가 필요한 흔한 상황

키 정규화는 주로 다음과 같은 상황에서 적용됩니다.

  • 📝문자열 대소문자 문제: ‘Apple’과 ‘apple’은 동일한 결과가 예상되지만, 캐시 키는 다르게 인식됩니다.
  • 🔢숫자/자료형 차이: 정수 100과 실수 100.0은 같은 값이지만 캐시 키는 다르게 처리됩니다.
  • ⚙️변경 가능한(Mutable) 객체 문제: 리스트 대신 해시 가능한 튜플로 변환해야 합니다.

normalize_key 함수를 이용한 해결

cachetools 라이브러리는 `key` 인수를 통해 키를 생성하는 사용자 정의 함수(Key Function)를 지정할 수 있게 합니다.

이 함수를 사용하여 인수가 캐시에 저장되기 전에 일관된 형식으로 변환, 즉 정규화할 수 있습니다.

아래 예시에서는 검색 키워드를 모두 소문자로 변환하여 대소문자에 관계없이 캐시를 적중시키는 방법을 보여줍니다.

CODE BLOCK
from cachetools import cached, LRUCache
import time

# 키 정규화 함수 정의: 튜플의 첫 번째 요소(키워드)를 소문자로 변환
def normalize_keyword(args, kwds):
    # args[0]이 검색 키워드라고 가정
    if args:
        normalized_key = (args[0].lower(),) + args[1:]
        return normalized_key
    return args

search_cache = LRUCache(maxsize=10)

@cached(search_cache, key=normalize_keyword)
def search_database(keyword, source="web"):
    time.sleep(1)
    print(f"DB 검색 실행: '{keyword}' from {source}")
    return f"결과 for {keyword.lower()}"

# 첫 번째 호출
print(search_database("Python")) 
# 두 번째 호출 (대소문자만 다름) -> 캐시 적중!
print(search_database("python")) 

# 캐시 통계 확인: hits가 1로 올라간 것을 볼 수 있습니다.
print(search_cache.info())

💬 키 정규화는 캐싱의 핵심 논리 중 하나입니다. 인수가 동일한 결과를 도출한다면, 캐시는 반드시 동일한 키를 사용하도록 보장해야 캐시 적중률을 최대로 끌어올릴 수 있습니다.

또한, functools.lru_cache를 사용하는 경우에도 내부적으로 키 정규화를 수행할 수 있습니다.

데코레이터가 적용된 함수 내에서 캐싱에 영향을 미치지 않는 인수는 기본값으로 처리하거나, 해시 가능한 형태로 변환하여 함수를 호출하도록 코드를 작성함으로써 간접적인 키 정규화 효과를 볼 수 있습니다.

성능 저하 방지: 캐시 무효화 (Cache Invalidation) 전략

캐싱을 잘못 적용하면 성능이 향상되는 것이 아니라, 오래되거나 잘못된 데이터(Stale Data)를 사용자에게 제공하는 심각한 문제를 초래할 수 있습니다.

이 문제를 방지하고 캐시의 정확성을 유지하기 위해 가장 중요한 것이 바로 캐시 무효화(Cache Invalidation) 전략입니다.

“컴퓨터 과학에서 가장 어려운 두 가지는 캐시 무효화와 이름 짓기”라는 농담이 있을 정도로, 캐시 무효화는 시스템 설계에서 핵심적이면서도 까다로운 부분입니다.

가장 일반적인 캐시 무효화 3가지 전략

캐시 무효화를 위한 세 가지 주요 접근 방식은 다음과 같습니다.

1. 시간 기반 만료 (TTL – Time-To-Live)

가장 간단하고 흔한 방법입니다.

데이터가 생성 또는 업데이트된 시점부터 일정 시간이 지나면 캐시를 자동으로 만료시킵니다.

이전 STEP에서 다룬 cachetools의 TTLCache가 이 전략의 대표적인 구현체입니다.

데이터의 변동성이 예측 가능한 경우에 매우 효과적이지만, 만료 시간 직전에 데이터가 변경되면 잠깐 동안 오래된 데이터를 제공할 수 있는 단점이 있습니다.

2. 쓰기 기반 무효화 (Write-Through / Write-Back)

원본 데이터(예: 데이터베이스)가 변경될 때마다 캐시도 함께 업데이트하거나 해당 캐시 항목을 삭제하는 방식입니다.

예를 들어, 데이터베이스의 사용자 정보가 업데이트되면, 해당 사용자 ID와 관련된 캐시 키를 명시적으로 삭제(Clear)합니다.

이는 가장 정확도가 높은 전략이지만, 데이터 변경 로직에 캐시 무효화 코드를 추가해야 하므로 복잡성이 증가합니다.

CODE BLOCK
from functools import lru_cache

# functools lru_cache를 사용한 쓰기 기반 무효화 예시
@lru_cache(maxsize=32)
def get_user_data(user_id):
    # DB에서 데이터 조회 (비용 발생)
    print(f"DB에서 사용자 {user_id} 조회")
    return f"User Data {user_id}"

def update_user_profile(user_id, new_data):
    # 1. DB 업데이트 (원본 데이터 변경)
    # ... update_db_function(user_id, new_data)
    
    # 2. 캐시 무효화 (핵심!)
    get_user_data.cache_clear() 
    # 또는 특정 키만 삭제하는 cachetools의 .pop() 사용
    print(f"사용자 {user_id} 업데이트 후 캐시 무효화")

# 호출 후 업데이트 시 캐시가 다시 Miss됨
get_user_data(101) # DB 조회
update_user_profile(101, "New Profile")
get_user_data(101) # DB 재조회 (캐시 무효화 확인)

3. 데이터 의존성 기반 무효화

하나의 캐시된 결과가 여러 다른 데이터에 의존하는 경우에 사용됩니다.

예를 들어, ‘총 주문 금액’을 캐시하는 함수가 ‘개별 상품 가격’과 ‘할인율’ 데이터에 의존한다면, 두 데이터 중 하나라도 변경되면 ‘총 주문 금액’ 캐시도 무효화해야 합니다.

이 방식은 복잡도가 높기 때문에 Redis나 Memcached 같은 분산 캐시 시스템에서 종종 사용되는 고급 기법입니다.

⚠️ 주의: 함수가 내부적으로 상태를 변경하는 경우(side effect) lru_cache를 사용하면 안 됩니다. 캐싱은 순수 함수(Pure Function, 동일 입력에 동일 출력)에만 적용되어야 하며, 데이터 변경 같은 부작용이 있는 함수에 적용하면 예상치 못한 동작을 유발합니다.



📊 캐싱 전략 적용 전/후 성능 비교 및 검증

캐싱은 코드를 복잡하게 만드는 요소이므로, 캐싱을 적용하기 전 반드시 성능 테스트를 통해 실제로 얼마나 개선되는지 확인하는 과정이 필수적입니다.

단순히 ‘느린 함수’에 적용하는 것만으로는 부족하며, 캐시 적중률(Hit Ratio)이 기대한 만큼 나오는지, 그리고 메모리 사용량에 문제가 없는지를 정확하게 검증해야 합니다.

timeit 모듈을 이용한 성능 측정

파이썬 표준 라이브러리의 `timeit` 모듈은 작은 코드 조각의 실행 시간을 정확하게 측정하는 데 사용됩니다.

캐싱 적용 전후의 실행 시간을 비교하여 성능 향상 폭을 정량적으로 확인할 수 있습니다.

CODE BLOCK
import timeit
from functools import lru_cache

# 캐싱 없는 함수
def fibonacci_no_cache(n):
    if n <= 1:
        return n
    return fibonacci_no_cache(n-1) + fibonacci_no_cache(n-2)

# lru_cache를 적용한 함수
@lru_cache(maxsize=None) # 무제한 캐시
def fibonacci_with_cache(n):
    if n <= 1:
        return n
    return fibonacci_with_cache(n-1) + fibonacci_with_cache(n-2)

# 성능 측정 (N=30 기준)
test_code_no_cache = 'fibonacci_no_cache(30)'
test_code_with_cache = 'fibonacci_with_cache(30)'

time_no_cache = timeit.timeit(test_code_no_cache, globals=globals(), number=1)
time_with_cache = timeit.timeit(test_code_with_cache, globals=globals(), number=1)

print(f"캐싱 없음 (재귀): {time_no_cache:.4f} 초")
print(f"lru_cache 적용: {time_with_cache:.4f} 초")
# 결과: 캐싱 없음은 수 초가 걸리는 반면, 캐싱 적용은 0.0001초 미만으로 실행됨

피보나치 수열 예제에서 볼 수 있듯이, 캐싱은 재귀 호출처럼 중복 계산이 많은 경우 성능을 비약적으로 개선합니다.

캐싱을 적용하기 전에 함수가 반복되는 인수로 여러 번 호출될 가능성이 있는지, 그리고 그 계산 비용이 충분히 큰지를 판단해야 합니다.

캐시 적중률 (Cache Hit Ratio) 분석

캐싱 전략이 성공적인지를 판단하는 가장 중요한 지표는 캐시 적중률입니다.

앞서 언급했듯이 functools.lru_cache의 `.cache_info()` 메서드나 cachetools의 `.info()` 메서드를 사용하면 이 정보를 얻을 수 있습니다.

캐시 적중률은 $\frac{\text{Hits}}{\text{Hits} + \text{Misses}} \times 100\%$로 계산되며, 이 수치가 높을수록 캐시가 효율적으로 작동하고 있다는 의미입니다.

만약 캐시 적중률이 낮다면, 캐시 키 정규화 전략이 부족했거나, maxsize가 너무 작아 캐시 항목이 너무 자주 밀려나고 있을 가능성이 높습니다.

이 경우 maxsize를 늘려보거나, TTL이 설정된 캐시라면 TTL을 늘려보는 등의 최적화 작업을 수행해야 합니다.

💡 TIP: 파이썬의 `cProfile` 모듈은 함수별 호출 횟수와 실행 시간을 자세히 분석할 수 있는 프로파일러 도구입니다. 캐싱 적용 전후의 병목 현상이 어떻게 해소되는지 시각적으로 확인하는 데 큰 도움이 됩니다.

자주 묻는 질문 (FAQ)

lru_cache를 사용할 때 maxsize를 None으로 설정해도 괜찮을까요?
maxsize를 None으로 설정하면 캐시 크기가 무제한이 됩니다. 이는 모든 고유한 함수 호출 결과를 영구적으로 저장하여 메모리 문제가 발생할 수 있습니다. 캐시 항목 수가 제한적이고 예측 가능하다면 괜찮지만, 일반적으로는 메모리 오버헤드를 막기 위해 적절한 maxsize(예: 128, 512)를 지정하는 것이 좋습니다.
cachetools의 TTLCache에서 ttl을 0으로 설정하면 어떻게 되나요?
TTL(Time-To-Live)을 0으로 설정하면 캐시 항목이 저장되는 즉시 만료된 것으로 간주됩니다. 즉, 캐싱 기능 자체가 작동하지 않고 함수가 호출될 때마다 재실행됩니다. 이는 TTLCache의 작동 방식을 테스트할 때 사용될 수 있으나, 실제 운영 환경에서는 의미가 없습니다.
클래스 메서드나 인스턴스 메서드에 lru_cache를 적용할 때 주의할 점은 무엇인가요?
lru_cache는 함수 호출 인수를 모두 캐시 키로 사용합니다. 인스턴스 메서드(self)를 캐싱하면 인스턴스 자체가 캐시 키에 포함되어, 인스턴스별로 캐시가 독립적으로 작동하게 됩니다. 만약 모든 인스턴스에서 결과를 공유하고 싶다면, lru_cache를 클래스 메서드가 아닌 일반 함수에 적용하거나, 인스턴스 메서드 대신 클래스 메서드(@classmethod)에 적용하는 것을 고려해야 합니다.
캐싱을 적용하기에 가장 적합한 함수 유형은 무엇인가요?
순수 함수(Pure Function)에 가장 적합합니다. 즉, 동일한 입력에 대해 항상 동일한 출력을 반환하고, 외부 상태를 변경하거나 외부 상태에 의존하지 않는 함수여야 합니다. 데이터베이스 조회, 복잡한 수학 계산, 파일 읽기 등 비용이 많이 드는 작업이지만 결과가 자주 변하지 않는 함수들이 대표적입니다.
mutable(변경 가능한) 인수를 캐싱하려면 어떻게 해야 하나요?
리스트(list)나 딕셔너리(dict)와 같은 변경 가능한 객체는 해시(hash)될 수 없으므로 lru_cache의 키로 사용할 수 없습니다. 캐싱을 적용하려면 함수 내부에서 인수를 불변(immutable) 객체인 튜플(tuple)로 변환하여 사용해야 합니다. cachetools를 사용한다면 `key` 함수를 이용해 인수를 정규화하고 튜플로 변환하는 것이 일반적입니다.
캐시 무효화 시 cache_clear() 대신 특정 키만 삭제하는 방법이 있나요?
functools.lru_cache는 전체 캐시를 무효화하는 `.cache_clear()`만 제공합니다. 특정 키만 무효화하고 싶다면 cachetools 라이브러리를 사용해야 합니다. cachetools의 캐시 객체는 파이썬 딕셔너리와 유사하게 작동하므로, `cache.pop(key)` 또는 `del cache[key]`를 사용하여 특정 키에 해당하는 캐시 항목만 제거할 수 있습니다.
파이썬 캐싱은 스레드 안전(Thread-Safe)한가요?
네, functools.lru_cache는 내부적으로 락(Lock) 메커니즘을 사용하여 스레드 안전을 보장합니다. 여러 스레드가 동시에 캐싱된 함수를 호출해도 데이터 경쟁(Race Condition) 없이 안전하게 작동합니다. cachetools 역시 기본적으로 스레드 안전하게 설계되었습니다.
캐싱이 성능에 부정적인 영향을 미치는 경우도 있나요?
네, 캐싱 자체가 오버헤드를 발생시키므로, 계산 비용이 매우 낮은 함수에 캐싱을 적용하면 오히려 성능이 저하될 수 있습니다. 또한, 캐시 크기가 너무 크면 메모리를 과도하게 사용하거나, 캐시 항목의 생성 및 제거 비용이 실제 함수 실행 비용보다 높아지는 경우도 발생합니다. 반드시 성능 측정 후 적용해야 합니다.

파이썬 성능 최적화: 캐싱 마스터 전략 최종 요약

파이썬에서 캐싱은 단순한 성능 개선을 넘어, 리소스 사용을 최적화하고 애플리케이션의 반응성을 획기적으로 높여주는 필수 기술입니다.

이 글에서 다룬 전략들을 정리하면, 개발자는 상황에 맞춰 가장 효과적인 캐싱 기법을 선택하고 적용할 수 있습니다.

가장 먼저, functools.lru_cache를 사용하여 간단하고 강력한 메모이제이션(Memoization)을 구현하여 대부분의 경우 성능 문제를 해결할 수 있습니다.

만약 데이터의 신선도(freshness)가 중요하다면, cachetools의 TTLCache를 통해 TTL(Time-To-Live) 기반의 시간 만료 기능을 추가하여 오래된 데이터를 사용하는 위험을 방지해야 합니다.

또한, 캐시 적중률을 높이기 위해 키 정규화(Key Normalization) 전략을 적용하여 ‘Python’과 ‘python’처럼 동일한 결과를 도출하는 인수가 하나의 캐시 키를 공유하도록 유도하는 것이 중요합니다.

마지막으로, 데이터가 변경되었을 때 `cache_clear()`나 쓰기 기반 무효화 전략을 통해 캐시를 즉시 제거함으로써 데이터 무결성을 유지하는 것이 캐싱 전략의 완성입니다.

캐싱은 코드를 복잡하게 만들 수 있지만, timeit 모듈 등을 이용한 정확한 성능 검증을 거친다면, 이보다 더 큰 효율을 가져오는 최적화 기법은 찾기 어려울 것입니다. 이제 여러분의 파이썬 프로젝트에 캐싱 전략을 도입하여 한 단계 더 높은 성능을 경험해 보세요.


🏷️ 관련 태그 : 파이썬성능최적화, lru_cache, cachetools, 캐싱전략, TTL캐시, 파이썬가속화, 캐시무효화, functools, 파이썬데코레이터, 메모이제이션