본문으로 건너뛰기
Previous
Next
Redis 고급 가이드 | 캐싱·Pub/Sub·Streams·클러스터·성능 최적화

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

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

이 글의 핵심

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

솔직히 말하면, Redis 쓰다가 제일 크게 갈리는 지점이 캐시 무효화야. user:42만 지우면 될 줄 알았는데, 팀 계정·권한·리스트·검색 인덱스까지 엮이면 “어디까지 지우지?”가 하루 종일 머릿속을 맴돈다. DB 트랜잭션이랑 캐시 키 설계를 동시에 맞출 때, “TTL만 길게 잡을까” 같은 타협이 나온다. 그게 캐시 무효화로 고생하는 기분이다. 누락되면 쓰기 직후에도 옛날 값이 보이고, 공격적으로 지우면 캐시 스탬피드불필요한 DB만 남는다. 여기서 설계를 안 하면, 나중에 “Redis 잘못 썼다”가 아니라 “우리가 그냥 못 썼다” 쪽에 가깝다.

그리고 한 줄 먼저 박는다. Redis를 DB로 쓰지 마세요. RDB·AOF로 복제돼서 “어느 정도” 붙는 건 맞는데, 쿼리·조인·트랜잭션·스키마·백업 복구 스토리를 DB처럼 기대하면 언젠가 터진다. 메인 소스 오브 트루스는 RDBMS나 문서 DB 같은 진짜 영속 계층에 두고, Redis는 캐시·세션·rate limit·짧은 수명의 카운터·pub/sub·스트림 버퍼 정도에 두는 쪽이 정신 건강에 이롭다. “빨라서 DB 대신”은 면접용 멘트로는 통해도, 운영은 안 통하는 경우가 많다.

이름만 보면 redis-advanced-guide인데, 이번엔 목차 달고 정리하던 패턴은 걷어 냈다. 읽다가 중간에 “아 이거 겪어 봤다”가 오면 그게 맞다.

Redis 자체는 인메모리에 자료 구조를 올려둔 저장소이고, 읽기/쓰기가 아주 빠르다는 게 전제다. String·Hash·List·Set·Sorted Set이 기본 뼈대고, 나중에 pub/sub이랑 Streams, 클러스터까지 감싸서 쓰는 그림이 흔하다. 로컬이면 Docker 한 줄이면 끝난다.

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

Node면 공식 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();

타입 몇 가지는 손에 익을 때까지는 그냥 쳐 보는 수밖에 없다. String은 캐시·카운트, Hash는 객체 필드, List는 큐(간단한 작업), Set은 태그·중복 제거, Sorted Set은 리더보드. 예시는 압축해 둔다.

await redis.set('user:1:name', 'John');
const name = await redis.get('user:1:name');
await redis.setEx('session:abc', 3600, 'user-data');
await redis.incr('page:views');

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

await redis.lPush('queue', 'task1');
const task = await redis.rPop('queue');

await redis.sAdd('tags:1', 'javascript', 'typescript');
await redis.zAdd('leaderboard', { score: 200, value: 'user2' });

캐싱은 Cache-Aside가 제일 흔하다. 없으면 DB 갔다 오고, 있으면 그걸 쓴다. 무효화는 삭제·업데이트 시점에 “이 키”를 명시적으로 잡는지, 버전/버킷으로 묶는지, 짧은 TTL로 반쯤 포기하는지 셋 중에 고른다. “그냥 다 지움”은 작은 서비스에선 먹히고, 커지면 키 규칙이랑 도메인 경계가 없을 때 터진다.

async function getUser(id: number) {
  const cacheKey = `user:${id}`;
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);
  const user = await db.user.findUnique({ where: { id } });
  await redis.setEx(cacheKey, 3600, JSON.stringify(user));
  return user;
}

쓰기 일관을 좋아하면 Write-Through(쓰고 캐시도 같이 갱신), 삭제할 땐 캐시 키도 같이 날리는 쪽. 이거 하나 안 맞으면 “사용자 삭제했는데 왜 프로필이 살아 있지?” 류의 이슈가 생긴다.

async function updateUser(id: number, data: any) {
  const user = await db.user.update({ where: { id }, data });
  await redis.setEx(`user:${id}`, 3600, JSON.stringify(user));
  return user;
}

async function deleteUser(id: number) {
  await db.user.delete({ where: { id } });
  await redis.del(`user:${id}`);
}

Pub/Sub은 “누가 메시지 보장해 줘?”에 답이 없을 때 쓰면 된다. 끊기면 그 구간은 사라질 수 있다. 실시간 푸시·간단한 브로드캐스트엔 좋다. Streams는 그보다 “남기고, 그룹으로 나눠 읽는” 쪽에 가깝다. 운영에서 뭘 쓰든 소비 쪽 재시도·멱등은 앱에서 책임진다.

// subscriber — 알림용으로 자주 씀
await subscriber.subscribe('notifications', (message) => {
  console.log('Received:', message);
});
// 발행
await redis.publish('notifications', JSON.stringify({ type: 'new_message', userId: 123 }));

Rate limiting은 INCR+만료, 아니면 sorted set에 타임스탬프 쌓는 슬라이딩 윈도우. “많이 맞는 패턴”이라 코드만 믿지 말고, 경계(유저? IP? API 키?)를 문서에 박아 두자.

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);
  if (count > limit) throw new Error('Rate limit exceeded');
  return { remaining: limit - count };
}

세션은 “빨리 읽고 빨리 지우는” 케이스로 Redis 잘 맞는다. 다만 로그아웃·권한 회수를 생각하면 TTL만으로는 부족할 때가 있다.

Pipeline / MULTI는 왕창 쏟아낼 때나 잔액 같이 “한 번에” 짜르면 쓴다. 클러스터 가면 키·해시 태그 이야기가 나온다. “장애 나면”은 멀티 노드, 센티넬, 랙 위치, 페일오버 연습 쪽으로 이어지는데, 그건 문서가 아니라 런북에 가깝다.

자주 나오는 말엔 짧게 답하자. Memcached vs Redis? 기능 넓이는 Redis, 단순 캐시만이면 Memcached 쪽도 괜찮다. 영속은 RDB·AOF 조합, 프로덕션 쓰는 팀이 많다는 건 맞는데, 그건 “캐시·보조”로 쓰는 그림이 대부분이다. 메모리 터지면 maxmemory-policy로 LRU 같은 걸 쓰는데, “중요한 키인데도 지워짐”이면 정책이 아니라 설계 문제인 경우가 많다.

끝으로, 스테이징에서 데이터·RTT·동시성 정도는 프로덕션에 가깝게 맞추는 게 이득이고, 배포는 팀 루트대로 git addcommitpushnpm run deploy 흐름이면 그걸로 된다. 같이 보면 좋다고 하던 PostgreSQL 가이드, Node.js 성능, Docker Compose 쪽은 여전히 옆에 두고, 이 글은 “Redis = 빠른 조각 도구”라는 쪽에 마음을 맞춰 읽으면 덜 삐끗한다. 기술 면접 완벽 대비코딩 테스트 전략은 면접 앞두고만 훑어도 된다.