performance2025-03-20·9 min·145/348

Vercel에서 Redis 연결 최적화

Vercel 환경에서 Redis 연결을 최적화하고 효율적으로 관리하는 방법을 알아봅니다.

Vercel에서 Redis 연결 최적화

Introduction

Redis는 빠른 데이터 캐싱과 세션 관리에 사용되는 인메모리 데이터베이스입니다. Vercel 서버리스 환경에서 Redis 연결을 최적화하는 방법을 알아보겠습니다.

Environment

  • Vercel 프로젝트 (Next.js 14+)
  • Upstash Redis
  • Vercel Functions
  • Node.js 18+

Problem

Vercel 서버리스 환경에서 Redis 연결 시 발생하는 문제:

// 연결 풀 문제
import { createClient } from 'redis';

const client = createClient({
  url: process.env.REDIS_URL
});

await client.connect();

// 서버리스 함수가 종료될 때 연결이 제대로 닫히지 않음

Analysis

Vercel 환경에서 Redis 연결이 어려운 이유:

  1. 무상태(stateless) 구조: 연결 상태를 유지하기 어려움
  2. Cold Start: 매번 새 연결 생성 필요
  3. 연결 한도: Redis 서버의 연결 수 제한
  4. 네트워크 지연: Vercel 리전과 Redis 서버 간 지연

Solution

1. Upstash Redis 설정

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

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

export default redis;

2. 연결 최적화

// lib/redis-optimized.js
import { Redis } from '@upstash/redis';

let redisInstance = null;

export function getRedisClient() {
  if (!redisInstance) {
    redisInstance = new Redis({
      url: process.env.UPSTASH_REDIS_REST_URL,
      token: process.env.UPSTASH_REDIS_REST_TOKEN,
      enableAutoPipelining: true,
      maxRetriesPerRequest: 3,
    });
  }
  
  return redisInstance;
}

// 연결 상태 확인
export async function checkRedisConnection() {
  try {
    const redis = getRedisClient();
    await redis.ping();
    return true;
  } catch (error) {
    console.error('Redis connection failed:', error);
    return false;
  }
}

3. 캐싱 전략 구현

// lib/cache.js
import { getRedisClient } from './redis-optimized';

export async function getCachedData(key, fetchFn, ttl = 3600) {
  const redis = getRedisClient();
  
  try {
    // 캐시에서 데이터 가져오기
    const cached = await redis.get(key);
    if (cached) {
      return JSON.parse(cached);
    }
    
    // 데이터베이스에서 데이터 가져오기
    const data = await fetchFn();
    
    // 캐시에 저장
    await redis.setex(key, ttl, JSON.stringify(data));
    
    return data;
  } catch (error) {
    console.error('Cache error:', error);
    // 캐시 실패 시 원본 데이터 반환
    return await fetchFn();
  }
}

export async function invalidateCache(pattern) {
  const redis = getRedisClient();
  
  try {
    const keys = await redis.keys(pattern);
    if (keys.length > 0) {
      await redis.del(...keys);
    }
  } catch (error) {
    console.error('Cache invalidation error:', error);
  }
}

4. 세션 관리

// lib/session.js
import { getRedisClient } from './redis-optimized';

const SESSION_TTL = 60 * 60 * 24 * 7; // 7일

export async function createSession(userId) {
  const redis = getRedisClient();
  const sessionId = crypto.randomUUID();
  
  await redis.setex(
    `session:${sessionId}`,
    SESSION_TTL,
    JSON.stringify({
      userId,
      createdAt: Date.now(),
      lastAccessedAt: Date.now(),
    })
  );
  
  return sessionId;
}

export async function getSession(sessionId) {
  const redis = getRedisClient();
  
  const session = await redis.get(`session:${sessionId}`);
  if (!session) {
    return null;
  }
  
  const data = JSON.parse(session);
  
  // 마지막 접근 시간 업데이트
  await redis.setex(
    `session:${sessionId}`,
    SESSION_TTL,
    JSON.stringify({
      ...data,
      lastAccessedAt: Date.now(),
    })
  );
  
  return data;
}

export async function deleteSession(sessionId) {
  const redis = getRedisClient();
  await redis.del(`session:${sessionId}`);
}

5. 레이트 리밋링 구현

// lib/rate-limiter.js
import { getRedisClient } from './redis-optimized';

export async function checkRateLimit(identifier, limit = 100, window = 60) {
  const redis = getRedisClient();
  const key = `ratelimit:${identifier}`;
  
  const current = await redis.incr(key);
  
  if (current === 1) {
    await redis.expire(key, window);
  }
  
  return {
    allowed: current <= limit,
    remaining: Math.max(0, limit - current),
    reset: Date.now() + (window * 1000),
  };
}

6. 모니터링

// lib/redis-monitor.js
import { getRedisClient } from './redis-optimized';

export async function getRedisStats() {
  const redis = getRedisClient();
  
  try {
    const info = await redis.info('memory');
    const stats = {};
    
    info.split('\r\n').forEach(line => {
      const [key, value] = line.split(':');
      if (key && value) {
        stats[key.trim()] = value.trim();
      }
    });
    
    return {
      usedMemory: stats.used_memory_human,
      connectedClients: stats.connected_clients,
      uptime: stats.uptime_in_seconds,
    };
  } catch (error) {
    console.error('Failed to get Redis stats:', error);
    return null;
  }
}

7. 에러 핸들링

// lib/redis-safe.js
import { getRedisClient } from './redis-optimized';

export async function safeRedisOperation(operation) {
  try {
    const redis = getRedisClient();
    return await operation(redis);
  } catch (error) {
    console.error('Redis operation failed:', error);
    
    // 대체 로직 (예: 메모리 캐시 사용)
    return null;
  }
}

// 사용 예시
async function getUserProfile(userId) {
  return safeRedisOperation(async (redis) => {
    const cached = await redis.get(`user:${userId}`);
    if (cached) {
      return JSON.parse(cached);
    }
    
    // 데이터베이스에서 가져오기
    const user = await prisma.user.findUnique({
      where: { id: userId },
    });
    
    await redis.setex(`user:${userId}`, 3600, JSON.stringify(user));
    return user;
  });
}

Lessons Learned

  1. Upstash Redis: Vercel 환경에서는 HTTP 기반의 Upstash Redis가 효과적입니다.
  2. 싱글톤 패턴: Redis 클라이언트를 싱글톤으로 관리하면 연결을 효율적으로 사용할 수 있습니다.
  3. 캐싱 전략: TTL과 캐시 무효화 전략을 수립해야 합니다.
  4. 에러 핸들링: Redis 연결 실패 시 대체 로직이 필요합니다.
  5. 모니터링: Redis 사용량을 모니터링하여 성능을 최적화해야 합니다.

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.