Redis 캐싱 전략 - Cache Aside, Write Through, Write Behind



Redis를 활용한 캐싱 전략을 이해하면 애플리케이션 성능을 크게 향상시킬 수 있습니다.



캐싱 전략 비교








전략읽기쓰기일관성
Cache Aside캐시 → DBDB 직접낮음
Write Through캐시캐시 + DB 동시높음
Write Behind캐시캐시 → 비동기 DB중간
Read Through캐시 자동 로드-중간


Cache Aside (Lazy Loading)


// 읽기
public User getUser(Long id) {
String key = "user:" + id;
User user = redisTemplate.opsForValue().get(key);

if (user == null) { // Cache Miss
user = userRepository.findById(id);
redisTemplate.opsForValue().set(key, user, Duration.ofHours(1));
}
return user;
}

// 쓰기 (캐시 무효화)
public void updateUser(User user) {
userRepository.save(user);
redisTemplate.delete("user:" + user.getId()); // 캐시 삭제
}


Write Through


// 쓰기 시 캐시와 DB 동시 업데이트
@Transactional
public void updateUser(User user) {
userRepository.save(user);
redisTemplate.opsForValue().set("user:" + user.getId(), user);
}


Write Behind (Write Back)


// 캐시에 먼저 쓰고 나중에 DB 동기화
public void updateUser(User user) {
redisTemplate.opsForValue().set("user:" + user.getId(), user);

// 비동기로 DB 동기화 (배치 처리)
asyncDbWriter.enqueue(user);
}

// 주기적으로 배치 처리
@Scheduled(fixedRate = 5000)
public void flushToDatabase() {
List<User> users = asyncDbWriter.drainQueue();
userRepository.saveAll(users);
}


TTL (Time To Live) 설정


// 고정 TTL
redisTemplate.opsForValue().set(key, value, Duration.ofMinutes(30));

// 조건부 TTL
long ttl = isPopular(id) ? 3600 : 300; // 인기 데이터는 1시간
redisTemplate.expire(key, Duration.ofSeconds(ttl));


캐시 Stampede 방지


// 분산 락으로 중복 조회 방지
public User getUserSafe(Long id) {
String key = "user:" + id;
User user = redisTemplate.opsForValue().get(key);

if (user == null) {
String lockKey = "lock:user:" + id;
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", Duration.ofSeconds(10));

if (locked) {
try {
user = userRepository.findById(id);
redisTemplate.opsForValue().set(key, user);
} finally {
redisTemplate.delete(lockKey);
}
} else {
Thread.sleep(100); // 대기 후 재시도
return getUserSafe(id);
}
}
return user;
}


Spring Cache 통합


@Cacheable(value = "users", key = "#id")
public User getUser(Long id) {
return userRepository.findById(id);
}

@CacheEvict(value = "users", key = "#user.id")
public void updateUser(User user) {
userRepository.save(user);
}

@CachePut(value = "users", key = "#user.id")
public User createUser(User user) {
return userRepository.save(user);
}


전략 선택 가이드



  • Cache Aside: 읽기 위주, 단순한 구현

  • Write Through: 쓰기 일관성 중요

  • Write Behind: 쓰기 성능 중요, 약간의 데이터 손실 허용