본문 바로가기
Tech Notes

DB 기반 캐싱 vs 메모리 캐싱: 언제, 어떻게 써야 할까?

by miracle-tech 2025. 8. 15.
728x90
반응형

DB 기반 캐싱 vs 메모리 캐싱: 언제, 어떻게 써야 할까?

🤔 캐싱에 대한 흔한 오해

많은 개발자들이 "캐싱 = 메모리에 임시 저장"이라고 생각합니다. 하지만 실제 서비스에서는 DB 기반 캐싱이 더 효과적인 경우가 많습니다.

일반적인 오해

// ❌ 많은 사람들이 생각하는 캐싱
const cache = new Map(); // 메모리 저장

function getAnalysis(userId) {
  if (cache.has(userId)) {
    return cache.get(userId); // 메모리에서 반환
  }
  
  const result = expensiveAIAnalysis(userId);
  cache.set(userId, result); // 메모리에 저장
  return result;
}

실제 서비스에서의 문제점

  • 🔄 서버 재시작 시 캐시 손실
  • 👥 여러 서버 인스턴스 간 캐시 불일치
  • 💾 메모리 부족 위험
  • 🔍 캐시 상태 추적 어려움

💡 DB 기반 캐싱의 장점

실제 구현 사례

// ✅ DB 기반 캐싱 구현
export async function POST(request: NextRequest) {
  const { userId, currentProfile } = await request.json();
  
  // 1. 현재 프로필로 해시 생성
  const currentHash = generateProfileHash(currentProfile);
  
  // 2. DB에서 기존 분석 결과 조회
  const existingAnalysis = await supabase
    .from('user_health_profiles')
    .select('body_analysis_result, body_analysis_hash')
    .eq('user_id', userId)
    .single();
  
  // 3. 해시 비교로 캐시 히트 판단
  if (existingAnalysis?.body_analysis_hash === currentHash) {
    // 캐시 히트: 기존 결과 반환
    return NextResponse.json({
      cached: true,
      result: existingAnalysis.body_analysis_result
    });
  }
  
  // 4. 캐시 미스: 새로운 AI 분석 실행
  const newAnalysis = await callChatGPTAnalysis(currentProfile);
  
  // 5. DB에 새 결과 저장
  await supabase
    .from('user_health_profiles')
    .upsert({
      user_id: userId,
      body_analysis_result: newAnalysis,
      body_analysis_hash: currentHash,
      updated_at: new Date()
    });
  
  return NextResponse.json({
    cached: false,
    result: newAnalysis
  });
}

🏗️ 해시 기반 캐시 무효화 전략

스마트한 해시 생성

function generateProfileHash(profile: UserProfile): string {
  // 체형분석에 영향을 주는 핵심 데이터만 해시화
  const relevantData = {
    height: profile.height,
    weight: profile.weight,
    age: profile.age,
    gender: profile.gender,
    activityLevel: profile.activityLevel,
    fitnessGoal: profile.fitnessGoal,
    // 주의: updated_at 같은 메타데이터는 제외!
  };
  
  return crypto
    .createHash('md5')
    .update(JSON.stringify(relevantData))
    .digest('hex');
}

왜 이 방식이 효과적인가?

// 시나리오 1: 프로필 변경 없음
const profile1 = { height: 175, weight: 70, age: 25 };
const profile2 = { height: 175, weight: 70, age: 25 };
generateProfileHash(profile1) === generateProfileHash(profile2) 
// → true: 캐시 사용 ✅

// 시나리오 2: 의미있는 변경
const profile3 = { height: 175, weight: 72, age: 25 }; // 체중 변화
generateProfileHash(profile1) === generateProfileHash(profile3)
// → false: 새로운 분석 필요 ✅

// 시나리오 3: 의미없는 변경
const profile4 = { 
  height: 175, 
  weight: 70, 
  age: 25, 
  lastLogin: '2024-01-15' // 해시에 포함되지 않음
};
generateProfileHash(profile1) === generateProfileHash(profile4)
// → true: 캐시 사용 ✅

📊 성능 비교: DB 캐싱 vs 메모리 캐싱

비용 분석

구분 메모리 캐싱 DB 캐싱 비고

캐시 히트 ~1ms ~50ms DB 조회 오버헤드 있음
캐시 미스 ~5000ms ~5050ms AI 호출이 대부분의 시간 차지
서버 재시작 모든 캐시 손실 캐시 유지 DB 캐싱의 큰 장점
확장성 서버별 독립 전체 공유 일관성 보장

실제 사용 패턴에서의 효과

// 사용 시나리오
const scenarios = [
  {
    case: "동일 프로필로 재분석 요청",
    frequency: "70%", 
    result: "DB 캐시 히트 → 50ms 응답"
  },
  {
    case: "체중 1kg 변화 후 분석",
    frequency: "20%",
    result: "새로운 AI 분석 → 5초 응답"
  },
  {
    case: "서버 재시작 후 접속",
    frequency: "10%",
    result: "DB 캐시 유지 → 50ms 응답"
  }
];

// 결과: 평균 응답시간 약 1.3초 (vs 메모리 캐싱 시 3.5초)

 

🎯 실전 적용 가이드

1. DB 캐싱이 적합한 경우

// ✅ 고비용 연산 + 상대적으로 안정적인 입력
- AI/ML 분석 결과
- 복잡한 리포트 생성
- 외부 API 호출 결과
- 이미지/동영상 처리 결과

// 예시: 맞춤 운동 추천
const workoutRecommendation = {
  input: userProfile, // 자주 변하지 않음
  process: chatGPTAnalysis, // 비용이 높음 (시간 + 돈)
  output: exercises, // 재사용 가능
  cacheDuration: "프로필 변경까지" // 조건부 무효화
};

2. 메모리 캐싱이 적합한 경우

// ✅ 저비용 연산 + 자주 변하는 데이터
- 간단한 계산 결과
- 세션 정보
- 임시 상태값
- 실시간 통계

// 예시: BMI 계산
const bmiCalculation = {
  input: { height, weight }, // 자주 변함
  process: simpleFormula, // 비용이 낮음
  output: bmiValue, // 간단한 값
  cacheDuration: "짧은 시간" // 시간 기반 무효화
};

3. 하이브리드 접근법

// 🔥 두 방식을 조합한 최적화
class SmartCache {
  private memoryCache = new Map();
  private readonly MEMORY_TTL = 5 * 60 * 1000; // 5분
  
  async get(key: string, expensiveFunction: Function) {
    // L1: 메모리 캐시 확인
    const memoryResult = this.memoryCache.get(key);
    if (memoryResult && !this.isExpired(memoryResult)) {
      return memoryResult.data;
    }
    
    // L2: DB 캐시 확인
    const dbResult = await this.getFromDB(key);
    if (dbResult) {
      // 메모리에도 저장
      this.memoryCache.set(key, {
        data: dbResult,
        timestamp: Date.now()
      });
      return dbResult;
    }
    
    // L3: 실제 연산 수행
    const result = await expensiveFunction();
    
    // 양쪽 캐시에 모두 저장
    await this.saveToDB(key, result);
    this.memoryCache.set(key, {
      data: result,
      timestamp: Date.now()
    });
    
    return result;
  }
}

🚀 성능 최적화 팁

1. 부분 캐시 업데이트

// ❌ 전체 프로필 변경 시 캐시 전체 무효화
const fullUpdate = {
  trigger: "사용자가 닉네임 변경",
  action: "전체 체형분석 캐시 삭제",
  cost: "불필요한 AI 재호출"
};

// ✅ 의미있는 변경만 캐시 무효화
const smartUpdate = {
  trigger: "체중만 변경",
  action: "해시 재계산 후 선택적 무효화",
  cost: "필요한 경우만 AI 호출"
};

2. 배치 캐시 워밍

// 인기 프로필 패턴에 대해 미리 캐시 생성
async function warmUpCache() {
  const popularProfiles = await getPopularProfilePatterns();
  
  await Promise.all(
    popularProfiles.map(profile => 
      generateBodyAnalysis(profile) // 미리 캐시에 저장
    )
  );
}

📈 모니터링 및 분석

캐시 효율성 측정

// 캐시 성능 메트릭 수집
const cacheMetrics = {
  hitRate: cachehits / totalRequests,
  avgResponseTime: {
    cacheHit: 50, // ms
    cacheMiss: 5000 // ms
  },
  costSavings: {
    aiCallsAvoided: 1000,
    costSaved: 500 // USD
  }
};

// 목표: 캐시 히트율 70% 이상 유지

🎯 결론

DB 기반 캐싱은 단순히 "느린 캐싱"이 아니라, 지속성과 일관성을 보장하는 스마트한 선택입니다.

핵심 포인트

  1. 고비용 연산에는 DB 캐싱이 효과적
  2. 해시 기반 무효화로 정확한 캐시 관리
  3. L1(메모리) + L2(DB) 하이브리드로 최적화
  4. 비즈니스 로직에 맞는 캐시 키 설계가 중요

💡 실전 팁: ChatGPT API 비용이 토큰당 과금인 상황에서, DB 캐싱으로 70% 비용 절감을 달성할 수 있습니다!


이 글이 도움이 되셨나요? 여러분의 프로젝트에서는 어떤 캐싱 전략을 사용하고 계신가요? 댓글로 공유해주세요! 🚀

728x90