Redis 완벽 가이드: 인메모리 데이터 저장소
이 글의 핵심
Redis는 초고속 인메모리 데이터 저장소로 캐싱, 세션 관리, 실시간 리더보드, Pub/Sub 메시징을 지원합니다. 다양한 데이터 구조(String, Hash, List, Set, Sorted Set)와 지속성 옵션으로 유연한 아키텍처 구현이 가능합니다.
Redis란?
Redis(Remote Dictionary Server)는 오픈소스 인메모리 데이터 구조 저장소입니다. 2009년 출시 이후 가장 인기있는 NoSQL 데이터베이스로 자리잡았습니다.
핵심 특징
-
초고속 성능
- 인메모리 저장
- 싱글 스레드 아키텍처
- 100,000+ ops/sec
-
다양한 데이터 구조
- String, Hash, List
- Set, Sorted Set
- Bitmap, HyperLogLog
-
지속성
- RDB 스냅샷
- AOF 로그
- 하이브리드 모드
-
고급 기능
- Pub/Sub
- 트랜잭션
- Lua 스크립팅
- 클러스터링
Redis vs Memcached 비교
| 항목 | Redis | Memcached |
|---|---|---|
| 데이터 구조 | 다양 (Hash, List, Set 등) | Key-Value만 |
| 지속성 | RDB, AOF | 없음 |
| 복제 | Master-Replica | 없음 |
| Pub/Sub | 지원 | 미지원 |
| 트랜잭션 | MULTI/EXEC | 없음 |
| 멀티스레드 | 싱글 (v6부터 I/O 멀티) | 멀티스레드 |
| 메모리 관리 | 정교함 | 단순함 |
| 사용 사례 | 캐시, 세션, 큐, 리더보드 | 단순 캐시 |
설치
Redis를 시작하는 가장 빠른 방법은 Docker를 사용하는 것입니다. 몇 초 만에 Redis 서버를 실행할 수 있고, 개발 환경을 오염시키지 않습니다. 프로덕션 환경에서는 전용 Redis 서버나 관리형 서비스(AWS ElastiCache, Redis Cloud)를 사용하는 것이 좋습니다.
Redis 설치
macOS 사용자라면 Homebrew로 설치하는 것이 편리합니다. 설치 후 백그라운드 서비스로 실행되어 자동으로 시작됩니다. Ubuntu에서는 APT 패키지 매니저로 간단히 설치할 수 있습니다.
Docker 방식은 alpine 이미지를 사용하여 용량이 작고(약 30MB) 시작이 빠릅니다. 개발 환경에서는 데이터 지속성이 중요하지 않으므로 볼륨 마운트 없이 사용해도 됩니다.
# macOS
brew install redis
brew services start redis
# Ubuntu
sudo apt update
sudo apt install redis-server
sudo systemctl start redis
# Docker
docker run -d -p 6379:6379 --name redis redis:7-alpine
# 연결 테스트
redis-cli ping
# PONG
Node.js 클라이언트
Node.js에서 Redis를 사용하려면 클라이언트 라이브러리가 필요합니다. ioredis는 Promise 기반이고 클러스터 지원이 우수하여 실무에서 많이 사용됩니다. node-redis는 공식 라이브러리이지만 API가 덜 직관적입니다.
# ioredis (권장)
npm install ioredis
# node-redis
npm install redis
기본 사용법
연결
import Redis from 'ioredis';
const redis = new Redis({
host: 'localhost',
port: 6379,
password: 'your-password',
db: 0,
retryStrategy: (times) => {
const delay = Math.min(times * 50, 2000);
return delay;
}
});
redis.on('connect', () => console.log('Redis Connected'));
redis.on('error', (err) => console.error('Redis Error:', err));
String 데이터 타입
// SET/GET
await redis.set('user:1:name', '홍길동');
const name = await redis.get('user:1:name');
// TTL 설정 (초)
await redis.setex('session:abc', 3600, 'user-data');
// NX (존재하지 않을 때만)
await redis.set('key', 'value', 'NX');
// INCR/DECR (원자적 증감)
await redis.incr('page:views');
await redis.incrby('score', 10);
await redis.decr('stock:item:1');
// MGET/MSET (배치)
await redis.mset('key1', 'val1', 'key2', 'val2');
const values = await redis.mget('key1', 'key2');
Hash 데이터 타입
// HSET/HGET
await redis.hset('user:1', {
name: '홍길동',
email: '[email protected]',
age: 25
});
const email = await redis.hget('user:1', 'email');
const user = await redis.hgetall('user:1');
// HINCRBY
await redis.hincrby('user:1', 'login_count', 1);
// HMSET (여러 필드)
await redis.hmset('user:2', 'name', '김철수', 'age', 30);
List 데이터 타입
// LPUSH/RPUSH (왼쪽/오른쪽 추가)
await redis.lpush('queue:jobs', 'job1', 'job2');
await redis.rpush('timeline', 'post1', 'post2');
// LPOP/RPOP (제거 및 반환)
const job = await redis.lpop('queue:jobs');
// LRANGE (범위 조회)
const posts = await redis.lrange('timeline', 0, 9);
// LLEN (길이)
const length = await redis.llen('queue:jobs');
// BLPOP (블로킹 팝)
const item = await redis.blpop('queue:jobs', 5); // 5초 대기
Set 데이터 타입
// SADD/SREM (추가/제거)
await redis.sadd('tags:post:1', 'javascript', 'nodejs', 'redis');
await redis.srem('tags:post:1', 'redis');
// SMEMBERS (모든 멤버)
const tags = await redis.smembers('tags:post:1');
// SISMEMBER (존재 확인)
const exists = await redis.sismember('tags:post:1', 'nodejs');
// SINTER/SUNION/SDIFF (집합 연산)
await redis.sadd('set1', 'a', 'b', 'c');
await redis.sadd('set2', 'b', 'c', 'd');
const intersection = await redis.sinter('set1', 'set2'); // ['b', 'c']
const union = await redis.sunion('set1', 'set2'); // ['a', 'b', 'c', 'd']
const diff = await redis.sdiff('set1', 'set2'); // ['a']
Sorted Set 데이터 타입
// ZADD (스코어와 함께 추가)
await redis.zadd('leaderboard', 100, 'user1', 200, 'user2', 150, 'user3');
// ZRANGE (순위 조회)
const top10 = await redis.zrange('leaderboard', 0, 9, 'WITHSCORES');
// ZREVRANGE (역순)
const topPlayers = await redis.zrevrange('leaderboard', 0, 2);
// ZINCRBY (스코어 증가)
await redis.zincrby('leaderboard', 50, 'user1');
// ZRANK (순위 확인)
const rank = await redis.zrank('leaderboard', 'user1');
// ZREM (제거)
await redis.zrem('leaderboard', 'user3');
캐싱 전략
캐시 패턴
// 1. Cache-Aside (Lazy Loading)
async function getUser(userId) {
const cacheKey = `user:${userId}`;
// 캐시 확인
let user = await redis.get(cacheKey);
if (user) {
return JSON.parse(user);
}
// DB 조회
user = await db.users.findById(userId);
// 캐시 저장 (1시간)
await redis.setex(cacheKey, 3600, JSON.stringify(user));
return user;
}
// 2. Write-Through (쓰기 시 캐시 갱신)
async function updateUser(userId, data) {
// DB 업데이트
const user = await db.users.update(userId, data);
// 캐시 갱신
await redis.setex(`user:${userId}`, 3600, JSON.stringify(user));
return user;
}
// 3. Write-Behind (비동기 쓰기)
async function updateUserAsync(userId, data) {
// 캐시 즉시 갱신
await redis.setex(`user:${userId}`, 3600, JSON.stringify(data));
// DB는 큐에 넣어 나중에 업데이트
await redis.lpush('queue:db-updates', JSON.stringify({ userId, data }));
}
캐시 무효화
// 단일 키 삭제
await redis.del('user:1');
// 패턴 매칭 삭제
const keys = await redis.keys('user:*');
if (keys.length > 0) {
await redis.del(...keys);
}
// SCAN을 사용한 안전한 삭제 (프로덕션)
async function deleteByPattern(pattern) {
let cursor = '0';
do {
const [newCursor, keys] = await redis.scan(
cursor,
'MATCH',
pattern,
'COUNT',
100
);
cursor = newCursor;
if (keys.length > 0) {
await redis.del(...keys);
}
} while (cursor !== '0');
}
await deleteByPattern('session:*');
Pub/Sub
// Publisher
const publisher = new Redis();
await publisher.publish('news:tech', JSON.stringify({
title: 'Breaking News',
content: 'Something happened'
}));
// Subscriber
const subscriber = new Redis();
subscriber.subscribe('news:tech', (err, count) => {
console.log(`Subscribed to ${count} channels`);
});
subscriber.on('message', (channel, message) => {
console.log(`${channel}: ${message}`);
const data = JSON.parse(message);
// 처리 로직
});
// 패턴 구독
subscriber.psubscribe('news:*', (err, count) => {
console.log(`Subscribed to ${count} patterns`);
});
트랜잭션
// MULTI/EXEC
const multi = redis.multi();
multi.set('key1', 'value1');
multi.set('key2', 'value2');
multi.incr('counter');
const results = await multi.exec();
// Pipeline (트랜잭션 아님, 배치)
const pipeline = redis.pipeline();
pipeline.set('key1', 'value1');
pipeline.set('key2', 'value2');
pipeline.get('key1');
const results = await pipeline.exec();
// WATCH (낙관적 락)
await redis.watch('balance:1');
const balance = await redis.get('balance:1');
if (parseInt(balance) >= 100) {
const multi = redis.multi();
multi.decrby('balance:1', 100);
multi.incrby('balance:2', 100);
await multi.exec();
}
실전 사례: API 응답 캐싱
가장 흔한 Redis 사용 사례는 API 응답 캐싱입니다. 데이터베이스 쿼리는 느리지만, Redis는 초고속이므로 응답 시간을 10배 이상 개선할 수 있습니다. 특히 복잡한 집계 쿼리나 조인이 많은 쿼리에 효과적입니다.
캐시 aside 패턴
캐시 aside는 가장 일반적인 캐싱 패턴입니다. 먼저 캐시를 확인하고, 없으면 DB에서 조회한 후 캐시에 저장합니다. 코드는 간단하지만 캐시 일관성을 유지하기 위해 업데이트 시 캐시를 무효화해야 합니다.
async function getUser(userId) {
const cacheKey = `user:${userId}`;
// 1. 캐시 확인
const cached = await redis.get(cacheKey);
if (cached) {
console.log('Cache hit');
return JSON.parse(cached);
}
// 2. DB 조회
console.log('Cache miss');
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
// 3. 캐시 저장 (1시간 TTL)
await redis.setex(cacheKey, 3600, JSON.stringify(user));
return user;
}
// 업데이트 시 캐시 무효화
async function updateUser(userId, data) {
await db.query('UPDATE users SET ... WHERE id = $1', [userId]);
await redis.del(`user:${userId}`);
}
이 패턴은 읽기가 많고 쓰기가 적은 경우에 적합합니다. 캐시 히트율이 높을수록 DB 부하가 줄어듭니다.
실시간 리더보드
Sorted Set을 사용하면 실시간 게임 리더보드를 쉽게 구현할 수 있습니다. 점수 업데이트와 순위 조회가 모두 O(log N) 시간 복잡도로 매우 빠릅니다.
// 점수 추가/업데이트
await redis.zadd('leaderboard:2026-04', 1500, 'player:홍길동');
await redis.zadd('leaderboard:2026-04', 2300, 'player:김철수');
// 점수 증가
await redis.zincrby('leaderboard:2026-04', 100, 'player:홍길동');
// 상위 10명 조회 (내림차순)
const topPlayers = await redis.zrevrange('leaderboard:2026-04', 0, 9, 'WITHSCORES');
// 특정 플레이어 순위 조회
const rank = await redis.zrevrank('leaderboard:2026-04', 'player:홍길동');
console.log(`순위: ${rank + 1}위`);
// 특정 플레이어 점수 조회
const score = await redis.zscore('leaderboard:2026-04', 'player:홍길동');
이 방식은 수백만 명의 플레이어를 실시간으로 관리할 수 있으며, 게임 서버에서 널리 사용됩니다.
성능 최적화
1. Pipeline으로 RTT 최소화
Network Round-Trip Time(RTT)은 Redis 성능의 주요 병목입니다. Pipeline을 사용하면 여러 명령을 한 번에 전송하여 RTT를 줄일 수 있습니다. 10,000개의 SET 명령을 개별 실행하면 10,000번의 RTT가 필요하지만, Pipeline은 1번의 RTT만 필요합니다.
// 1. Pipeline 사용
const pipeline = redis.pipeline();
for (let i = 0; i < 10000; i++) {
pipeline.set(`key:${i}`, `value:${i}`);
}
await pipeline.exec();
// 2. Connection Pooling
const redis = new Redis({
maxRetriesPerRequest: 3,
enableReadyCheck: false,
lazyConnect: true
});
// 3. 적절한 Eviction 정책
// redis.conf
// maxmemory 2gb
// maxmemory-policy allkeys-lru
2. 메모리 관리
Redis는 인메모리 저장소이므로 메모리 관리가 중요합니다. maxmemory를 설정하고 적절한 eviction 정책을 선택하세요. LRU(Least Recently Used)는 캐시에, LFU(Least Frequently Used)는 접근 빈도가 중요한 경우에 적합합니다.
3. 키 설계
키 이름은 짧고 명확하게 만드세요. user:1:profile 같은 계층적 구조를 사용하면 관리가 쉽습니다. 너무 긴 키는 메모리를 낭비하고 성능을 저하시킵니다.
주의사항
1. 캐시 스탬피드
캐시가 만료된 순간 여러 요청이 동시에 DB를 조회하는 문제입니다. 락이나 확률적 조기 만료로 해결할 수 있습니다.
2. 데이터 일관성
Redis는 캐시이므로 데이터베이스와 불일치할 수 있습니다. 중요한 데이터는 항상 DB를 source of truth로 사용하세요.
3. 지속성 트레이드오프
RDB는 성능이 좋지만 최근 데이터를 잃을 수 있고, AOF는 안전하지만 느립니다. 사용 사례에 따라 선택하세요.
Redis는 강력하고 다재다능한 인메모리 저장소입니다. 캐싱, 세션 관리, 실시간 기능 구현에 필수적인 도구이며, 올바르게 사용하면 애플리케이션 성능을 극적으로 향상시킬 수 있습니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Redis를 활용한 인메모리 캐싱 및 데이터 저장소 완벽 가이드. Memcached 대비 장점, 설치부터 데이터 타입, 캐싱 전략, Pub/Sub, 트랜잭션, 클러스터링, 성능 최적화까지 실전 예제로 학습합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Redis 고급 가이드 | 캐싱·Pub/Sub·Streams·클러스터·성능 최적화
- [Redis Complete Guide | Caching· Pub/Sub](/en/blog/redis-advanced-guide/
- [Node.js Redis Caching Patterns | Cache-Aside· Write-Through](/en/blog/nodejs-redis-caching-patterns/
이 글에서 다루는 키워드 (관련 검색어)
Redis, Cache, In-Memory, Database, NoSQL, Backend, Performance 등으로 검색하시면 이 글이 도움이 됩니다.