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 등 많은 기업에서 사용합니다.