Skip to content

Latest commit

ย 

History

History
269 lines (215 loc) ยท 8.64 KB

caching.md

File metadata and controls

269 lines (215 loc) ยท 8.64 KB

์บ์‹ฑ ์ „๋žต ์„ค๊ณ„ ๋ฉด์ ‘

๋ฉด์ ‘๊ด€: "๋Œ€๊ทœ๋ชจ ์ด์ปค๋จธ์Šค ์‹œ์Šคํ…œ์—์„œ ํšจ์œจ์ ์ธ ์บ์‹ฑ ์ „๋žต์„ ์„ค๊ณ„ํ•ด์ฃผ์„ธ์š”. ํŠนํžˆ ์ƒํ’ˆ ์ •๋ณด์™€ ๊ฐ™์ด ์ž์ฃผ ์กฐํšŒ๋˜๋Š” ๋ฐ์ดํ„ฐ์˜ ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”."

์ง€์›์ž: ๋„ค, ๋จผ์ € ๋ช‡ ๊ฐ€์ง€ ์งˆ๋ฌธ์„ ๋“œ๋ ค๋„ ๋ ๊นŒ์š”?

๋ฉด์ ‘๊ด€: ๋„ค, ๋ง์”€ํ•ด์ฃผ์„ธ์š”.

์ง€์›์ž: ๋‹ค์Œ ์‚ฌํ•ญ๋“ค์„ ํ™•์ธํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค:

  1. ์บ์‹œํ•ด์•ผ ํ•  ์ฃผ์š” ๋ฐ์ดํ„ฐ์˜ ํฌ๊ธฐ์™€ ์œ ํ˜•์€ ์–ด๋–ป๊ฒŒ ๋˜๋‚˜์š”?
  2. ๋ฐ์ดํ„ฐ ๊ฐฑ์‹  ๋นˆ๋„๋Š” ์–ด๋Š ์ •๋„์ธ๊ฐ€์š”?
  3. ์บ์‹œ ํžˆํŠธ์œจ(Cache Hit Ratio)์— ๋Œ€ํ•œ ๋ชฉํ‘œ๊ฐ€ ์žˆ๋‚˜์š”?
  4. ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ์— ๋Œ€ํ•œ ์š”๊ตฌ์‚ฌํ•ญ์€ ์–ด๋–ป๊ฒŒ ๋˜๋‚˜์š”?

๋ฉด์ ‘๊ด€:

  1. ์ƒํ’ˆ ์ •๋ณด๋Š” ๊ฐ๊ฐ ์•ฝ 1KB ํฌ๊ธฐ์ด๋ฉฐ, ์ด 100๋งŒ ๊ฐœ์˜ ์ƒํ’ˆ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ƒํ’ˆ ์ƒ์„ธ, ๊ฐ€๊ฒฉ, ์žฌ๊ณ ๊ฐ€ ์ฃผ์š” ์ •๋ณด์ž…๋‹ˆ๋‹ค.
  2. ์ƒํ’ˆ ๊ฐ€๊ฒฉ๊ณผ ์žฌ๊ณ ๋Š” ์‹ค์‹œ๊ฐ„์„ฑ์ด ์ค‘์š”ํ•˜๋ฉฐ, ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด๋Š” ํ•˜๋ฃจ ํ‰๊ท  10% ์ •๋„๊ฐ€ ๊ฐฑ์‹ ๋ฉ๋‹ˆ๋‹ค.
  3. ์บ์‹œ ํžˆํŠธ์œจ 95% ์ด์ƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค.
  4. ๊ฐ€๊ฒฉ๊ณผ ์žฌ๊ณ ๋Š” ๊ฐ•ํ•œ ์ผ๊ด€์„ฑ์ด, ์ƒํ’ˆ ์ƒ์„ธ๋Š” ์ตœ๋Œ€ 5๋ถ„์˜ ์ง€์—ฐ์ด ํ—ˆ์šฉ๋ฉ๋‹ˆ๋‹ค.

์ง€์›์ž: ์ดํ•ดํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ๊ณ„์ธต๋ณ„ ์บ์‹ฑ ์ „๋žต์„ ์„ค๊ณ„ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

1. ๋ฉ€ํ‹ฐ ๋ ˆ์ด์–ด ์บ์‹ฑ ์•„ํ‚คํ…์ฒ˜

@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;
        }
    }
}

๋ฉด์ ‘๊ด€: ์บ์‹œ ์ผ๊ด€์„ฑ์€ ์–ด๋–ป๊ฒŒ ๋ณด์žฅํ•˜์‹ค ๊ฑด๊ฐ€์š”?

2. ์บ์‹œ ์ผ๊ด€์„ฑ ๋ณด์žฅ ์ „๋žต

  1. ๋ถ„์‚ฐ ์บ์‹œ 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);
        }
    }
}
  1. 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๋ถ„ ๋ฏธ๋งŒ ๋‚จ์€ ์บ์‹œ
    }
}
  1. ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์บ์‹œ ๋™๊ธฐํ™”
@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));
    }
}
  1. ์บ์‹œ ์ •ํ•ฉ์„ฑ ๊ฒ€์ฆ
@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);
    }
}

์ด๋Ÿฌํ•œ ์ „๋žต๋“ค์„ ํ†ตํ•ด ์บ์‹œ์˜ ์ผ๊ด€์„ฑ์„ ๋ณด์žฅํ•˜๋ฉด์„œ๋„ ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.