본문 바로가기
Tech Notes

서버리스 환경에서 캐싱이 동작하지 않는 이유와 해결책

by miracle-tech 2025. 9. 24.
728x90
반응형

서버리스 환경에서 애플리케이션을 배포하다 보면 예상과 다르게 캐싱이 제대로 동작하지 않는 경험을 해보셨을 겁니다. 특히 Vercel 같은 플랫폼에서 통계 데이터를 캐싱했는데 매번 새로 불러와지는 현상을 겪으신 분들이 많을 텐데요. 오늘은 이 문제의 원인과 해결책을 알아보겠습니다.

 

 

서버리스는 정말 '서버가 없는' 것일까?

서버리스(Serverless)라는 이름 때문에 서버가 아예 없다고 생각하기 쉽지만, 실제로는 서버가 필요할 때만 켜지고 꺼지는 개념입니다.

 

 

전통적인 서버 vs 서버리스

전통적인 서버

  • 24시간 365일 항상 켜져있음
  • 메모리에 데이터가 지속적으로 유지됨
  • 고정 비용 발생

서버리스

  • 평상시: 완전히 꺼져있는 상태 (메모리에서 제거)
  • 요청 시: 함수를 새로 시작 (Cold Start)
  • 처리 후: 일정 시간 Warm 상태 유지
  • 시간 경과: 다시 완전 종료

 

왜 캐싱이 동작하지 않을까?

서버리스 환경에서 캐싱이 실패하는 주요 원인들을 살펴보겠습니다.

1. Cold Start 문제

// ❌ 문제가 되는 코드
let cache = {};

export default function handler(req, res) {
  if (!cache.data) {
    console.log('캐시 미스! 데이터를 새로 불러옵니다.');
    cache.data = fetchExpensiveData(); // 매번 실행될 수 있음
  }
  return res.json(cache.data);
}

함수가 Cold Start될 때마다 메모리 캐시가 초기화되어 데이터를 다시 불러와야 합니다.

2. 인스턴스 격리

여러 사용자의 요청이 서로 다른 함수 인스턴스에서 처리될 때, 각 인스턴스는 독립적인 메모리 공간을 가지므로 캐시를 공유할 수 없습니다.

3. 실행 시간 제한

Vercel의 경우 함수 실행 시간이 제한되어 있어 긴 캐싱 작업이 중단될 수 있습니다.

 

 

서버리스 환경에 적합한 캐싱 전략

1. 외부 캐시 저장소 활용

메모리 캐시 대신 외부 저장소를 사용하여 여러 인스턴스 간 데이터를 공유합니다.

// ✅ Redis를 활용한 해결책
import { kv } from '@vercel/kv';

export default async function handler(req, res) {
  // 캐시에서 먼저 확인
  let cachedData = await kv.get('statistics');
  
  if (!cachedData) {
    console.log('캐시 미스! 새 데이터를 불러와 캐시에 저장합니다.');
    cachedData = await fetchExpensiveData();
    
    // 1시간 동안 캐시 (3600초)
    await kv.set('statistics', cachedData, { ex: 3600 });
  }
  
  return res.json(cachedData);
}

2. 정적 파일 캐싱

자주 변경되지 않는 통계 데이터는 정적 파일로 생성하여 CDN에서 서비스합니다.

// build 시점에 통계 데이터 생성
export async function getStaticProps() {
  const statisticsData = await generateStatistics();
  
  return {
    props: { statisticsData },
    revalidate: 3600, // 1시간마다 재생성
  };
}

3. SWR (Stale-While-Revalidate) 패턴

오래된 데이터를 먼저 보여주고 백그라운드에서 새 데이터를 가져오는 방식입니다.

import { kv } from '@vercel/kv';

export default async function handler(req, res) {
  const cacheKey = 'statistics';
  const cached = await kv.get(cacheKey);
  
  if (cached && cached.timestamp > Date.now() - 300000) { // 5분 이내
    return res.json(cached.data);
  }
  
  // 백그라운드에서 새 데이터 준비
  if (cached) {
    // 즉시 오래된 데이터 반환
    res.json(cached.data);
    
    // 비동기로 새 데이터 캐시 업데이트
    fetchAndUpdateCache(cacheKey);
  } else {
    // 캐시가 없으면 새 데이터 동기 로딩
    const freshData = await fetchExpensiveData();
    await kv.set(cacheKey, { data: freshData, timestamp: Date.now() });
    return res.json(freshData);
  }
}

 

 

Vercel과 호환되는 무료 Redis 서비스

1. Vercel KV (가장 추천!)

# 설치
npm install @vercel/kv
import { kv } from '@vercel/kv';

// 간단한 사용법
await kv.set('key', 'value');
const value = await kv.get('key');
await kv.del('key');

// TTL 설정
await kv.set('temp-data', data, { ex: 3600 }); // 1시간 후 만료

특징:

  • 무료 한도: 30,000 commands/월
  • Vercel 대시보드에서 원클릭 설정
  • 환경변수 자동 구성
  • 서버리스 최적화된 성능

2. Upstash Redis

npm install @upstash/redis
import { Redis } from '@upstash/redis';

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL,
  token: process.env.UPSTASH_REDIS_REST_TOKEN,
});

await redis.set('key', 'value');
const value = await redis.get('key');

특징:

  • 무료 한도: 10,000 commands/일
  • REST API 기반 (서버리스 친화적)
  • Vercel과 공식 파트너십

3. Redis Cloud

특징:

  • 무료 한도: 30MB 저장공간
  • 전통적인 Redis 프로토콜
  • 더 많은 Redis 기능 지원

 

성능 최적화 팁

1. 적절한 캐시 만료 시간 설정

// 데이터 특성에 따른 캐시 시간 설정
const CACHE_TIMES = {
  realtime: 30,        // 실시간 데이터: 30초
  hourly: 3600,        // 시간별 통계: 1시간
  daily: 86400,        // 일별 통계: 24시간
  static: 604800,      // 정적 데이터: 1주일
};

2. 캐시 키 전략

// 사용자별, 날짜별로 구분된 캐시 키
const getCacheKey = (userId, date, type) => 
  `stats:${userId}:${date}:${type}`;

// 계층적 캐시 무효화
const invalidateUserCache = async (userId) => {
  const pattern = `stats:${userId}:*`;
  // 해당 패턴의 모든 캐시 삭제
};

3. 에러 핸들링과 폴백

export default async function handler(req, res) {
  try {
    const cached = await kv.get(CACHE_KEY);
    if (cached) return res.json(cached);
    
    const fresh = await fetchData();
    await kv.set(CACHE_KEY, fresh, { ex: 3600 });
    return res.json(fresh);
    
  } catch (cacheError) {
    console.warn('캐시 오류, 직접 데이터 조회:', cacheError);
    
    try {
      // 캐시 실패 시 직접 데이터 조회
      const fallbackData = await fetchData();
      return res.json(fallbackData);
    } catch (dataError) {
      return res.status(500).json({ error: 'Service unavailable' });
    }
  }
}

 

마무리

서버리스 환경에서 캐싱은 전통적인 서버 환경과는 다른 접근이 필요합니다. 메모리 캐시 대신 외부 저장소를 활용하고, 적절한 캐시 전략을 수립하면 성능과 비용 모두를 최적화할 수 있습니다.

특히 Vercel KV는 설정이 간단하고 서버리스 환경에 최적화되어 있어 첫 번째 선택지로 추천드립니다. 무료 한도도 충분히 관대하니 부담 없이 시작해보세요!

핵심 포인트:

  • 서버리스는 함수가 필요할 때만 실행되는 환경
  • 메모리 캐시는 함수 종료 시 사라짐
  • 외부 캐시 저장소 (Redis) 활용이 필수
  • Vercel KV가 가장 간단하고 효율적인 해결책

성능 좋은 서버리스 애플리케이션을 만드는 데 도움이 되기를 바랍니다!

728x90