7장. 웹 UI 연동과 배포
캐시: cache_resource 와 cache_data
식당은 아침에 육수를 한 솥 끓여 두고 하루 종일 손님마다 덜어 씁니다. 손님이 올 때마다 뼈를 고아 육수를 새로 끓이면 손님은 한 시간을 기다려야 합니다. 반면 반찬 접시는 손님마다 새로 담아 냅니다. 한 손님이 반찬을 덜어 먹어도 다른 손님의 접시는 그대로여야 하기 때문입니다. Streamlit 의 캐시 두 가지가 이 둘입니다. st.cache_resource 가 함께 쓰는 육수 솥, st.cache_data 가 손님마다 새로 담는 반찬 접시입니다.
세 상자를 위에서 아래로 읽으면 만드는 횟수, 나눠 쓰는 법, 어울리는 것입니다. 왼쪽 파랑 상자(캐시 없음)는 다시 돌 때마다 새로 만들고, 가운데 보라 상자(cache_resource)는 서버에 하나만 만들어 모든 손님이 같은 것을 쓰고, 오른쪽 하늘 상자(cache_data)는 결과를 보관하되 꺼낼 때마다 복사본을 줍니다. 여기서 육수 솥이 모델과 문서 색인, 반찬 접시가 표나 글 같은 데이터입니다. 비유와 다른 점은, 육수는 하루가 지나면 버리지만 캐시는 서버를 다시 켜거나 함수의 코드를 고치거나 ttl(time to live, 살아 있는 시간)로 정한 시간이 지날 때까지 남는다는 것입니다.
두 캐시 견주기
| 견줄 것 | st.cache_resource | st.cache_data |
|---|---|---|
| 보관하는 것 | 함수가 돌려준 객체 그 자체 | 함수가 돌려준 값의 복사본 |
| 손님끼리 | 같은 객체를 함께 쓴다 | 꺼낼 때마다 새 복사본을 받는다 |
| 어울리는 것 | 모델, 체인, FAISS 색인, 데이터베이스 연결 | pd.read_csv 의 표, API 로 받은 글 |
| 넣으면 안 되는 것 | 손님마다 달라야 하는 것(대화 기록) | 복사할 수 없는 것(열린 연결) |
| 비우기 | st.cache_resource.clear() | st.cache_data.clear() |
캐시는 함수의 인자도 함께 봅니다. load_index("travel_terms.pdf") 와 load_index("faq.pdf") 는 따로 보관합니다.
짧은 예시
색인을 만드는 척하는 함수에 st.cache_resource 를 붙이고, 만든 횟수를 셉니다. 횟수를 적는 목록도 캐시한 함수가 돌려주어 서버에 하나만 있게 했습니다.
import streamlit as st
from streamlit.testing.v1 import AppTest
APP = """
import streamlit as st
@st.cache_resource
def build_log():
return [] # 서버가 켜져 있는 동안 하나만 있는 목록
@st.cache_resource
def load_index():
build_log().append("만듦") # 이 줄은 처음 한 번만 돈다
return {"조각 수": 11}
index = load_index()
st.write(f"색인 조각 {index['조각 수']}개, 색인을 만든 횟수 {len(build_log())}번")
st.button("다시 그리기")
"""
# 앞에서 만든 캐시를 비우고 시작한다(서버를 새로 켠 것과 같다)
st.cache_resource.clear()
at = AppTest.from_string(APP).run()
for _ in range(3):
at.button[0].click().run()
print("네 번 그린 뒤:", at.markdown[0].value)
# 다른 손님이 새로 들어와도 이미 만든 색인을 같이 쓴다
other = AppTest.from_string(APP).run()
print("새 손님:", other.markdown[0].value)네 번 그린 뒤: 색인 조각 11개, 색인을 만든 횟수 1번
새 손님: 색인 조각 11개, 색인을 만든 횟수 1번스크립트는 네 번 돌았지만 load_index 의 몸통은 처음 한 번만 돌았습니다. 둘째부터는 보관해 둔 객체를 그대로 돌려주었기 때문입니다. 새 손님(other)도 색인을 다시 만들지 않았습니다. st.session_state 가 손님마다 따로인 것과 달리 st.cache_resource 는 모든 손님이 함께 씁니다. AppTest 는 한 파이썬 프로그램 안에서 돌므로 캐시가 앞의 시험에서 남아 있을 수 있어, 시험을 시작하기 전에 st.cache_resource.clear() 로 비웠습니다.
코드가 의미하는 것
이 코드는 육수를 아침에 한 솥만 끓이고 하루 종일 덜어 쓰는지 세어 본 것입니다. 끓인 횟수를 적는 장부(build_log)와 육수(load_index)를 둘 다 st.cache_resource 로 감쌌습니다.
| 코드 | 하는 일 | 챗봇에서 뜻하는 것 |
|---|---|---|
@st.cache_resource 와 def load_index(): | 처음 부를 때만 몸통을 돌리고 결과를 보관한다 | 챗봇에서는 PDF 색인이나 체인처럼 만들기 무거운 부품을 여기에 둡니다 |
build_log().append("만듦") | 몸통이 돈 횟수를 적는다 | 캐시가 정말 한 번만 만들었는지 확인하는 장치입니다 |
st.cache_resource.clear() | 보관한 것을 모두 비운다 | 서버를 새로 켠 것과 같은 상태에서 시험을 시작하려는 것입니다 |
other = AppTest.from_string(APP).run() | 새 손님으로 연다 | 손님이 바뀌어도 같은 캐시를 함께 쓰는지 봅니다 |
결과에서 읽을 것은 두 가지입니다.
- 네 번 그려도 만든 횟수는 1번입니다. 단추를 누를 때마다 스크립트는 다시 돌았지만 색인 만들기는 다시 하지 않았습니다.
- 새 손님도 1번입니다. 캐시는 손님끼리 함께 쓰므로, 손님마다 달라야 하는 대화 기록은 캐시가 아니라
st.session_state에 둡니다.
주의할 점
- 대화 기록처럼 손님마다 달라야 하는 것을
st.cache_resource에 두면 모든 손님의 대화가 한 목록에 섞입니다. 손님별 것은st.session_state, 함께 쓸 것은 캐시에 둡니다. - PDF 파일을 고쳐도 캐시는 옛 색인을 그대로 씁니다. 파일을 바꾼 뒤에는 앱을 다시 켜거나
st.cache_resource.clear()로 비웁니다. - 이 장의 실습은 한 프로그램에서
AppTest를 여러 번 열므로 실습마다 처음에 캐시를 비웁니다.streamlit run으로 띄운 진짜 앱에서는 그럴 필요가 없습니다.