Study for me
캐시를 사용해보자 본문
현재 학교 카카오톡 챗봇을 만들고 운영하고 있다.
이런 식으로 산재되어 있는 학교 공지사항들과 학식 메뉴들을 크롤링해서 보여주는 시스템인데, 개선점을 남겨보려고 글을 작성해 본다.
문제점
@router.post("/menu/{location}")
async def read_menu(location: str):
menu = await Menu.filter(location=location).first()
items = []
for i in range(1, 6):
menu_field = getattr(menu, f"menu{i}", None)
if menu_field:
items.append({"description": menu_field})
response = {
"version": "2.0",
"template": {"outputs": [{"carousel": {"type": "textCard", "items": items}}]},
}
return response
/menu/식당코드 url로 POST 요청을 보내면 카카오톡 챗봇이 응답을 보여주는 방식이다.
tortoise orm을 사용해서 Menu에 있는 데이터베이스를 가져오는 것을 확인할 수 있다.
여기서 문제점이 하나 있는데, POST 요청이 올 때마다 매번 DB에 접근해서 데이터를 읽어온다.
해당 카카오톡 챗봇은 매일 밤 00시 00분에 학교 공지사항과 학식 메뉴를 크롤링하는 봇이다. 즉, 데이터가 하루동안 바뀌지 않고 그대로 유지된다는 뜻이다.
그대로 있는 데이터를 매번 DB에 접속에서 읽어오는 게 비효율적인 작업이라는 생각이 들었다.
아직은 챗봇 사용자가 많지 않아서 문제가 없지만, 나중에 수백 명의 사람이 동시에 학식 정보를 받아오려 한다면 불필요한 접속이 너무 많아지게 될 것이다.
개선하기
생각한 방법은 크게 2가지가 있었다.
첫 번째는, redis 사용하기
사실 redis는 들어보기만 하고 사용해 본 적은 한 번도 없었다. (인 메모리 데이터베이스라서 속도가 굉장히 빠르다 정도)
현재 내가 만든 챗봇은 규모가 작을뿐더러, 데이터베이스도 sqlite을 사용하기 때문에 굳이 redis를 쓸 필요는 없을 것 같다.
여기서 다른 방법 하나가 생각이 났는데, 파이썬 딕셔너리를 사용하는 것이다.
매번 POST요청이 올 때 DB에 접근할 필요 없이 00시 01분에 DB에서 읽어온 정보를 딕셔너리에 저장하고, 요청이 올 때 딕셔너리에서 꺼내서 보여주는 것이다.
파이썬 딕셔너리의 내용도 메모리에 저장되기 때문에, 불러오는 속도도 굉장히 빠를 것 같다.
코드
menu_cache = {}
cache_expiry_time = None
@router.get("/menu/{location}")
async def read_menu(location: str):
global cache_expiry_time
now = datetime.now()
today = now.date()
if not cache_expiry_time or now >= cache_expiry_time:
menu_cache.clear()
cache_expiry_time = datetime.combine(
today + timedelta(days=1), datetime.min.time()
)
cache_key = f"{location}_{today}"
if cache_key in menu_cache:
return menu_cache[cache_key]
# 캐시가 없으면 DB에서 읽기
menu = await Menu.filter(location=location, date=today).first()
if not menu:
return {"error": "Menu not found"}
items = [
{"description": getattr(menu, f"menu{i}", None)}
for i in range(1, 6)
if getattr(menu, f"menu{i}", None)
]
response = {
"version": "2.0",
"template": {"outputs": [{"carousel": {"type": "textCard", "items": items}}]},
}
menu_cache[cache_key] = response
return response
cache_expiry_time는 캐시 만료 시간이다.
datetime.combine(today + timedelta(days=1), datetime.min.time())를 사용해 다음 자정을 계산한다.
if문을 사용해서 현재 시간이 cache_expiry_time을 넘으면 menu_cache를 초기화하고, cache_expiry_time을 다음 자정으로 갱신한다.
캐시 확인은 cache_key를 사용해서 딕셔너리에서 확인하고 있다면 캐시를 리턴, 없을 경우 DB에서 읽어와 캐시에 저장하고 리턴한다.
{} 2024-12-25 00:00:00
INFO: 127.0.0.1:55077 - "GET /api/v1/menu/h203 HTTP/1.1" 200 OK
{'h203_2024-12-24': {'version': '2.0', 'template': {'outputs': [{'carousel': {'type': 'textCard', 'items': [{'description': '일품\n1100~1340\n제육덮밥\n계란후라이\n생선까스*타르타르\n팽이장국\n배추김치\n요구르트\n4,500원'}]}}]}}} 2024-12-25 00:00:00
INFO: 127.0.0.1:55077 - "GET /api/v1/menu/h203 HTTP/1.1" 200 OK
print(menu_cache)를 해보면 위 같은 결과가 나오는 걸 확인할 수 있다.
첫 접속에는 캐시가 비어있다가, 두번째 접속에서는 캐시에 저장된 데이터가 불러와지는 것을 확인할 수 있다.
결론
지금은 global 변수로 캐시를 구현했지만, FastAPI의 의존성 주입을 사용해서 코드를 다시 리팩터링 해야겠다.
아직 의존성 주입이라는 용어 자체가 생소해서 더 공부해야 할 것 같다.