๋ฉด์ ๊ด: "๋๊ท๋ชจ ์ด์ปค๋จธ์ค ์์คํ ์์ ํจ์จ์ ์ธ ์บ์ฑ ์ ๋ต์ ์ค๊ณํด์ฃผ์ธ์. ํนํ ์ํ ์ ๋ณด์ ๊ฐ์ด ์์ฃผ ์กฐํ๋๋ ๋ฐ์ดํฐ์ ์ฒ๋ฆฌ ๋ฐฉ๋ฒ์ ๋ํด ์ค๋ช ํด์ฃผ์ธ์."
์ง์์: ๋ค, ๋จผ์ ๋ช ๊ฐ์ง ์ง๋ฌธ์ ๋๋ ค๋ ๋ ๊น์?
๋ฉด์ ๊ด: ๋ค, ๋ง์ํด์ฃผ์ธ์.
์ง์์: ๋ค์ ์ฌํญ๋ค์ ํ์ธํ๊ณ ์ถ์ต๋๋ค:
- ์บ์ํด์ผ ํ ์ฃผ์ ๋ฐ์ดํฐ์ ํฌ๊ธฐ์ ์ ํ์ ์ด๋ป๊ฒ ๋๋์?
- ๋ฐ์ดํฐ ๊ฐฑ์ ๋น๋๋ ์ด๋ ์ ๋์ธ๊ฐ์?
- ์บ์ ํํธ์จ(Cache Hit Ratio)์ ๋ํ ๋ชฉํ๊ฐ ์๋์?
- ๋ฐ์ดํฐ ์ผ๊ด์ฑ์ ๋ํ ์๊ตฌ์ฌํญ์ ์ด๋ป๊ฒ ๋๋์?
๋ฉด์ ๊ด:
- ์ํ ์ ๋ณด๋ ๊ฐ๊ฐ ์ฝ 1KB ํฌ๊ธฐ์ด๋ฉฐ, ์ด 100๋ง ๊ฐ์ ์ํ์ด ์์ต๋๋ค. ์ํ ์์ธ, ๊ฐ๊ฒฉ, ์ฌ๊ณ ๊ฐ ์ฃผ์ ์ ๋ณด์ ๋๋ค.
- ์ํ ๊ฐ๊ฒฉ๊ณผ ์ฌ๊ณ ๋ ์ค์๊ฐ์ฑ์ด ์ค์ํ๋ฉฐ, ์ํ ์์ธ ์ ๋ณด๋ ํ๋ฃจ ํ๊ท 10% ์ ๋๊ฐ ๊ฐฑ์ ๋ฉ๋๋ค.
- ์บ์ ํํธ์จ 95% ์ด์์ ๋ชฉํ๋ก ํฉ๋๋ค.
- ๊ฐ๊ฒฉ๊ณผ ์ฌ๊ณ ๋ ๊ฐํ ์ผ๊ด์ฑ์ด, ์ํ ์์ธ๋ ์ต๋ 5๋ถ์ ์ง์ฐ์ด ํ์ฉ๋ฉ๋๋ค.
์ง์์: ์ดํดํ์ต๋๋ค. ๊ทธ๋ฌ๋ฉด ๊ณ์ธต๋ณ ์บ์ฑ ์ ๋ต์ ์ค๊ณํด๋ณด๊ฒ ์ต๋๋ค.
@Service
public class CacheLayerService {
private final LoadingCache<String, String> localCache; // L1 Cache
private final RedisTemplate<String, String> redisCache; // L2 Cache
public CacheLayerService() {
this.localCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(5))
.build(key -> getFromRedis(key));
}
public Optional<Product> getProduct(String productId) {
try {
// 1. Local Cache ์กฐํ
return Optional.ofNullable(localCache.get(productId));
} catch (CacheLoadingException e) {
// 2. Redis Cache ์กฐํ
return Optional.ofNullable(redisCache.opsForValue().get(productId));
}
}
}
๋ฉด์ ๊ด: ๊ฐ๊ฒฉ๊ณผ ์ฌ๊ณ ๊ฐ์ ์ค์๊ฐ์ฑ์ด ์ค์ํ ๋ฐ์ดํฐ๋ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ์ค ๊ฑด๊ฐ์?
์ง์์: Write-Through ์ ๋ต๊ณผ ์บ์ ๋ฌดํจํ๋ฅผ ์กฐํฉํ์ฌ ๊ตฌํํ๊ฒ ์ต๋๋ค.
@Service
@Slf4j
public class ProductPriceService {
private final RedisTemplate<String, String> redisTemplate;
private final PriceRepository priceRepository;
@Transactional
public void updatePrice(String productId, BigDecimal newPrice) {
try {
// 1. DB ์
๋ฐ์ดํธ
priceRepository.updatePrice(productId, newPrice);
// 2. ์บ์ ์
๋ฐ์ดํธ (Write-Through)
String cacheKey = "price:" + productId;
redisTemplate.opsForValue().set(cacheKey, newPrice.toString());
// 3. ์ด๋ฒคํธ ๋ฐํ (๋ค๋ฅธ ์๋น์ค์ ํต์ง)
eventPublisher.publishPriceChange(productId, newPrice);
} catch (Exception e) {
// 4. ์คํจ ์ ์บ์ ๋ฌดํจํ
redisTemplate.delete("price:" + productId);
throw e;
}
}
}
๋ฉด์ ๊ด: ์บ์ ์ผ๊ด์ฑ์ ์ด๋ป๊ฒ ๋ณด์ฅํ์ค ๊ฑด๊ฐ์?
- ๋ถ์ฐ ์บ์ Lock ๊ตฌํ
@Service
public class DistributedCacheLockService {
private final RedisTemplate<String, String> redisTemplate;
public boolean acquireLock(String key, long timeoutMs) {
String lockKey = "lock:" + key;
String lockValue = UUID.randomUUID().toString();
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue,
Duration.ofMillis(timeoutMs));
return Boolean.TRUE.equals(acquired);
}
@Async
public void updateWithLock(String productId, Runnable updateAction) {
String lockKey = "lock:" + productId;
try {
if (acquireLock(lockKey, 5000)) { // 5์ด ํ์์์
updateAction.run();
} else {
throw new CacheLockException("Lock acquisition failed");
}
} finally {
releaseLock(lockKey);
}
}
}
- TTL(Time To Live) ๊ธฐ๋ฐ ์บ์ ๊ฐฑ์
@Component
public class CacheRefreshStrategy {
@Scheduled(fixedRate = 300000) // 5๋ถ๋ง๋ค ์คํ
public void refreshExpiredCache() {
// 1. TTL์ด ์๋ฐํ ์บ์ ์กฐํ
Set<String> expiringKeys = redisTemplate
.keys("product:*")
.stream()
.filter(this::isExpiringSoon)
.collect(Collectors.toSet());
// 2. ๋ฐฑ๊ทธ๋ผ์ด๋์์ ๊ฐฑ์
expiringKeys.forEach(key ->
CompletableFuture.runAsync(() -> refreshCache(key)));
}
private boolean isExpiringSoon(String key) {
Long ttl = redisTemplate.getExpire(key);
return ttl != null && ttl < 300; // 5๋ถ ๋ฏธ๋ง ๋จ์ ์บ์
}
}
- ์ด๋ฒคํธ ๊ธฐ๋ฐ ์บ์ ๋๊ธฐํ
@Service
public class CacheEventHandler {
@KafkaListener(topics = "cache-invalidation")
public void handleCacheInvalidation(CacheInvalidationEvent event) {
switch(event.getType()) {
case PRODUCT_UPDATE:
invalidateProductCache(event.getProductId());
break;
case PRICE_UPDATE:
invalidatePriceCache(event.getProductId());
break;
case STOCK_UPDATE:
invalidateStockCache(event.getProductId());
break;
}
}
private void invalidateProductCache(String productId) {
// ๋ก์ปฌ ์บ์ ๋ฌดํจํ
localCache.invalidate(productId);
// Redis ์บ์ ๋ฌดํจํ
redisTemplate.delete("product:" + productId);
// ๊ด๋ จ ์ง๊ณ ์บ์ ๋ฌดํจํ
redisTemplate.delete("category:" +
productRepository.getCategoryId(productId));
}
}
- ์บ์ ์ ํฉ์ฑ ๊ฒ์ฆ
@Service
public class CacheValidationService {
@Scheduled(cron = "0 */10 * * * *") // 10๋ถ๋ง๋ค ์คํ
public void validateCacheConsistency() {
// 1. ์ํ๋ง๋ ์บ์ ํค ์ ํ
List<String> sampleKeys = getSampleCacheKeys();
// 2. DB์ ์บ์ ๋ฐ์ดํฐ ๋น๊ต
Map<String, InconsistencyReport> inconsistencies =
checkConsistency(sampleKeys);
// 3. ๋ถ์ผ์น ์ฒ๋ฆฌ
if (!inconsistencies.isEmpty()) {
handleInconsistencies(inconsistencies);
}
}
private Map<String, InconsistencyReport> checkConsistency(List<String> keys) {
return keys.stream()
.map(this::compareWithDatabase)
.filter(InconsistencyReport::hasDiscrepancy)
.collect(Collectors.toMap(
InconsistencyReport::getKey,
Function.identity()
));
}
private void handleInconsistencies(Map<String, InconsistencyReport> reports) {
reports.forEach((key, report) -> {
// ๋ก๊ทธ ๊ธฐ๋ก
log.warn("Cache inconsistency detected for key: {}", key);
// ๋ฉํธ๋ฆญ ๊ธฐ๋ก
meterRegistry.counter("cache.inconsistency").increment();
// ์บ์ ์ฌ๊ตฌ์ฑ
rebuildCache(key);
});
}
}
๋ฉด์ ๊ด: Hot Key ๋ฌธ์ ๋ ์ด๋ป๊ฒ ํด๊ฒฐํ์๊ฒ ์ต๋๊น?
์ง์์: Hot Key ๋ฌธ์ ์ ๋ํด์๋ ๋ค์๊ณผ ๊ฐ์ ์ ๋ต์ ์ฌ์ฉํ๊ฒ ์ต๋๋ค:
@Service
public class HotKeyHandlingService {
private final LoadingCache<String, String> localCache;
private final RedisTemplate<String, String> redisTemplate;
// Hot Key ์ค๋ฉ ์ฒ๋ฆฌ
public String getWithSharding(String key, int shardCount) {
int shard = Math.abs(key.hashCode() % shardCount);
String shardedKey = key + ":shard:" + shard;
// ๋ก์ปฌ ์บ์ ํ์ธ
String value = localCache.getIfPresent(key);
if (value != null) {
return value;
}
// Redis ์บ์ ํ์ธ
return redisTemplate.opsForValue().get(shardedKey);
}
// Hot Key ๋ชจ๋ํฐ๋ง
@Scheduled(fixedRate = 1000)
public void monitorHotKeys() {
RedisCallback<Set<String>> callback = connection -> {
// Redis INFO ๋ช
๋ น์ด๋ก Hot Key ๊ฐ์ง
return connection.info("commandstats");
};
Set<String> hotKeys = redisTemplate.execute(callback);
// Hot Key ๋ฐ๊ฒฌ ์ ์ค๋ฉ ์ฒ๋ฆฌ
hotKeys.forEach(this::handleHotKey);
}
}
์ด๋ฌํ ์ ๋ต๋ค์ ํตํด ์บ์์ ์ผ๊ด์ฑ์ ๋ณด์ฅํ๋ฉด์๋ ์ฑ๋ฅ์ ์ต์ ํํ ์ ์์ต๋๋ค.