Skip to content

블로그에 AI 기능 붙이기 (3편) - 영구 캐싱으로 비용 99% 줄이기

블로그에 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

처음엔 세 가지 선택지를 고민했습니다.

항목LocalStorageRedisSupabase선택 이유
비용무료월 $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: 미사용 캐시 정리용

2단계: 캐시 조회 로직

배경 설명

사용자가 버튼을 누르면 Supabase에서 캐시를 먼저 찾습니다. 있으면 즉시 반환하고, 없으면 OpenAI API를 호출합니다.

코드 구현

async function getCachedExplanation(codeHash) {
  // Supabase에서 캐시 조회
  const response = await fetch(
    `${SUPABASE_URL}/rest/v1/ai_code_explanations?code_hash=eq.${codeHash}`,
    {
      headers: {
        apikey: SUPABASE_SERVICE_KEY,
        Authorization: `Bearer ${SUPABASE_SERVICE_KEY}`,
      },
    }
  )

  const data = await response.json()

  if (data && data.length > 0) {
    const cached = data[0]

    // TTL 체크 제거: 영구 캐싱
    // 히트 카운트만 증가 (비동기)
    incrementHitCount(codeHash)

    console.log(`Cache HIT (hits: ${cached.hit_count + 1})`)

    return {
      explanation: cached.explanation,
      model: cached.model,
      cached: true,
      hitCount: cached.hit_count + 1,
    }
  }

  console.log('Cache MISS')
  return null
}

변경 사항

  • ~TTL 체크 로직 제거~
  • 히트 카운트만 증가
  • 캐시가 있으면 무조건 반환 (영구)

3단계: 캐시 저장 로직

코드 구현

async function saveCachedExplanation(
  codeHash,
  code,
  language,
  explanation,
  model,
  tokensUsed
) {
  const cacheData = {
    code_hash: codeHash,
    code_snippet: code.substring(0, 5000),
    language: language || 'unknown',
    explanation,
    model,
    tokens_used: tokensUsed,
    hit_count: 0,
    created_at: new Date().toISOString(),
    last_accessed_at: new Date().toISOString(),
    expires_at: null, // 영구 보존
  }

  const response = await fetch(`${SUPABASE_URL}/rest/v1/ai_code_explanations`, {
    method: 'POST',
    headers: {
      apikey: SUPABASE_SERVICE_KEY,
      Authorization: `Bearer ${SUPABASE_SERVICE_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(cacheData),
  })

  return response.ok
}

핵심 포인트

  • expires_at: null: 영구 보존
  • hit_count: 0: 초기값, 이후 증가
  • code_snippet: 최대 5000자 제한

전체 흐름

flowchart TD
    A[AI 설명 요청] --> B[SHA-256 해시 생성]
    B --> C[Supabase 캐시 조회]
    C --> D{캐시 존재?}
    D -->|Yes| E[히트 카운트 +1]
    E --> F[캐시 반환 200ms]
    D -->|No| G[OpenAI API 호출 2초]
    G --> H[Supabase 저장]
    H --> I[AI 설명 반환]




스마트 정리 시스템

문제: DB 용량 관리

“캐시가 계속 쌓이면 DB 용량이 부족하지 않나요?”

맞는 말입니다. 저도 이 부분이 걱정됐거든요. 그래서 스마트 정리 시스템을 추가했습니다.


정리 전략

💡핵심 원칙

사용하는 캐시는 보존, 사용하지 않는 캐시만 삭제

  1. 6개월 미사용 캐시: hit_count = 0 + 생성 후 6개월 경과
  2. 1년 미접근 캐시: hit_count < 3 + 1년간 접근 없음

정리 함수

CREATE OR REPLACE FUNCTION cleanup_unused_cache()
RETURNS TABLE(deleted_count bigint) AS $$
DECLARE
  deleted bigint;
BEGIN
  -- 정리 대상
  DELETE FROM ai_code_explanations
  WHERE
    (hit_count = 0 AND created_at < NOW() - INTERVAL '6 months')
    OR
    (hit_count < 3 AND last_accessed_at < NOW() - INTERVAL '1 year');

  GET DIAGNOSTICS deleted = ROW_COUNT;
  RAISE NOTICE '🧹 Cleaned up % unused cache entries', deleted;
  RETURN QUERY SELECT deleted;
END;
$$ LANGUAGE plpgsql;

이렇게 하면:

  • 인기 캐시는 영구 보존
  • 쓸모없는 캐시만 삭제
  • DB 용량 관리 자동화

통계 뷰

개인적으로 캐시 효율을 추적하고 싶어서 통계 뷰도 만들어봤습니다.

CREATE VIEW ai_cache_stats AS
SELECT
  COUNT(*) as total_cached,
  SUM(hit_count) as total_hits,
  AVG(hit_count) as avg_hits_per_cache,

  -- 사용률
  COUNT(CASE WHEN hit_count > 0 THEN 1 END) as used_cache_count,
  COUNT(CASE WHEN hit_count = 0 THEN 1 END) as unused_cache_count,

  -- 비용 절감 (GPT-4o-mini: ~200 tokens = $0.001)
  SUM(tokens_used) as total_tokens_saved,
  ROUND(SUM(tokens_used) * 0.001 / 200.0, 2) as estimated_cost_saved_usd,

  -- 정리 가능 캐시
  COUNT(CASE
    WHEN hit_count = 0 AND created_at < NOW() - INTERVAL '6 months'
    THEN 1
  END) as cleanable_old_unused

FROM ai_code_explanations;

실제로 조회해보면:

total_cached: 47개
total_hits: 142회
avg_hits_per_cache: 3.0회
used_cache_count: 28개 (59.6%)
unused_cache_count: 19개 (40.4%)
total_tokens_saved: 42,600 tokens
estimated_cost_saved_usd: $0.21
cleanable_old_unused: 0개

2주 운영 기준, 이미 $0.21 절감했습니다. 연간으로 환산하면 약 $5.5 정도 절약할 수 있겠네요.





실전 적용 후기

캐시 히트율

실제로 적용해보고 2주간 운영해본 결과입니다.

pie title 2주 운영 결과
    "캐시 히트" : 142
    "API 호출" : 47

2주 운영 결과:

  • 총 요청: 189회
  • 캐시 히트: 142회 (약 75%)
  • OpenAI 호출: 47회 (약 25%)

히트율 75%면 꽤 성공적이라고 봅니다. 블로그 글은 한번 쓰면 계속 읽히거든요. 특히 인기 글일수록 캐시 히트율이 높아집니다.


응답 시간 개선

구분응답 시간개선 효과
OpenAI API 호출약 2초-
Supabase 캐시 조회약 200ms10배 정도 빠름

캐시 히트 시 응답 속도가 10배 정도 빨라졌습니다. 사용자 입장에서는 거의 즉시 설명이 표시되는 느낌이에요.


인기 코드 분석

재밌는 건 hit_count를 추적하니까 어떤 코드가 인기 있는지 알 수 있다는 겁니다.

SELECT
  language,
  hit_count,
  LEFT(code_snippet, 50) as code_preview
FROM ai_code_explanations
ORDER BY hit_count DESC
LIMIT 5;

결과:

언어히트코드 미리보기
java23회orders.stream().filter(...
javascript18회const [value, setValue] = useState(0)
java15회@Transactional...
sql12회SELECT * FROM users WHERE...
java11회Optional.ofNullable(...

역시나 Stream API와 함수형 프로그래밍 관련 코드가 인기가 많네요.





예상치 못한 문제들

운영하면서 몇 가지 질문을 받았는데, 정리해봤습니다.

문제 1: 해시 충돌 걱정

“SHA-256 해시 충돌이 발생하면 어떡하죠?”

현실적으로 불가능합니다

SHA-256 충돌 확률은 2^256분의 1입니다. 우주의 모든 원자 개수보다 많다고 하네요.

블로그 코드 블록 100개 정도로는 절대 충돌하지 않으니 걱정 안 해도 됩니다.


문제 2: 코드 미세 변경

“코드를 한 글자만 바꿔도 해시가 달라지지 않나요?”

맞습니다. 그게 의도입니다.

// 이 코드와
function add(a, b) {
  return a + b
}

// 이 코드는 다른 해시 (공백 제거)
function add(a, b) {
  return a + b
}

코드가 다르면 설명도 달라야 하니까요. 정확하게 같은 코드만 캐시에서 반환합니다.


문제 3: AI 모델 업그레이드

“GPT-4o-mini에서 GPT-5로 업그레이드하면 캐시는?”

💡모델별 캐시 삭제 함수
CREATE FUNCTION cleanup_cache_by_model(target_model VARCHAR)
RETURNS TABLE(deleted_count bigint) AS $$
DECLARE
  deleted bigint;
BEGIN
  DELETE FROM ai_code_explanations
  WHERE model = target_model;

  GET DIAGNOSTICS deleted = ROW_COUNT;
  RETURN QUERY SELECT deleted;
END;
$$ LANGUAGE plpgsql;

-- 사용 예시
SELECT cleanup_cache_by_model('gpt-4o-mini');

모델 업그레이드 시 기존 캐시를 일괄 삭제하고, 새 모델로 다시 캐싱하면 됩니다.





내 프로젝트에 바로 적용하기

체크리스트

  • Supabase 계정과 프로젝트를 생성했는가?
  • 테이블 스키마를 생성했는가?
  • 인덱스를 추가했는가?
  • 환경 변수에 SUPABASE_URL과 KEY를 등록했는가?
  • 스마트 정리 함수를 생성했는가?

주의사항

🚨절대 하지 말 것

❌ Supabase Service Key를 클라이언트에 노출

  • Service Key는 서버에서만 사용
  • 클라이언트에서는 Anon Key만 사용

❌ 인덱스 없이 운영

  • code_hash 인덱스는 필수
  • 조회 속도가 100배 차이

❌ 정리 시스템 없이 무한 증가

  • 6개월마다 cleanup_unused_cache() 실행
  • 또는 cron job으로 자동화

추천 설정

검증된 설정값

테이블 설정:

-- PK로 code_hash 사용
-- expires_at은 NULL (영구 보존)
-- hit_count로 인기 추적

정리 주기:

- 6개월마다 미사용 캐시 정리
- 또는 DB 용량 80% 도달 시

모니터링:

-- 매월 통계 확인
SELECT * FROM ai_cache_stats;

트러블슈팅

Q. “캐시 조회가 느려요”

  • 인덱스를 확인하세요 (idx_code_hash)
  • Supabase 무료 플랜은 제한이 있을 수 있습니다

Q. “DB 용량이 빠르게 차요”

  • cleanup_unused_cache() 실행하세요
  • code_snippet을 5000자로 제한했는지 확인하세요

Q. “히트율이 낮아요”

  • 초기엔 낮을 수 있습니다
  • 2-3주 후 안정화됩니다




이 접근의 아쉬운 점

1. DB 용량은 계속 증가한다

영구 캐싱의 가장 큰 문제입니다.

Supabase 무료 플랜: 500MB

현재 (2주 운영):

  • 캐시 데이터: 약 5MB
  • 예상 증가율: 월 2~3MB

6개월 후 예상:

  • 약 20~30MB
  • 여유롭지만, 1년 후엔?

해결책:

  • 6개월마다 cleanup_unused_cache() 수동 실행
  • 또는 cron job으로 자동화 (별도 인프라 필요)

2. 코드 블록 변경 시 수동 삭제

블로그 글을 수정하면?

// 기존 코드 (이미 캐싱됨)
function add(a, b) {
  return a + b
}

// 수정된 코드
function add(a, b, c) {
  return a + b + c
}

문제:

  • 기존 코드의 AI 설명이 DB에 남아있음
  • 새 코드는 다른 해시 → 새로 캐싱됨
  • 결과: 불필요한 캐시 2개

해결책:

  • 글 수정 시 수동으로 기존 캐시 삭제
  • 또는 “글 ID + 코드 블록 순서”로 캐시 키 설계 (복잡)

3. Supabase 의존성

Supabase가 문제 생기면?

  • 무료 플랜 변경 (500MB → 100MB?)
  • 서비스 장애
  • 가격 정책 변경

대응책:

  • PostgreSQL 백업 (주 1회)
  • 필요 시 다른 DB로 마이그레이션 준비 (SQL 표준 준수)

4. 개선할 수 있는 방법들 (하지만 안 했다)

방법 1: Redis + RDB Persistence

장점: 메모리 캐시 + 영구 저장
단점: 운영 복잡도 급증, 월 $10+ 비용

왜 안 했나?
블로그 트래픽(하루 500~1000명)에는 오버스펙입니다.

방법 2: CDN Edge Function + KV Store

장점: 글로벌 분산 캐싱
단점: Cloudflare Workers KV = 월 $5+

왜 안 했나?
비용이 절감 효과를 초과합니다.

방법 3: 완벽한 캐시 무효화 시스템

장점: 글 수정 시 자동 삭제
단점: CMS와 통합 필요, 개발 복잡도 ↑

왜 안 했나?
글 수정 빈도가 낮아서 수동 삭제로 충분합니다.


5. 결국 “충분히 좋은 해결”을 선택했다

이 시스템은 완벽하지 않습니다. 하지만:

  • ✅ 개인 블로그 규모에는 충분
  • ✅ 운영 복잡도가 낮음
  • ✅ 비용이 거의 안 듦
  • ✅ 6개월마다 정리하면 됨

만약 하루 방문자 1만 명 이상이라면?
그때는 Redis나 CDN을 고민해야 할 겁니다.

지금은 이 정도로 충분합니다.





시스템 점검 체크리스트

저도 배포 전에 이 항목들을 꼭 확인합니다. Supabase 영구 캐싱을 사용한다면 참고하시면 좋을 것 같습니다.

  • code_hash 인덱스: 캐시 조회 속도를 위해 PK 인덱스가 제대로 설정되었는가?
  • hit_count 모니터링: 캐시 히트율이 50% 이상인가? (2~3주 후 안정화)
  • DB 용량 관리: 6개월마다 미사용 캐시 정리 계획이 있는가?
  • code_snippet 길이 제한: 5000자로 제한하여 DB 용량 폭발을 방지했는가?
  • Service Key 보안: Supabase Service Key가 서버 환경변수에만 있고, 클라이언트에 노출되지 않는가?




마무리

TTL 기반 캐싱을 영구 캐싱으로 전환한 경험을 정리해봤습니다.

핵심은 이겁니다. “코드는 불변이다 → 캐시도 불변이다 → TTL이 필요 없다”


실제 효과:

  • OpenAI API 호출 약 75% 감소
  • 응답 속도 10배 정도 향상
  • 연간 약 $5 비용 절감
  • 덤으로 인기 코드 분석 가능

Trade-off 해결:

  • DB 용량 관리 → 스마트 정리로 해결
  • 모델 업그레이드 → 일괄 삭제 함수로 해결

솔직히 금액만 보면 작아 보입니다. 하지만 원칙이 중요하다고 생각해요. “불필요한 API 호출을 하지 않는다”는 원칙이 쌓이면 결국 큰 차이를 만들거든요.

블로그에 AI 기능을 추가하고 있거나, 이미 1편을 구현하신 분들께 도움이 되었으면 합니다. 로컬 스토리지 캐싱에서 한 단계 더 나아가면 비용을 꽤 줄일 수 있습니다.





참고 :

https://supabase.com/docs
https://www.postgresql.org/docs/current/sql-createview.html
https://platform.openai.com/docs/pricing




읽어주셔서 감사합니다.🖐


Ramsbaby
Written byRamsbaby
이 블로그는 직접 개발/운영하는 블로그이므로 당신을 불쾌하게 만드는 불필요한 광고가 없습니다.

#My Github#소개 페이지#Blog OpenSource Github#Blog OpenSource Demo Site