Redis 활용 사례: 캐싱, 큐, 속도 제한
사용자가 몇 명일 때는 웹 앱이 잘 작동하지만, 트래픽이 증가하면 페이지 로딩이 느려지고, 백그라운드 작업이 중복되며, API가 악의적인 클라이언트에 의해 과도하게 호출되는 문제가 발생합니다. 이러한 전형적인 확장 문제를 Redis로 우아하게 해결할 수 있습니다. Redis는 인메모리 데이터 저장소로, 세 가지 핵심 작업에 뛰어납니다: 캐싱, 큐, 속도 제한. 이 글에서는 각 사용 사례를 실용적인 예제와 모범 사례와 함께 살펴보겠습니다.
이러한 사용 사례에 Redis를 사용하는 이유
Redis는 데이터를 메모리에 저장하여 매우 빠릅니다—종종 밀리초 이하의 응답 시간을 제공합니다. 문자열, 리스트, 집합, 정렬된 집합, 해시와 같은 풍부한 데이터 구조를 지원하여 캐싱, 큐, 속도 제한에 자연스럽게 매핑됩니다. 또한 원자적 연산, 발행/구독, 지속성 옵션을 제공하여 백엔드 개발자에게 다재다능한 도구입니다.
1. Redis를 사용한 캐싱
캐싱은 가장 일반적인 Redis 사용 사례입니다. 자주 액세스하는 데이터를 Redis에 저장하면 데이터베이스 부하를 줄이고 응답 속도를 높일 수 있습니다.
캐싱 작동 방식
요청이 들어오면 Redis에 데이터가 있는지 확인합니다. 있으면(캐시 적중) 직접 반환합니다. 없으면(캐시 미스) 데이터베이스에서 가져와 만료 시간과 함께 Redis에 저장한 후 반환합니다. 이 패턴을 캐시-어사이드라고 합니다.
// ioredis를 사용한 Node.js 예제
const Redis = require('ioredis');
const redis = new Redis();
async function getUser(userId) {
const cacheKey = `user:${userId}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600); // 1시간 TTL
return user;
}
캐싱 모범 사례
- TTL 설정: 항상 만료 시간을 설정하여 오래된 데이터와 메모리 비대화를 방지합니다.
- 적절한 데이터 구조 사용: 단순 키-값에는 문자열을 사용합니다. 객체에는 해시를 사용하여 필드를 개별적으로 업데이트합니다.
- 캐시 무효화 처리: 기본 데이터가 변경되면 캐시 항목을 업데이트하거나 삭제합니다.
- 적중률 모니터링: 적중률이 낮으면 캐시가 효과적이지 않으므로 키나 TTL을 조정합니다.
캐시 무효화 전략
무효화는 어렵습니다. 일반적인 전략은 다음과 같습니다:
- 시간 기반: TTL에 의존합니다. 간단하지만 오래된 데이터를 제공할 수 있습니다.
- 쓰기 통과: 데이터베이스를 업데이트할 때마다 캐시를 업데이트합니다.
- 쓰기 지연: 캐시를 먼저 업데이트한 다음 비동기적으로 데이터베이스를 업데이트합니다(데이터 손실 위험).
2. Redis를 사용한 큐
큐는 시간이 많이 걸리는 작업을 요청-응답 주기에서 분리합니다. 예를 들어 이메일 보내기, 이미지 처리, 보고서 생성 등을 비동기적으로 수행할 수 있습니다.
간단한 큐 구현
Redis 리스트는 큐에 완벽합니다. LPUSH를 사용하여 왼쪽에 작업을 추가하고 BRPOP을 사용하여 오른쪽에서 블록 및 팝합니다(FIFO).
// 생산자: 작업 추가
await redis.lpush('email_queue', JSON.stringify({ to: 'user@example.com', subject: 'Welcome' }));
// 소비자: 작업 처리(워커에서)
while (true) {
const job = await redis.brpop('email_queue', 0); // 0 = 무한 대기
const { to, subject } = JSON.parse(job[1]);
await sendEmail(to, subject);
}
BRPOPLPUSH를 사용한 신뢰할 수 있는 큐
워커가 충돌할 때 작업 손실을 방지하려면 BRPOPLPUSH를 사용하여 작업을 처리 목록으로 원자적으로 이동합니다. 성공적으로 처리한 후 처리 목록에서 제거합니다. 워커가 죽으면 다른 워커가 처리 목록에서 작업을 복구할 수 있습니다.
const job = await redis.brpoplpush('email_queue', 'processing_queue', 0);
try {
// 작업 처리
await redis.lrem('processing_queue', 1, job);
} catch (err) {
// 오류 처리, 재큐잉 가능
}
전용 큐 라이브러리
프로덕션에서는 Bull(Node.js), RQ(Python), Sidekiq(Ruby)와 같은 라이브러리를 고려하세요. 재시도, 스케줄링, 모니터링을 기본 제공합니다.
3. Redis를 사용한 속도 제한
속도 제한은 API를 남용으로부터 보호하고 공정한 사용을 보장합니다. Redis의 원자적 연산은 요청 수를 세는 데 이상적입니다.
고정 윈도우 속도 제한
시간 창당 각 사용자에 대한 카운터를 증가시킵니다. 카운트가 제한을 초과하면 요청을 거부합니다.
async function isAllowed(userId, limit = 100, windowSec = 60) {
const key = `rate:${userId}:${Math.floor(Date.now() / 1000 / windowSec)}`;
const count = await redis.incr(key);
if (count === 1) await redis.expire(key, windowSec);
return count