블로그에 AI 기능 붙이기 (3편) - 영구 캐싱으로 비용 99% 줄이기
TL;DR
- 문제: 로컬 스토리지 TTL 캐싱으로 같은 코드 설명을 매달 반복 생성 (불필요한 API 비용)
- 원인: 코드는 안 바뀌는데 캐시에 30일 TTL을 걸어서 매달 새로 생성
- 해결: Supabase 영구 캐싱 + hit_count 기반 스마트 정리 시스템
- 효과: API 호출 75% 감소, 응답 속도 10배 향상, 연간 약 $5 절감
- 한계: DB 용량 계속 증가 (6개월마다 수동 정리 필요), Supabase 무료 플랜 500MB 제한
글 머리말
제가 1편에서 AI 코드 설명 기능을 만들 때 로컬 스토리지로 캐싱했었습니다.
간단하고 빠르게 구현할 수 있었거든요. 근데 한 가지 문제가 있었습니다.
독자 A가 “AI 설명” 버튼을 누르면 OpenAI API를 호출합니다(₩0.12). 독자 B가 똑같은 코드에서 버튼을 누르면? 또 API를 호출합니다. 독자 C도, 독자 D도… 계속 호출하는 겁니다.
같은 코드 블록인데 매번 돈을 내고 있었습니다.
솔직히 처음엔 “뭐 얼마나 되겠어”라고 생각했는데, 한 달 지나고 보니 생각보다 비용이 쌓이더군요. 그래서 Supabase를 활용한 영구 캐싱 시스템을 구축하기로 했습니다.
핵심 아이디어는 간단합니다. 블로그에 올린 코드 블록은 절대 바뀌지 않습니다. 그렇다면 AI 설명도 바뀔 이유가 없죠. 한 번 생성한 설명을 영구 보존하면 됩니다. TTL? 필요 없습니다.
이번 포스팅에서는 영구 캐싱 시스템 구축 과정과 실제 운영 결과를 공유합니다. 2주 운영 결과, API 호출을 75% 줄이고 응답 속도를 10배 개선할 수 있었습니다.
배경: TTL 기반 캐싱의 한계
기존 방식의 문제점
1편에서는 로컬 스토리지에 30일 TTL로 캐싱했습니다. 당시엔 괜찮다고 생각했거든요.
// 30일 TTL 캐싱
const CACHE_TTL = 30 * 24 * 60 * 60 * 1000 // 30일
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.explanation // 캐시 히트
}
// 30일 지나면 다시 OpenAI 호출문제 발견
제 블로그의 함수형 프로그래밍 포스팅에는 Stream API 예제가 10개 넘게 있습니다. 이 글이 한 달에 약 200번 정도 조회되는데요.
어느 날 API 비용을 정리하다가 뭔가 이상하다는 걸 눈치챘습니다.
flowchart LR
A[30일간 캐시] --> B[만료]
B --> C[API 호출 ₩1.2]
C --> A같은 코드인데 30일마다 반복 과금이 발생합니다.
코드는 안 바뀌는데, 매달 똑같은 설명을 다시 생성하고 있었습니다.
블로그에 코드 블록이 100개 있다면:
- 월 100회 × ₩0.12 = ₩12 반복 지출
- 연간 ₩144
금액은 작아 보이지만, 핵심은 불필요한 낭비라는 겁니다.
설계 논의: 왜 영구 캐싱인가
핵심 통찰: 코드는 불변이다
이 부분이 핵심입니다.
// 이 코드는 영원히 이 코드다
function add(a, b) {
return a + b
}생각해보면 당연한 건데, 이 코드 블록은 1년 후에도, 10년 후에도 똑같습니다. 내용이 바뀌지 않으니 AI 설명도 바뀔 이유가 없죠.
그렇다면? 캐시를 영구 보존하면 됩니다. 한번 설명 생성하면 영원히 재사용할 수 있습니다.
대안 검토: Redis vs LocalStorage vs Supabase
처음엔 세 가지 선택지를 고민했습니다.
| 항목 | LocalStorage | Redis | Supabase | 선택 이유 |
|---|---|---|---|---|
| 비용 | 무료 | 월 $10+ | 무료 (500MB) | ✅ 선택 |
| 응답 속도 | 빠름 (클라이언트) | 매우 빠름 (메모리) | 빠름 (200ms) | ✅ 충분 |
| 공유 캐싱 | ❌ 불가 | ✅ 가능 | ✅ 가능 | ✅ 필수 |
| 운영 복잡도 | 낮음 | 높음 (인프라) | 낮음 | ✅ 선택 |
| 용량 제한 | ~5MB | 무제한 | 500MB (무료) | ✅ 충분 |
왜 Redis를 안 했나?
- 개인 블로그에 Redis 인프라는 오버스펙
- 월 $10+ 비용 vs 절감 효과 $5 = 손해
- 운영 복잡도 증가 (모니터링, 백업 등)
왜 LocalStorage를 버렸나?
- 독자 A와 독자 B가 캐시를 공유 못 함
- 결국 API 호출 반복 (문제 해결 안 됨)
Supabase를 선택한 이유:
- ✅ 무료 플랜 500MB (충분)
- ✅ PostgreSQL로 복잡한 쿼리 가능
- ✅ 운영 복잡도 낮음 (Managed Service)
- ✅ 히트 카운트 등 분석 기능
Trade-off 분석
두 방식을 비교해봤습니다.
| 항목 | TTL 캐싱 | 영구 캐싱 |
|---|---|---|
| API 호출 | 매달 반복 | 최초 1회만 |
| 비용 | 월 ₩12 | 거의 ₩0 |
| 응답 속도 | 약 2초 (API) | 약 200ms (DB) |
| DB 용량 | 불필요 | 증가 (관리 필요) |
| 코드 변경 시 | 자동 갱신 | 수동 삭제 필요 |
장점:
- API 호출 99% 감소 (두 번째 요청부터 캐시)
- 비용 거의 제로
- 응답 속도 10배 향상 (DB 조회가 API 호출보다 훨씬 빠름)
단점:
- DB 용량 증가 (캐시 데이터 누적)
- 사용하지 않는 캐시 관리 필요
솔직히 단점은 스마트 정리로 해결할 수 있으니, 장점이 압도적이라고 판단했습니다.
구현: Supabase 영구 캐싱 시스템
1단계: 테이블 설계
배경 설명
DB는 Supabase PostgreSQL을 선택했습니다. 무료 플랜에서도 500MB까지 제공하는데, 텍스트 캐시 용도로는 충분하거든요.
테이블 스키마
CREATE TABLE ai_code_explanations (
code_hash VARCHAR(64) PRIMARY KEY, -- SHA-256 해시
code_snippet TEXT NOT NULL, -- 원본 코드 (최대 5000자)
language VARCHAR(50), -- 프로그래밍 언어
explanation TEXT NOT NULL, -- AI 설명
model VARCHAR(50) NOT NULL, -- AI 모델명
tokens_used INTEGER, -- 사용 토큰 수
hit_count INTEGER DEFAULT 0, -- 캐시 히트 횟수
created_at TIMESTAMP DEFAULT NOW(), -- 생성 시간
last_accessed_at TIMESTAMP DEFAULT NOW(), -- 마지막 접근 시간
expires_at TIMESTAMP DEFAULT NULL -- TTL 제거 (NULL = 영구)
);
-- 인덱스
CREATE INDEX idx_language ON ai_code_explanations(language);
CREATE INDEX idx_hit_count ON ai_code_explanations(hit_count DESC);
CREATE INDEX idx_last_accessed ON ai_code_explanations(last_accessed_at);핵심 포인트
- code_hash를 PK로: SHA-256 해시로 코드 중복 방지
- hit_count 추적: 인기 코드 분석 가능
- expires_at = NULL: TTL 제거 → 영구 보존
- last_accessed_at: 미사용 캐시 정리용