본문 바로가기

프로그래밍/Python

[Python] lru_cache는 언제 사용해?

728x90
Python @lru_cache 실습 중심 가이드
Python · @lru_cache

Python @lru_cache 실습 중심 가이드

이 글은 Python의 @lru_cache 데코레이터를 사용해 함수 결과를 캐싱하고, 성능을 개선하는 방법을 실습 위주로 설명합니다.

1. @lru_cache 한 줄 정의

@lru_cache“함수의 결과를 캐싱하여, 같은 인자로 다시 호출될 때 계산을 생략하고 저장된 값을 반환하는 데코레이터”입니다.

특히 재귀 함수나, 같은 입력으로 여러 번 호출되는 비싼 연산에서 큰 효과를 발휘합니다.

2. 기본 사용법

from functools import lru_cache

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

print(fib(10))
print(fib(10))  # 두 번째 호출은 캐시된 값 사용

maxsize=None으로 설정하면 캐시 크기에 제한이 없습니다(메모리가 허용하는 한). 재귀적인 피보나치 함수에 적용하면 성능이 눈에 띄게 좋아집니다.

3. 주요 옵션 – maxsize, typed

3-1. maxsize

maxsize는 캐시에 저장할 수 있는 최대 엔트리 개수입니다.

from functools import lru_cache

@lru_cache(maxsize=2)
def add(a, b):
    print(f"실제 계산: {a} + {b}")
    return a + b

add(1, 2)  # 계산 발생
add(1, 2)  # 캐시 사용
add(2, 3)  # 계산 발생
add(3, 4)  # 계산 발생 (오래 안 쓰인 항목부터 제거)
add(1, 2)  # 상황에 따라 다시 계산 발생 가능

maxsize를 초과하면, “가장 오랫동안 사용되지 않은(LRU, Least Recently Used)” 항목부터 제거됩니다.

3-2. typed

typed 옵션은 인자의 타입을 구분할지 여부를 결정합니다.

from functools import lru_cache

@lru_cache(maxsize=None, typed=False)
def show(x):
    print(f"계산! x={x} ({type(x)})")
    return x

show(1)    # 계산
show(1.0)  # typed=False 이면 캐시 사용 (동일 키)
@lru_cache(maxsize=None, typed=True)
def show_typed(x):
    print(f"계산! x={x} ({type(x)})")
    return x

show_typed(1)    # 계산
show_typed(1.0)  # typed=True 이면 별도 계산

4. 캐시 상태 확인 & 초기화

4-1. cache_info()

from functools import lru_cache

@lru_cache(maxsize=4)
def slow_square(n):
    print(f"계산: {n} * {n}")
    return n * n

slow_square(2)
slow_square(3)
slow_square(2)

print(slow_square.cache_info())

출력 예:

CacheInfo(hits=1, misses=2, maxsize=4, currsize=2)
  • hits: 캐시를 사용한 횟수
  • misses: 실제 계산이 수행된 횟수
  • maxsize: 설정된 최대 캐시 크기
  • currsize: 현재 캐시에 저장된 항목 수

4-2. cache_clear()

slow_square.cache_clear()

캐시를 초기화하여, 이후 호출부터는 다시 계산이 수행되도록 만들 수 있습니다.

5. 실전 예제 1 – 재귀 함수 최적화

5-1. 캐시 없이

import time

def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)

start = time.time()
print(fib(35))
end = time.time()
print("걸린 시간:", end - start)

5-2. @lru_cache 사용

import time
from functools import lru_cache

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

start = time.time()
print(fib_cached(35))
end = time.time()
print("걸린 시간(캐시):", end - start)

동일한 값을 반복 계산하지 않고 캐시를 활용하기 때문에, 큰 n에 대해서도 실행 시간이 크게 줄어듭니다.

6. 실전 예제 2 – 비싼 연산 캐싱

설정 정보를 읽는 함수에 캐시를 적용하는 예제입니다.

from functools import lru_cache
import time

@lru_cache(maxsize=1)
def load_config():
    print("설정 로딩 중... (예: 파일/DB 읽기)")
    time.sleep(1)
    return {"env": "prod", "debug": False}

print(load_config())  # 첫 호출: 느림
print(load_config())  # 두 번째: 바로 반환

인자가 없는 함수라도 @lru_cache를 사용할 수 있으며, “한 번 계산해두고 계속 재사용하면 좋은 값”에 특히 적합합니다.

7. @lru_cache 사용 시 주의할 점

7-1. 인자는 해시 가능(hashable)해야 한다

내부적으로는 “인자 → 결과”를 딕셔너리 형태로 저장하기 때문에, 키로 사용할 수 없는 타입(예: list, dict)을 인자로 사용할 수 없습니다.

from functools import lru_cache

@lru_cache(maxsize=None)
def f(x):
    return x

f([1, 2, 3])  # TypeError: unhashable type: 'list'

대신 튜플 등 해시 가능한 타입을 사용해야 합니다.

f((1, 2, 3))  # OK

7-2. 부작용이 있는 함수에는 사용하지 않기

@lru_cache(maxsize=None)
def send_email(to):
    print(f"{to}에게 이메일 전송!")
    # 실제 이메일 전송 코드...
    return "OK"

같은 인자로 두 번째 호출부터는 실제 전송이 일어나지 않고, 캐시된 결과만 반환됩니다. 즉, 외부 부작용(side effect)이 중요한 함수에는 @lru_cache를 사용하면 안 됩니다.

7-3. 메모리 사용량

maxsize=None으로 두고 다양한 인자가 계속 들어오면, 캐시가 무한히 커져 메모리를 많이 사용할 수 있습니다.

인자 조합이 많을 것으로 예상된다면, maxsize를 적절한 값으로 제한하는 것이 좋습니다. (예: maxsize=1024)

8. 다른 데코레이터와 함께 사용할 때

@lru_cache는 다른 데코레이터와 함께 사용할 수도 있습니다.

from functools import lru_cache, wraps

def debug(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("호출:", func.__name__, args, kwargs)
        return func(*args, **kwargs)
    return wrapper

@debug
@lru_cache(maxsize=None)
def square(n):
    print("실제 계산:", n)
    return n * n

데코레이터의 순서에 따라 로그가 찍히는 위치나 캐싱 동작 방식이 달라질 수 있으므로, 필요에 따라 순서를 조정해야 합니다.

9. 한 줄 요약

  • @lru_cache는 “함수의 결과를 캐싱하는 데코레이터”이다.
  • 중복 계산이 많은 재귀/비싼 연산에 사용하면 큰 성능 향상이 있다.
  • maxsize, typed 옵션과 cache_info(), cache_clear() 메서드를 잘 활용하면 편리하다.
  • 인자는 해시 가능해야 하며, 부작용이 중요한 함수에는 사용하지 않는 것이 좋다.

10. 연습 문제

연습 1 – factorial 캐싱

factorial(n)을 재귀로 구현하고, @lru_cache를 적용해 보세요.

  • n=1000까지 계산
  • 마지막에 factorial.cache_info() 출력

연습 2 – API 캐싱 시뮬레이션

다음과 같은 함수를 작성해 보세요.

import time

def get_user_from_server(user_id):
    time.sleep(1)
    return {"id": user_id, "name": f"user{user_id}"}

여기에 @lru_cache(maxsize=128)를 적용하고, 같은 user_id로 여러 번 호출하면서 캐시 효과를 체감해 보세요.