Redis 운영 Best Practice
1. 데이터 설계 원칙
✅ Redis의 역할을 명확히 정의하라
❌ 잘못된 접근: Redis를 Primary DB처럼 사용
✅ 올바른 접근: 역할에 따라 명확히 구분
┌─────────────────────────────────────────────────────┐
│ Redis가 잘하는 것 │ RDBMS에 맡길 것 │
├─────────────────────────────────────────────────────┤
│ 캐시 (Cache) │ 중요 금융 트랜잭션 │
│ 세션 저장소 │ 복잡한 조인 쿼리 │
│ 분산락 │ 정합성 중요 데이터 │
│ 카운터/랭킹 │ 감사 로그(Audit Log) │
│ 임시 데이터, 큐 │ 장기 보관 데이터 │
└─────────────────────────────────────────────────────┘
✅ TTL은 거의 항상 설정하라
# ❌ TTL 없이 저장 → 메모리 누수
SET user:session:123 "data"
# ✅ 항상 만료 시간 설정
SET user:session:123 "data" EX 3600 # 1시간
SET cache:product:456 "data" EX 300 # 5분
# TTL 없는 키 주기적 점검
redis-cli --scan --pattern "*" | xargs -L 1 redis-cli TTL
✅ 키 네이밍 컨벤션
# 규칙: {서비스}:{도메인}:{식별자}:{속성}
service:user:1001:session
service:product:456:stock
service:order:789:status
# 클러스터 환경: Hash Tag로 슬롯 그룹핑
{user:1001}:session
{user:1001}:profile
{user:1001}:cart
# → 세 키가 같은 슬롯, Lua/MULTI 사용 가능
2. 트랜잭션 / 원자성 설계 원칙
✅ 트랜잭션 방식 선택 기준을 명확히
비즈니스 로직 복잡도에 따라:
단순 명령 묶음
└→ MULTI/EXEC
조건 분기 필요 (if-else)
└→ Lua Script
충돌 감지 + 재시도 필요
└→ WATCH + MULTI/EXEC
완전한 All or Nothing 필요 (금융 등)
└→ Redis 단독 불가 → RDBMS 병행 설계
✅ Lua Script 설계 패턴 - "검증 먼저, 실행 나중"
-- ✅ 모든 검증을 앞에 몰아서 부분 실행 방지
local function validate(balance, amount)
if not balance then return "KEY_NOT_FOUND" end
if tonumber(balance) < tonumber(amount) then return "INSUFFICIENT" end
return nil
end
local balance = redis.call('GET', KEYS[1])
local err = validate(balance, ARGV[1])
if err then
return redis.error_reply(err) -- 여기서 중단, 아무것도 변경 안 됨
end
-- 검증 완료 후 실행 (부분 실행 가능성 최소화)
redis.call('DECRBY', KEYS[1], ARGV[1])
redis.call('LPUSH', KEYS[2], ARGV[2])
return 1
✅ 중요 데이터는 RDBMS와 이중 기록
// 재고 차감 예시
@Transactional // DB 트랜잭션
public void decreaseStock(Long productId, int quantity) {
// 1. RDBMS에 먼저 기록 (Source of Truth)
productRepository.decreaseStock(productId, quantity);
// 2. Redis 캐시 갱신 (보조)
try {
redisStockService.decrease(productId, quantity);
} catch (Exception e) {
// Redis 실패해도 DB는 이미 커밋됨
// 캐시 무효화로 다음 요청 시 DB에서 재로딩
redisTemplate.delete("stock:" + productId);
log.warn("Redis 캐시 갱신 실패, 캐시 무효화 처리", e);
}
}
3. 분산락 Best Practice
✅ 분산락 구현 체크리스트
// 올바른 분산락 구현
public boolean acquireLock(String key, String token, long ttlMs) {
// ✅ 1. SET NX + TTL을 원자적으로 (두 명령 분리 금지!)
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(key, token, Duration.ofMillis(ttlMs));
return Boolean.TRUE.equals(result);
}
public boolean releaseLock(String key, String token) {
// ✅ 2. 반드시 Lua로 토큰 비교 + DEL 원자적 처리
String script =
"if redis.call('GET', KEYS[1]) == ARGV[1] then " +
" return redis.call('DEL', KEYS[1]) " +
"else " +
" return 0 " +
"end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
List.of(key), token
);
return Long.valueOf(1L).equals(result);
}
분산락 필수 체크리스트:
✅ 락 획득: SET NX PX {ttl} (원자적 획득 + 만료)
✅ 고유 토큰: UUID로 소유자 식별
✅ 락 해제: Lua Script로 토큰 비교 후 DEL
✅ TTL 설정: 데드락 방지 (프로세스 죽어도 자동 해제)
✅ 재시도 로직: 획득 실패 시 backoff 재시도
✅ TTL > 작업시간: 작업 중 락 만료 방지
4. 캐시 전략 Best Practice
✅ 캐시 패턴 선택
[Cache-Aside (Look-Aside)] ← 가장 일반적
읽기: Redis Miss → DB 조회 → Redis 저장 → 반환
쓰기: DB 저장 → Redis 삭제(무효화)
[Write-Through]
쓰기: DB + Redis 동시 저장
→ 쓰기 지연 발생, 캐시 항상 최신
[Write-Behind]
쓰기: Redis 먼저 → 비동기로 DB 저장
→ 빠른 쓰기, 데이터 유실 위험 ⚠️
✅ 캐시 스탬피드(Cache Stampede) 방지
// 문제: 캐시 만료 시 동시에 수백 요청이 DB로 몰림
// 해결 1: 뮤텍스 락
public Product getProduct(Long id) {
String cacheKey = "product:" + id;
Product cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) return cached;
// ✅ 락으로 단 하나의 요청만 DB 조회
String lockKey = "lock:product:" + id;
String token = UUID.randomUUID().toString();
if (acquireLock(lockKey, token, 3000)) {
try {
// Double-check (락 획득 사이 다른 스레드가 캐싱했을 수 있음)
cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) return cached;
Product product = productRepository.findById(id);
redisTemplate.opsForValue().set(cacheKey, product, Duration.ofMinutes(5));
return product;
} finally {
releaseLock(lockKey, token);
}
}
// 락 획득 실패 시 잠시 대기 후 재시도
Thread.sleep(50);
return getProduct(id);
}
// 해결 2: TTL Jitter (만료 시간 랜덤화)
long ttl = 300 + ThreadLocalRandom.current().nextInt(60); // 300~360초
redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(ttl));
5. 성능 Best Practice
✅ Pipeline 사용 기준
// ❌ 루프에서 개별 호출 (N번의 RTT)
for (String key : keys) {
redisTemplate.opsForValue().get(key);
}
// ✅ Pipeline으로 일괄 처리 (1번의 RTT)
List<Object> results = redisTemplate.executePipelined(
(RedisCallback<Object>) connection -> {
keys.forEach(key -> connection.get(key.getBytes()));
return null;
}
);
// ✅ 배치 크기 조절 (메모리 버퍼 주의)
int BATCH_SIZE = 500; // 한 번에 너무 많이 X
for (int i = 0; i < keys.size(); i += BATCH_SIZE) {
List<String> batch = keys.subList(i, Math.min(i + BATCH_SIZE, keys.size()));
// pipeline 처리
}
✅ 절대 사용하지 말아야 할 명령어
# ❌ KEYS * → 전체 키 스캔, 프로덕션 Redis Hang
KEYS *
KEYS user:*
# ✅ SCAN으로 대체 (커서 기반, 논블로킹)
SCAN 0 MATCH user:* COUNT 100
# ❌ HGETALL, SMEMBERS, LRANGE 0 -1 → 데이터 크면 블로킹
HGETALL huge_hash # 수백만 필드면?
SMEMBERS huge_set
# ✅ 페이지네이션
HSCAN myhash 0 COUNT 100
SSCAN myset 0 COUNT 100
LRANGE mylist 0 99 # 범위 제한
6. 운영/모니터링 Best Practice
✅ 메모리 관리
# redis.conf 필수 설정
maxmemory 4gb # 메모리 상한 설정
maxmemory-policy allkeys-lru # 상한 초과 시 LRU 방식 제거
# (캐시 용도라면 allkeys-lru)
# (세션 등 중요 데이터라면 volatile-lru)
# 메모리 정책 종류
# allkeys-lru : 전체 키 중 LRU
# volatile-lru : TTL 있는 키 중 LRU ← 세션/중요 데이터
# allkeys-lfu : 전체 키 중 LFU (접근 빈도)
# volatile-ttl : TTL 짧은 것부터 제거
# noeviction : 제거 안 함, 에러 반환 ← 절대 캐시에 쓰지 말 것
✅ 모니터링 핵심 지표
# 주기적으로 확인할 지표
redis-cli INFO stats
# ┌─────────────────────────────────────────────┐
# │ 지표 │ 임계값 │
# ├─────────────────────────────────────────────┤
# │ used_memory │ maxmemory 80% 경보 │
# │ keyspace_hits/misses │ hit rate < 80% 점검 │
# │ connected_clients │ 비정상 급증 경보 │
# │ blocked_clients │ 0 이상이면 조사 │
# │ rejected_connections │ 0 이상이면 위험 │
# │ latency │ p99 > 10ms 경보 │
# └─────────────────────────────────────────────┘
# Slow Query 로그
redis-cli CONFIG SET slowlog-log-slower-than 10000 # 10ms 이상
redis-cli SLOWLOG GET 10
✅ 장애 대비 설정
# AOF + RDB 함께 사용 (데이터 영속성)
save 900 1 # RDB: 900초 내 1번 변경 시 스냅샷
appendonly yes # AOF 활성화
appendfsync everysec # 1초마다 fsync (성능 vs 안정성 균형)
# Replica 필수 구성
# Master 1 + Replica 2 이상
# Sentinel 또는 Cluster로 자동 Failover
7. 아키텍처 레벨 Best Practice
✅ 데이터 정합성 보장 패턴
중요 데이터 흐름:
[요청]
│
├→ 1. RDBMS 트랜잭션 (Source of Truth)
│ └→ 커밋 성공
│
└→ 2. Redis 갱신 (캐시 업데이트 or 무효화)
└→ 실패해도 서비스 지속 가능
(다음 요청에서 DB에서 재로딩)
핵심 원칙:
Redis 실패 = 캐시 미스 (서비스 장애 X)
RDBMS 실패 = 트랜잭션 롤백 (데이터 보호)
전체 요약 체크리스트
┌─────────────────────────────────────────────────────────────┐
│ Redis 운영 체크리스트 │
├──────────────────────────────────────────────────────────────┤
│ 데이터 설계 │
│ ✅ 역할 명확히 정의 (캐시 vs 저장소) │
│ ✅ 모든 키에 TTL 설정 │
│ ✅ 키 네이밍 컨벤션 통일 │
├──────────────────────────────────────────────────────────────┤
│ 트랜잭션 │
│ ✅ 조건부 로직 → Lua Script │
│ ✅ Lua에서 검증 먼저, 실행 나중 패턴 │
│ ✅ All or Nothing → RDBMS 병행 설계 │
├──────────────────────────────────────────────────────────────┤
│ 분산락 │
│ ✅ SET NX PX (원자적 획득 + TTL) │
│ ✅ UUID 토큰으로 소유자 식별 │
│ ✅ Lua Script로 원자적 해제 │
├──────────────────────────────────────────────────────────────┤
│ 성능 │
│ ✅ KEYS * 절대 금지 → SCAN 사용 │
│ ✅ 대량 처리 → Pipeline (배치 단위) │
│ ✅ TTL Jitter로 캐시 스탬피드 방지 │
├──────────────────────────────────────────────────────────────┤
│ 운영 │
│ ✅ maxmemory + eviction policy 설정 │
│ ✅ 핵심 지표 모니터링 (hit rate, latency, memory) │
│ ✅ AOF + RDB 영속성 설정 │
│ ✅ Replica + Sentinel/Cluster 구성 │
└──────────────────────────────────────────────────────────────┘
좋은 질문입니다! 결론부터:
redis.call()사용 시, 그 시나리오는 발생하지 않습니다. 오히려 반대 방향이 위험합니다.
redis.call() 에러 동작 원리
redis.call('DECRBY', KEYS[1], ARGV[1]) -- ❌ 실패 시
redis.call('LPUSH', KEYS[2], ARGV[2]) -- 🚫 여기까지 도달 자체를 안 함
redis.call()은 에러 발생 시 Lua 에러를 throw → 스크립트 즉시 중단합니다.
DECRBY가 실패하면 LPUSH는 실행조차 되지 않습니다.
실제 위험한 방향은 반대입니다
redis.call('DECRBY', KEYS[1], ARGV[1]) -- ✅ 성공 → 잔액 차감됨
redis.call('LPUSH', KEYS[2], ARGV[2]) -- ❌ 실패 → 로그 기록 안 됨
-- 잔액은 이미 차감된 상태!
사용자 시나리오: DECRBY 실패 → LPUSH 실행 X ← redis.call() 로 막힘 ✅
실제 위험 시나리오: DECRBY 성공 → LPUSH 실패 ← 여전히 부분 실행 위험 ⚠️
redis.pcall() 을 쓰면 사용자 시나리오가 가능해짐
-- pcall은 에러를 잡고 계속 진행
local ok = redis.pcall('DECRBY', KEYS[1], ARGV[1]) -- ❌ 실패해도 계속!
redis.call('LPUSH', KEYS[2], ARGV[2]) -- ✅ 실행됨 ← 위험!
redis.call() | redis.pcall() | |
|---|---|---|
| 에러 발생 시 | 즉시 중단 | 에러 캡처 후 계속 실행 |
| 이후 명령 실행 | ❌ 실행 안 됨 | ✅ 실행됨 |
| 부분 실행 방향 | 에러 이후 명령만 막힘 | 모든 방향 가능 |
결국 남아있는 근본적인 한계
어떤 방법을 써도 "이미 실행된 명령의 롤백"은 불가능
DECRBY 성공
│
└→ LPUSH 실패 ←── 이 경우는 Lua도 막을 수 없음
DECRBY는 이미 Redis에 반영된 상태
실무에서의 대응 전략
-- 전략 1: 덜 중요한 작업을 뒤에 배치
redis.call('LPUSH', KEYS[2], ARGV[2]) -- 로그 (덜 중요) → 먼저
redis.call('DECRBY', KEYS[1], ARGV[1]) -- 잔액 (핵심) → 나중
-- LPUSH 실패 시 잔액은 건드리지 않음
-- 전략 2: 멱등성 설계 (재시도해도 안전하게)
-- DECRBY 대신 SET으로 최종값을 명시적으로 저장
local new_balance = tonumber(balance) - tonumber(ARGV[1])
redis.call('SET', KEYS[1], new_balance) -- 재시도해도 같은 결과
전략 3: Redis는 캐시/보조, RDBMS가 Source of Truth
[요청]
├→ RDBMS 트랜잭션 (완전한 All or Nothing) ← 핵심 데이터
└→ Redis 업데이트 실패 시 캐시 무효화 ← 정합성 복구
요약:
redis.call()은 "에러 이후 명령 실행"은 막아주지만, "에러 이전 명령의 롤백"은 불가능합니다. 완전한 원자성이 필요한 로직은 RDBMS와 함께 설계해야 합니다.
Redis 추가로 알아야 할 원리와 실용 사례
이미 학습한 내용
✅ Eviction Policy
✅ Lua Script / Pipeline / MULTI/EXEC
✅ Single Thread Queue 동작 원리
✅ Hash Slot (Cluster/Non-Cluster)
✅ Connection Pool (Lettuce)
✅ Master/Replica 분리
✅ Persistence (RDB/AOF)
✅ Distributed Lock
✅ WATCH/DISCARD
✅ SessionCallback 원리
추가로 알아야 할 것들
🔴 높은 우선순위 (실무 필수)
1. Hot Key / Big Key 문제
2. Cache 전략 패턴 (Cache-Aside, Write-Through 등)
3. Cache Stampede 방지
4. Keyspace Notification (키 만료 이벤트)
5. Redis Sentinel (HA)
6. Redis Streams (Kafka 유사 메시징)
🟡 중간 우선순위 (운영 필수)
7. Slow Log 분석
8. Memory 최적화 (자료구조 선택)
9. SCAN 패턴
10. Circuit Breaker (Redis 장애 시)
11. Redis Pub/Sub
🟢 낮은 우선순위 (상황별)
12. HyperLogLog
13. Bitmap
14. Geo (지리 데이터)
15. 무중단 데이터 마이그레이션
1. 🔴 Hot Key / Big Key 문제
Hot Key:
특정 키에 트래픽 집중
→ Redis 단일 스레드 특성상 해당 키 처리에 CPU 집중
→ 다른 요청 지연 발생
예: 인기 상품 상세 페이지
이벤트 시 특정 쿠폰 키에 수만 TPS 집중
Big Key:
값이 너무 큰 키
→ 직렬화/역직렬화 시간 증가
→ 네트워크 전송 지연
→ Eviction 시 메모리 급증
기준:
String > 10KB
Collection > 5000개 요소
// Hot Key 해결: Local Cache 계층 추가
@Service
public class ProductService {
// L1: 로컬 캐시 (Caffeine)
private final Cache<Long, Product> localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofSeconds(5)) // 짧은 TTL
.build();
// L2: Redis 캐시
private final StringRedisTemplate redisTemplate;
public Product getProduct(Long productId) {
// L1 먼저 확인 (Redis 요청 자체를 줄임)
Product local = localCache.getIfPresent(productId);
if (local != null) return local;
// L2 Redis 확인
String cached = redisTemplate.opsForValue()
.get("product:" + productId);
if (cached != null) {
Product product = deserialize(cached);
localCache.put(productId, product); // L1에 저장
return product;
}
// DB 조회
Product product = productRepository.findById(productId)
.orElseThrow();
redisTemplate.opsForValue()
.set("product:" + productId, serialize(product),
Duration.ofMinutes(30));
localCache.put(productId, product);
return product;
}
}
// Big Key 해결: 데이터 분산
// ❌ 하나의 큰 Hash
HSET user:1:profile field1 val1 field2 val2 ... (1000개 필드)
// ✅ 여러 작은 Hash로 분산
HSET user:1:basic name Alice age 30
HSET user:1:address city Seoul
HSET user:1:settings theme dark
2. 🔴 Cache 전략 패턴
┌──────────────────┬────────────────────────────────────────────┐
│ 패턴 │ 동작 │
├──────────────────┼────────────────────────────────────────────┤
│ Cache-Aside │ Miss → DB 조회 → Cache 저장 (가장 일반적) │
│ Write-Through │ 쓰기 시 Cache + DB 동시 저장 │
│ Write-Behind │ Cache 먼저 → 비동기 DB 저장 (빠른 쓰기) │
│ Read-Through │ Cache가 직접 DB 조회 (투명한 캐싱) │
└──────────────────┴────────────────────────────────────────────┘
// Cache-Aside (가장 일반적)
public Product getProduct(Long id) {
// 1. Cache 확인
String cached = redisTemplate.opsForValue().get("product:" + id);
if (cached != null) return deserialize(cached);
// 2. DB 조회
Product product = productRepository.findById(id).orElseThrow();
// 3. Cache 저장
redisTemplate.opsForValue()
.set("product:" + id, serialize(product), Duration.ofMinutes(30));
return product;
}
// Write-Through
@Transactional
public Product updateProduct(Product product) {
// DB 저장
Product saved = productRepository.save(product);
// Cache 동시 갱신
redisTemplate.opsForValue()
.set("product:" + saved.getId(), serialize(saved),
Duration.ofMinutes(30));
return saved;
}
// Write-Behind (비동기)
public void updateProductAsync(Product product) {
// Cache 먼저
redisTemplate.opsForValue()
.set("product:" + product.getId(), serialize(product),
Duration.ofMinutes(30));
// DB는 비동기로
applicationEventPublisher.publishEvent(
new ProductUpdateEvent(product)
);
}
@Async
@EventListener
public void handleProductUpdate(ProductUpdateEvent event) {
productRepository.save(event.getProduct());
}
3. 🔴 Cache Stampede 방지
문제:
인기 키 TTL 만료 시
동시에 수천 요청이 DB로 몰림
→ DB 부하 폭발
@Service
public class CacheStampedeService {
private final StringRedisTemplate redisTemplate;
private final DistributedLockService lockService;
// 해결책 1: 뮤텍스 락
public Product getProductWithLock(Long productId) {
String cacheKey = "product:" + productId;
String lockKey = "lock:product:" + productId;
String token = UUID.randomUUID().toString();
// Cache 확인
String cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) return deserialize(cached);
// 락 획득 시도
if (lockService.acquireLock(lockKey, token, 3000)) {
try {
// Double Check (락 획득 사이 다른 스레드가 캐싱했을 수 있음)
cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) return deserialize(cached);
// DB 조회 + 캐싱
Product product = productRepository.findById(productId)
.orElseThrow();
redisTemplate.opsForValue()
.set(cacheKey, serialize(product),
Duration.ofMinutes(30));
return product;
} finally {
lockService.releaseLock(lockKey, token);
}
}
// 락 획득 실패 → 잠시 대기 후 재시도
Thread.sleep(50);
return getProductWithLock(productId);
}
// 해결책 2: TTL Jitter (만료 시간 분산)
public void setWithJitter(String key, String value, long baseTtlSeconds) {
long jitter = ThreadLocalRandom.current().nextLong(60);
redisTemplate.opsForValue()
.set(key, value, Duration.ofSeconds(baseTtlSeconds + jitter));
}
// 해결책 3: 논리적 만료 (Logical Expiration)
public Product getWithLogicalExpiration(Long productId) {
String cacheKey = "product:" + productId;
String cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
CacheWrapper wrapper = deserialize(cached, CacheWrapper.class);
// 만료됐으면 비동기로 갱신 (일단 구버전 반환)
if (wrapper.isExpired()) {
refreshAsync(productId); // 비동기 갱신
return wrapper.getData(); // 구버전 즉시 반환
}
return wrapper.getData();
}
return loadFromDb(productId);
}
@Async
public void refreshAsync(Long productId) {
Product product = productRepository.findById(productId)
.orElseThrow();
CacheWrapper wrapper = new CacheWrapper(
product,
LocalDateTime.now().plusMinutes(30)
);
redisTemplate.opsForValue()
.set("product:" + productId, serialize(wrapper),
Duration.ofMinutes(60)); // TTL은 길게 (논리적 만료로 관리)
}
}
4. 🔴 Keyspace Notification (키 만료 이벤트)
키가 만료되거나 삭제될 때 이벤트 발행
→ 만료 시 자동 처리 로직 구현 가능
# redis.conf 설정
notify-keyspace-events "Ex" # E: keyevent, x: expired
// 만료 이벤트 리스너
@Configuration
public class RedisKeyExpirationConfig {
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(
RedisConnectionFactory connectionFactory,
KeyExpirationEventListener listener
) {
RedisMessageListenerContainer container =
new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 만료 이벤트 구독
container.addMessageListener(
listener,
new PatternTopic("__keyevent@0__:expired")
);
return container;
}
}
@Component
@Slf4j
public class KeyExpirationEventListener implements MessageListener {
@Override
public void onMessage(Message message, byte[] pattern) {
String expiredKey = message.toString();
log.info("키 만료: {}", expiredKey);
// 만료된 키에 따라 처리
if (expiredKey.startsWith("session:")) {
handleSessionExpiration(expiredKey);
} else if (expiredKey.startsWith("lock:")) {
handleLockExpiration(expiredKey);
}
}
private void handleSessionExpiration(String key) {
// 세션 만료 시 로그아웃 처리
String userId = key.replace("session:", "");
log.info("세션 만료 → 로그아웃 처리: userId={}", userId);
}
private void handleLockExpiration(String key) {
// 락 만료 로그 기록
log.warn("락 TTL 만료 (비정상 종료 가능성): {}", key);
}
}
5. 🔴 Redis Streams (Kafka 유사 메시징)
Redis Pub/Sub: At Most Once (유실 가능)
Redis Streams: At Least Once (Kafka와 유사)
→ Consumer Group, Offset 관리
// Producer
@Service
@RequiredArgsConstructor
public class OrderEventProducer {
private final StringRedisTemplate redisTemplate;
public void publishOrder(Order order) {
Map<String, String> message = Map.of(
"orderId", String.valueOf(order.getId()),
"userId", String.valueOf(order.getUserId()),
"amount", String.valueOf(order.getAmount()),
"status", order.getStatus()
);
redisTemplate.opsForStream()
.add("order:stream", message);
log.info("주문 이벤트 발행: {}", order.getId());
}
}
// Consumer
@Service
@RequiredArgsConstructor
@Slf4j
public class OrderEventConsumer {
private final StringRedisTemplate redisTemplate;
@PostConstruct
public void init() {
// Consumer Group 생성
try {
redisTemplate.opsForStream()
.createGroup("order:stream", "order-group");
} catch (Exception e) {
// 이미 존재하면 무시
}
}
@Scheduled(fixedDelay = 100)
public void consume() {
// Consumer Group에서 읽기 (At Least Once)
List<MapRecord<String, Object, Object>> messages =
redisTemplate.opsForStream().read(
Consumer.from("order-group", "consumer-1"),
StreamReadOptions.empty().count(10),
StreamOffset.create("order:stream",
ReadOffset.lastConsumed())
);
if (messages == null) return;
messages.forEach(message -> {
try {
processOrder(message.getValue());
// 처리 완료 ACK (Kafka commitOffset과 동일)
redisTemplate.opsForStream()
.acknowledge("order:stream", "order-group",
message.getId());
} catch (Exception e) {
log.error("주문 처리 실패: {}", message.getId(), e);
// ACK 안 함 → 재처리 가능
}
});
}
}
6. 🟡 Slow Log 분석
// Slow Log 조회 및 모니터링
@Component
@Slf4j
@RequiredArgsConstructor
public class RedisSlowLogMonitor {
private final StringRedisTemplate redisTemplate;
@Scheduled(fixedDelay = 60_000) // 1분마다
public void checkSlowLog() {
redisTemplate.execute((RedisCallback<Void>) connection -> {
// Slow Log 조회 (10ms 이상)
List<Object> slowLogs = (List<Object>)
connection.execute("SLOWLOG", "GET".getBytes(), "10".getBytes());
if (slowLogs != null && !slowLogs.isEmpty()) {
log.warn("Slow Query 감지: {}개", slowLogs.size());
// 알람 발송
}
return null;
});
}
}
# redis.conf
slowlog-log-slower-than 10000 # 10ms 이상
slowlog-max-len 128
# 확인
SLOWLOG GET 10
SLOWLOG LEN
SLOWLOG RESET
7. 🟡 Circuit Breaker (Redis 장애 대응)
// Redis 장애 시 서비스 보호
@Service
@RequiredArgsConstructor
@Slf4j
public class ResilientCacheService {
private final StringRedisTemplate redisTemplate;
private final ProductRepository productRepository;
// Resilience4j Circuit Breaker 적용
@CircuitBreaker(name = "redis", fallbackMethod = "getFromDb")
public Product getProduct(Long productId) {
String cached = redisTemplate.opsForValue()
.get("product:" + productId);
if (cached != null) return deserialize(cached);
return loadAndCache(productId);
}
// Redis 장애 시 폴백 → DB 직접 조회
public Product getFromDb(Long productId, Exception e) {
log.warn("Redis 장애 → DB Fallback: {}", e.getMessage());
return productRepository.findById(productId).orElseThrow();
}
}
# application.yml
resilience4j:
circuitbreaker:
instances:
redis:
failure-rate-threshold: 50
wait-duration-in-open-state: 10s
sliding-window-size: 10
8. 🟡 Redis Pub/Sub
// Publisher
@Service
@RequiredArgsConstructor
public class NotificationPublisher {
private final StringRedisTemplate redisTemplate;
public void publish(String channel, String message) {
redisTemplate.convertAndSend(channel, message);
// At Most Once (구독자 없으면 유실!)
}
}
// Subscriber
@Configuration
public class PubSubConfig {
@Bean
public RedisMessageListenerContainer listenerContainer(
RedisConnectionFactory factory,
NotificationSubscriber subscriber
) {
RedisMessageListenerContainer container =
new RedisMessageListenerContainer();
container.setConnectionFactory(factory);
container.addMessageListener(
subscriber,
new ChannelTopic("notifications")
);
return container;
}
}
@Component
@Slf4j
public class NotificationSubscriber implements MessageListener {
@Override
public void onMessage(Message message, byte[] pattern) {
log.info("알림 수신: {}", message.toString());
// 처리 로직
}
}
전체 학습 로드맵
┌─────────────────────────────────────────────────────────────┐
│ Redis 학습 로드맵 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ✅ 완료 │
│ Single Thread / Queue / Event Loop │
│ Lua Script / Pipeline / MULTI/EXEC │
│ Connection Pool / SessionCallback │
│ Eviction Policy / Persistence │
│ Master/Replica / Hash Slot │
│ Distributed Lock / WATCH │
│ │
│ 🔴 다음 학습 (필수) │
│ Hot Key / Big Key │
│ Cache 전략 패턴 │
│ Cache Stampede │
│ Keyspace Notification │
│ Redis Streams │
│ │
│ 🟡 그 다음 (운영) │
│ Slow Log 분석 │
│ Memory 최적화 │
│ Circuit Breaker │
│ Pub/Sub │
│ │
│ 🟢 상황별 학습 │
│ HyperLogLog / Bitmap / Geo │
│ 무중단 마이그레이션 │
│ Sentinel 구성 │
│ │
└─────────────────────────────────────────────────────────────┘