
Synthadoc: 자체 무효화 기능을 갖춘 쿼리 캐시 (Query Cache)
요약
Synthadoc v0.7.0은 데이터 변경 시 자동으로 캐시를 무효화하는 쿼리 캐싱 기능을 도입했습니다. 이를 통해 LLM 호출 비용을 절감하고 지연 시간을 줄이면서도 항상 최신 정보를 제공합니다.
핵심 포인트
- Wiki 에포크 기반의 자동 캐시 무효화 메커니즘 구현
- 질문 정규화, 에포크, 모델 정보를 조합한 캐시 키 설계
- LLM 호출 비용 절감 및 응답 지연 시간(Latency) 단축
- 모델 변경 시 캐시를 자동 무효화하여 답변 일관성 유지
대부분의 LLM 쿼리는 두 번째 실행될 때 비용이 많이 든다고 느껴집니다. 당신은 이미 오늘 아침 위키(wiki)에 "무어의 법칙(Moore's Law)이 무엇인가요?"라고 물어보았습니다. 답변은 바뀌지 않았고 - 당신의 위키 내 그 어떤 것도 바뀌지 않았지만 - 만약 오늘 오후에 다시 질문한다면, Synthadoc는 검색 파이프라인(retrieval pipeline)을 작동시키고, 프롬프트(prompt)를 생성하며, LLM을 호출하고, 이미 알고 있는 답변을 얻기 위해 2~10초를 기다리게 될 것입니다.
v0.7.0은 이를 제거합니다. 쿼리 캐시(query cache)는 첫 번째 실행 시 결과를 저장하고, 반복될 때마다 즉시 이를 제공합니다. 단, 한 가지 규칙이 있습니다: 위키가 변경될 때마다 캐시가 자동으로 자체 무효화(invalidate)된다는 점입니다. 수동 플러시(manual flush)도 필요 없고, TTL(Time To Live)을 설정할 필요도 없습니다. 당신은 결코 오래된(stale) 답변을 제공하지 않으며, 동일한 질문에 대해 두 번 비용을 지불하지도 않습니다.
이 포스트는 캐시 설계와 이를 대상으로 실행한 성능 벤치마크(performance benchmarks)를 다룹니다. v0.7.0과 함께 출시된 스트리밍 쿼리 파이프라인(streaming query pipeline) 및 웹 채팅 UI(web chat UI)는 동반 포스트인 **Synthadoc: 스트리밍 쿼리 및 로컬 웹 채팅 UI**에서 다룹니다.
쿼리 캐싱 (Query Caching): LLM을 호출하지 말아야 할 때
캐시 설계는 특정 관찰에서 시작되었습니다: 도메인 위키(domain wiki)에 대한 대부분의 쿼리는 반복된다는 점입니다. 소프트웨어 아키텍처(software architecture)에 대한 지식 베이스(knowledge base)를 유지 관리하는 팀은 온보딩(onboarding) 중, 장애 검토(incident reviews) 중, 계획 세션(planning sessions) 중에 동일한 질문을 수십 번씩 하게 됩니다. 이러한 호출 하나하나가 LLM에 도달하여 지연 시간(latency)과 비용을 모두 발생시킵니다.
캐시는 이를 제거합니다. 하지만 까다로운 부분은 언제 캐시를 무효화해야 할지 아는 것입니다.
캐시 키 설계 (Cache Key Design)
key = SHA-256(normalized_question + "|" + wiki_epoch + "|" + provider_model)
세 가지 구성 요소:
정규화된 질문 (Normalized question) - 소문자화 및 공백 제거 처리됨. "What is Moore's Law?"와 "what is moore's law?"는 동일한 캐시 항목에 접근합니다.
Wiki epoch (위키 에포크) - 서버 인스턴스 상의 정수형 카운터 (integer counter)입니다. 시작 시 0에서 시작하며, 모든 인제스트 (ingest) 작업 완료 및 모든 라이프사이클 상태 전이 시마다 증가합니다. 에포크가 변경되면 모든 질문에 대한 캐시 키 (cache key)가 변경됩니다. 이전 항목들이 즉시 삭제되지는 않지만, 접근할 수 없는 상태가 됩니다. 오래된 항목들은 백그라운드 스윕 (background sweep)을 통해 정리됩니다 (현재 에포크보다 5단계 이상 뒤처진 항목 또는 7일 이상 된 항목).
Provider/model (제공자/모델) - "openai/gpt-4o-mini" 또는 "anthropic/claude-sonnet-4-6"입니다. 모델을 전환하면 캐시가 무효화됩니다. 더 나은 모델로 업그레이드했을 때, 더 작은 모델에서 생성된 캐시된 답변이 나타나서는 안 됩니다.
에포크 방식은 무효화 (invalidation)를 자동으로 만드는 핵심 요소입니다. 인제스트 후에 "캐시 무효화"를 직접 호출할 필요 없이, 에포크 상승이 이를 암시적으로 수행합니다. 위키 변경 후의 모든 쿼리는 이전에 본 적 없는 새로운 키를 계산하며, 캐시 미스 (cache miss)가 발생하여 LLM을 새로 호출하게 됩니다. 이전 답변을 삭제할 필요는 없습니다. 단순히 조회 대상에서 제외될 뿐입니다.
다이어그램: 캐시 조회, 히트, 미스 및 에포크 무효화
측정 결과
지연 시간 (latency) 주장을 단순한 추정치로 남겨두는 대신, 캐시 레이어에 대한 전체 성능 테스트 스위트 (performance test suite)를 작성했습니다. 아래의 모든 수치는 SSD가 장착된 Windows 개발 환경에서 pytest tests/performance/test_query_cache_perf.py를 로컬에서 실행하여 얻은 결과입니다. Linux 베어메탈 (bare-metal) 수치는 일관되게 30~40% 더 우수합니다.
차트 1 - 캐시 읽기 지연 시간 분포 (500회 읽기, 200개 캐시 항목)
P50 = 0.26ms, P95 = 0.34ms, P99 = 0.41ms로, 10ms의 SLO (Service Level Objective) 대비 매우 낮은 수치를 기록했습니다. 분포가 극도로 조밀합니다. 지속 연결 (persistent connection)을 통해 이상치 (outliers)의 주요 원인이었던 호출당 연결 개방 (connection-open) 오버헤드를 제거했기 때문입니다. 모든 백분위수 (percentile)가 예산 범위 내에 충분히 들어와 있으며 여유 공간도 넉넉합니다.
차트 2 - 다양한 LLM 속도에서의 캐시 히트 (Cache hit) vs 미스 (miss) 지연 시간 (latency)
왼쪽 패널은 격차가 너무 커서 선형적으로 보여줄 수 없기 때문에 로그 스케일 (log scale)을 사용했습니다. 캐시 히트 (Cache hit) P50은 LLM 속도와 관계없이 약 0.25ms로 일정하게 유지됩니다. 하나의 공유된 지속 연결 (persistent connection) 덕분에 히트 경로 (hit path)는 파일 개방 비용이 없는 순수한 큐 및 실행 (queue-and-execute) 방식의 SQLite 읽기로 작동합니다. 미스 (miss) 경로는 LLM 지연 시간 (latency)에 따라 직접적으로 확장됩니다. 오른쪽 패널은 그 결과로 나타나는 속도 향상 계수 (speedup factor)를 보여줍니다:
| 시뮬레이션된 LLM 속도 | 캐시 미스 (Cache miss) P50 | 캐시 히트 (Cache hit) P50 | 속도 향상 (Speedup) |
|---|---|---|---|
| 50ms (빠른 제공자) | 95ms | 0.29ms | ~330× |
| ... |
캐시 히트 (cache hit) 시간은 실제 LLM에 비해 매우 작기 때문에, 그 비율은 전적으로 제공자 지연 시간 (provider latency)에 의해 결정됩니다. 추론 모델 (reasoning models, 예: o3-mini, MiniMax M2)의 경우, 단 한 번의 왕복 (round-trip) 절약만으로도 15~30초의 실제 시간 (wall time)을 확보할 수 있습니다.
차트 3 - 동시 읽기 작업자 (Concurrent readers): 지속 연결 (persistent connection) 확장 곡선
단일 읽기: 0.5ms P95. 10명의 읽기: 2.0ms. 25명: 3.8ms. 50명: 7.8ms. 100명: 14.9ms. 곡선은 매끄럽고 단조 증가 (monotonically increasing) 합니다. Windows 특유의 스파이크(spikes)나 비단조적 지터(non-monotonic jitter)는 나타나지 않습니다. 모든 동시 읽기는 하나의 공유된 aiosqlite 백그라운드 스레드를 통해 큐(queue)를 통과합니다. 이전의 불안정성을 유발했던 연결 오픈(connection-open) 오버헤드는 더 이상 존재하지 않습니다. 로컬 단일 사용자 도구의 현실적인 상한선은 n=5–10의 동시 읽기이며, 이때 P95는 2ms 미만입니다. n=100인 경우에도 꼬리 지연 시간(tail latency)은 50ms 예산 범위 내에 충분히 들어옵니다.
차트 4 - 캐시 사용 시 vs 미사용 시 처리량 (queries/second)
처리량의 이점은 n=1에서 75.7배로 시작하여 n=100에서는 4.1배로 압축됩니다. 이러한 압축은 예상된 결과입니다. asyncio.gather()가 시뮬레이션된 LLM 호출을 병렬화하므로, 캐시 미사용 경로는 동시성(concurrency)에 따라 거의 선형적으로 확장됩니다. 반면 하나의 연결을 공유하는 캐시 경로는 aiosqlite 큐를 통해 직렬화(serialize)되어 하위 선형적(sublinearly)으로 증가합니다. 하지만 결정적으로, 캐시는 항상 큰 차이로 승리합니다. n=100에서 4.1배의 이점은 영구 연결(persistent connection) 수정 전의 1.3배보다 훨씬 뛰어납니다. 현실적인 단일 사용자 동시성(n=1–5)에서는 그 이점이 33–76배에 달합니다.
예상 지연 시간(Latency) 이득
중간 규모의 위키(wiki)를 대상으로 하는 전형적인 Synthadoc 쿼리는 두 가지 지연 시간 구성 요소를 가집니다:
- 1단계 (BM25 검색): 100–200ms. 이는 캐시 여부와 상관없이 실행됩니다.
- 2단계 (LLM 합성): 제공자(provider), 모델, 답변 길이에 따라 2–10초가 소요됩니다.
캐시 히트 (Cache hit)가 발생하면 2단계를 완전히 건너뜁니다. 서버는 SQLite에서 캐싱된 result_json을 읽어오며(지속 연결을 통한 SSD 기준 P50 약 0.26ms 소요), 그 다음 네트워크 최대 속도로 합성된 SSE 버스트 (SSE burst)를 방출합니다. 클라이언트는 마치 라이브 스트리밍되는 응답처럼 보이는 것을 받게 되지만, LLM을 위해 210초를 기다리는 대신 전체 버스트가 100ms 이내에 완료됩니다. 추론 모델 (reasoning model) 제공자를 사용하는 경우, 그 격차는 쿼리당 1530초까지 벌어지며, 캐시는 이러한 쿼리들을 즉각적인 것처럼 느끼게 만듭니다.
캐시는 세 가지 인터페이스 모두에서 공유됩니다
CLI, Obsidian 플러그인, 그리고 Web Chat UI는 모두 동일한 cache.db를 공유합니다. 오늘 아침 CLI에서 synthadoc query "..."를 실행했고 위키(wiki)가 변경되지 않았다면, Obsidian 모달을 열고 동일한 질문을 던졌을 때 캐시를 히트하게 됩니다. 키(key)는 동일합니다. 즉, 동일하게 정규화된 질문, 동일한 에포크 (epoch), 동일한 모델을 사용합니다.
# 전체 캐시 삭제 - LLM 응답 캐시와 쿼리 캐시 모두 삭제
synthadoc cache clear
Cache cleared: 47 entries removed.
아키텍처 측면에서 무엇이 다른가
이 캐싱 아키텍처는 명시적인 TTL (Time To Live, 예: 24시간 동안 캐시 또는 일주일 동안 캐시)을 설정하는 일반적인 방식과는 다릅니다. TTL 기반 캐시는 가장자리(edges)에서 거의 항상 잘못됩니다. 기간이 너무 짧거나(여전히 유효한 답변을 제거함), 너무 길거나(위키 업데이트 후에도 오래된 콘텐츠를 제공함) 둘 중 하나이기 때문입니다. 에포크 기반 무효화 (Epoch-based invalidation)는 이벤트 중심 (event-driven)입니다. 즉, 위키의 무언가가 변경될 때까지 캐시가 정확히 유효하게 유지됩니다. 만료(expiry)에 대해 고민할 필요가 없습니다. 새로운 콘텐츠를 수집하면, 다음 쿼리는 자동으로 LLM으로 향합니다. 그 이후의 모든 쿼리는 다음 변경이 있을 때까지 다시 캐시를 히트합니다.
빠른 데모
쿼리 캐싱은 '컴퓨터 역사(history-of-computing)' 데모 위키를 활용한 퀵스타트 가이드에서 다룹니다:
- 쿼리 캐싱: Step 23 - Query caching
전체 과정은 로컬에서 약 10분 정도 소요됩니다:
git clone https://github.com/axoviq-ai/synthadoc.git
pip install -e ".[dev]"
synthadoc install history-of-computing --target ~/wikis --demo
...
Synthadoc이 유용하다고 느끼신다면, GitHub에서 ⭐(Star)를 눌러 프로젝트가 더 많은 사람에게 알려지도록 도와주세요: https://github.com/axoviq-ai/synthadoc.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기


