Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

Study for me

캐시를 사용해보자 본문

개발

캐시를 사용해보자

k4sud0n 2024. 12. 24. 11:42

현재 학교 카카오톡 챗봇을 만들고 운영하고 있다.

이런 식으로 산재되어 있는 학교 공지사항들과 학식 메뉴들을 크롤링해서 보여주는 시스템인데, 개선점을 남겨보려고 글을 작성해 본다.

문제점

@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의 의존성 주입을 사용해서 코드를 다시 리팩터링 해야겠다.
아직 의존성 주입이라는 용어 자체가 생소해서 더 공부해야 할 것 같다.

Comments