Redis 고급 가이드 | 캐싱·Pub/Sub·Streams·클러스터·성능 최적화

Redis 고급 가이드 | 캐싱·Pub/Sub·Streams·클러스터·성능 최적화

이 글의 핵심

Redis로 고성능 캐싱과 메시징을 구현하는 완벽 가이드입니다. 데이터 타입, 캐싱 전략, Pub/Sub, Streams, 클러스터, 성능 최적화까지 실전 예제로 정리했습니다.

실무 경험 공유: 대규모 API 서버에 Redis 캐싱을 도입하면서, 응답 시간을 평균 500ms에서 50ms로 단축하고 데이터베이스 부하를 80% 줄인 경험을 공유합니다.

들어가며: “데이터베이스가 느려요”

실무 문제 시나리오

시나리오 1: DB 쿼리가 500ms 걸려요
복잡한 쿼리가 느립니다. Redis 캐싱으로 50ms로 단축합니다.

시나리오 2: 실시간 알림이 필요해요
WebSocket 서버가 복잡합니다. Redis Pub/Sub으로 간단합니다.

시나리오 3: 세션 관리가 복잡해요
DB에 세션 저장이 느립니다. Redis로 빠르게 처리합니다.


1. Redis란?

핵심 특징

Redis는 인메모리 데이터 구조 저장소입니다.

주요 사용 사례:

  • 캐싱: 데이터베이스 쿼리 결과
  • 세션: 사용자 세션 관리
  • Pub/Sub: 실시간 메시징
  • Rate Limiting: API 속도 제한
  • Leaderboard: 순위표

성능:

  • 읽기/쓰기: 100,000+ ops/sec
  • 지연 시간: < 1ms

2. 설치 및 연결

Docker로 설치

docker run -d --name redis -p 6379:6379 redis:latest

Node.js 클라이언트

npm install redis
// lib/redis.ts
import { createClient } from 'redis';

export const redis = createClient({
  url: process.env.REDIS_URL || 'redis://localhost:6379',
});

redis.on('error', (err) => console.error('Redis Client Error', err));

await redis.connect();

3. 기본 데이터 타입

String

// SET
await redis.set('user:1:name', 'John');

// GET
const name = await redis.get('user:1:name');  // 'John'

// SETEX (만료 시간 설정)
await redis.setEx('session:abc', 3600, 'user-data');

// INCR (증가)
await redis.incr('page:views');  // 1
await redis.incr('page:views');  // 2

Hash

// HSET
await redis.hSet('user:1', {
  name: 'John',
  email: '[email protected]',
  age: '30',
});

// HGET
const email = await redis.hGet('user:1', 'email');

// HGETALL
const user = await redis.hGetAll('user:1');
// { name: 'John', email: '[email protected]', age: '30' }

List

// LPUSH (왼쪽에 추가)
await redis.lPush('queue', 'task1');
await redis.lPush('queue', 'task2');

// RPOP (오른쪽에서 제거)
const task = await redis.rPop('queue');  // 'task1'

// LRANGE (범위 조회)
const tasks = await redis.lRange('queue', 0, -1);

Set

// SADD
await redis.sAdd('tags:1', 'javascript');
await redis.sAdd('tags:1', 'typescript');

// SMEMBERS
const tags = await redis.sMembers('tags:1');
// ['javascript', 'typescript']

// SISMEMBER
const exists = await redis.sIsMember('tags:1', 'javascript');  // true

Sorted Set

// ZADD (점수와 함께 추가)
await redis.zAdd('leaderboard', { score: 100, value: 'user1' });
await redis.zAdd('leaderboard', { score: 200, value: 'user2' });
await redis.zAdd('leaderboard', { score: 150, value: 'user3' });

// ZRANGE (범위 조회, 점수 오름차순)
const top3 = await redis.zRange('leaderboard', 0, 2);
// ['user1', 'user3', 'user2']

// ZREVRANGE (점수 내림차순)
const topUsers = await redis.zRangeWithScores('leaderboard', 0, 2, {
  REV: true,
});
// [{ value: 'user2', score: 200 }, { value: 'user3', score: 150 }, ...]

4. 캐싱 전략

Cache-Aside (Lazy Loading)

async function getUser(id: number) {
  const cacheKey = `user:${id}`;
  
  // 1. 캐시 확인
  const cached = await redis.get(cacheKey);
  if (cached) {
    return JSON.parse(cached);
  }
  
  // 2. DB 조회
  const user = await db.user.findUnique({ where: { id } });
  
  // 3. 캐시 저장 (1시간)
  await redis.setEx(cacheKey, 3600, JSON.stringify(user));
  
  return user;
}

Write-Through

async function updateUser(id: number, data: any) {
  // 1. DB 업데이트
  const user = await db.user.update({
    where: { id },
    data,
  });
  
  // 2. 캐시 업데이트
  await redis.setEx(`user:${id}`, 3600, JSON.stringify(user));
  
  return user;
}

Cache Invalidation

async function deleteUser(id: number) {
  // 1. DB 삭제
  await db.user.delete({ where: { id } });
  
  // 2. 캐시 삭제
  await redis.del(`user:${id}`);
}

5. Pub/Sub

구독자

// subscriber.ts
import { createClient } from 'redis';

const subscriber = createClient();
await subscriber.connect();

await subscriber.subscribe('notifications', (message) => {
  console.log('Received:', message);
});

발행자

// publisher.ts
await redis.publish('notifications', JSON.stringify({
  type: 'new_message',
  userId: 123,
  message: 'Hello!',
}));

실전 예제: 실시간 채팅

// server.ts
import { Server } from 'socket.io';
import { createClient } from 'redis';

const io = new Server(3000);
const subscriber = createClient();
const publisher = createClient();

await subscriber.connect();
await publisher.connect();

// Redis 구독
await subscriber.subscribe('chat:messages', (message) => {
  const data = JSON.parse(message);
  io.to(data.room).emit('message', data);
});

// Socket.io 이벤트
io.on('connection', (socket) => {
  socket.on('join', (room) => {
    socket.join(room);
  });

  socket.on('message', async (data) => {
    // Redis로 발행
    await publisher.publish('chat:messages', JSON.stringify({
      room: data.room,
      user: data.user,
      message: data.message,
    }));
  });
});

6. Redis Streams

메시지 추가

// XADD
await redis.xAdd('events', '*', {
  type: 'user_registered',
  userId: '123',
  email: '[email protected]',
});

메시지 읽기

// XREAD
const messages = await redis.xRead(
  { key: 'events', id: '0' },
  { COUNT: 10 }
);

Consumer Group

// 그룹 생성
await redis.xGroupCreate('events', 'processors', '0', {
  MKSTREAM: true,
});

// 메시지 읽기
const messages = await redis.xReadGroup(
  'processors',
  'consumer1',
  { key: 'events', id: '>' },
  { COUNT: 10 }
);

// 처리 완료
for (const message of messages) {
  await redis.xAck('events', 'processors', message.id);
}

7. Rate Limiting

Fixed Window

async function rateLimit(userId: string, limit: number = 100) {
  const key = `rate:${userId}:${Math.floor(Date.now() / 60000)}`;
  
  const count = await redis.incr(key);
  
  if (count === 1) {
    await redis.expire(key, 60);  // 1분 후 만료
  }
  
  if (count > limit) {
    throw new Error('Rate limit exceeded');
  }
  
  return { remaining: limit - count };
}

Sliding Window

async function slidingWindowRateLimit(userId: string, limit: number = 100) {
  const key = `rate:${userId}`;
  const now = Date.now();
  const window = 60000;  // 1분
  
  // 오래된 항목 제거
  await redis.zRemRangeByScore(key, 0, now - window);
  
  // 현재 카운트
  const count = await redis.zCard(key);
  
  if (count >= limit) {
    throw new Error('Rate limit exceeded');
  }
  
  // 새 요청 추가
  await redis.zAdd(key, { score: now, value: `${now}` });
  await redis.expire(key, 60);
  
  return { remaining: limit - count - 1 };
}

8. 세션 관리

// 세션 저장
async function createSession(userId: string, data: any) {
  const sessionId = crypto.randomUUID();
  
  await redis.setEx(
    `session:${sessionId}`,
    86400,  // 24시간
    JSON.stringify({ userId, ...data })
  );
  
  return sessionId;
}

// 세션 조회
async function getSession(sessionId: string) {
  const data = await redis.get(`session:${sessionId}`);
  return data ? JSON.parse(data) : null;
}

// 세션 삭제
async function deleteSession(sessionId: string) {
  await redis.del(`session:${sessionId}`);
}

9. 성능 최적화

Pipeline

// 여러 명령을 한 번에 실행
const pipeline = redis.multi();

pipeline.set('key1', 'value1');
pipeline.set('key2', 'value2');
pipeline.set('key3', 'value3');

await pipeline.exec();

Transaction

// 원자적 실행
await redis.watch('balance');

const balance = parseInt(await redis.get('balance') || '0');

if (balance >= 100) {
  const multi = redis.multi();
  multi.decrBy('balance', 100);
  multi.incrBy('spent', 100);
  await multi.exec();
}

연결 풀

import { createClient } from 'redis';

const redis = createClient({
  socket: {
    reconnectStrategy: (retries) => Math.min(retries * 50, 500),
  },
});

10. 클러스터

Redis Cluster 설정

# redis.conf
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000

Node.js 연결

import { createCluster } from 'redis';

const cluster = createCluster({
  rootNodes: [
    { url: 'redis://localhost:7000' },
    { url: 'redis://localhost:7001' },
    { url: 'redis://localhost:7002' },
  ],
});

await cluster.connect();

정리 및 체크리스트

핵심 요약

  • Redis: 인메모리 데이터 구조 저장소
  • 캐싱: DB 부하 감소, 응답 속도 향상
  • Pub/Sub: 실시간 메시징
  • Streams: 이벤트 스트리밍
  • Rate Limiting: API 속도 제한
  • 세션: 빠른 세션 관리

구현 체크리스트

  • Redis 설치 및 연결
  • 캐싱 전략 구현
  • Pub/Sub 설정
  • Rate Limiting 구현
  • 세션 관리 구현
  • 성능 최적화
  • 모니터링 설정

같이 보면 좋은 글

  • PostgreSQL 고급 가이드
  • Node.js 성능 최적화
  • Docker Compose 실전 가이드

이 글에서 다루는 키워드

Redis, Cache, Pub/Sub, Streams, In-Memory, Database, Performance

자주 묻는 질문 (FAQ)

Q. Redis vs Memcached, 어떤 게 나은가요?

A. Redis가 더 많은 데이터 타입과 기능을 제공합니다. 대부분의 경우 Redis를 권장합니다.

Q. 데이터 영속성은 어떻게 보장하나요?

A. RDB 스냅샷과 AOF 로그를 사용합니다. 중요한 데이터는 AOF를 활성화하세요.

Q. 메모리가 부족하면 어떻게 되나요?

A. maxmemory-policy 설정에 따라 오래된 키를 삭제하거나 에러를 반환합니다. LRU 정책을 권장합니다.

Q. 프로덕션에서 사용해도 되나요?

A. 네, Redis는 매우 안정적이며 Twitter, GitHub 등 많은 기업에서 사용합니다.

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3