
Redis 캐시는 자주 사용되는 데이터를 메모리에 저장하여 빠르게 접근할 수 있는 도구입니다.
하지만 캐시를 잘못 설계하거나 적절히 사용하지 않으면,
캐싱이 오히려 성능 병목을 일으킬 수 있습니다.
이번 포스팅에서는 Redis를 이용해 특정 API 호출에서 발생한 성능 저하 문제를 분석하고,
캐시 전략을 개선하여 응답 속도를 대폭 줄인 사례를 소개합니다.
@Service
@Slf4j
public class OrderProcessingService {
private final RedisTemplate<String, Object> redisTemplate;
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
private static final String STOCK_KEY_PREFIX = "stock:";
private static final String ORDER_LOCK_PREFIX = "order_lock:";
@Autowired
public OrderProcessingService(RedisTemplate<String, Object> redisTemplate,
OrderRepository orderRepository,
ProductRepository productRepository) {
this.redisTemplate = redisTemplate;
this.orderRepository = orderRepository;
this.productRepository = productRepository;
}
@Transactional
public OrderResult processOrder(OrderRequest request) {
String lockKey = ORDER_LOCK_PREFIX + request.getProductId();
String stockKey = STOCK_KEY_PREFIX + request.getProductId();
try {
// Redis를 활용한 분산 락 획득 (Redisson 사용)
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "LOCKED", Duration.ofSeconds(3));
if (!locked) {
return OrderResult.builder()
.success(false)
.message("주문 처리 중 입니다. 잠시 후 다시 시도해주세요.")
.build();
}
// Redis에서 재고 확인
Integer currentStock = (Integer) redisTemplate.opsForValue().get(stockKey);
if (currentStock == null || currentStock < request.getQuantity()) {
return OrderResult.builder()
.success(false)
.message("재고가 부족합니다.")
.build();
}
// 재고 감소
redisTemplate.opsForValue().set(stockKey, currentStock - request.getQuantity());
// 주문 정보 저장 (비동기 처리)
CompletableFuture.runAsync(() -> {
Order order = Order.builder()
.productId(request.getProductId())
.quantity(request.getQuantity())
.userId(request.getUserId())
.orderStatus(OrderStatus.COMPLETED)
.build();
orderRepository.save(order);
// 재고 정보 DB 동기화 (배치 처리)
redisTemplate.opsForList()
.rightPush("stock_sync_queue", new StockSyncEvent(request.getProductId(), currentStock - request.getQuantity()));
});
return OrderResult.builder()
.success(true)
.message("주문이 성공적으로 처리되었습니다.")
.build();
} finally {
redisTemplate.delete(lockKey);
}
}
}


이번 Redis 학습 포스팅은 사용자 요청이 빈번히 발생하는 사용자 프로필 조회 API에 적용되었습니다.
이 접근 방식은 반복적인 데이터 요청이 많은 서비스에서 유용하게 활용될 수 있습니다.
@Recover
public OrderResult fallbackProcess(RedisConnectionFailureException ex, OrderRequest request) {
// MySQL을 이용한 폴백 처리 로직
return processOrderWithMySQL(request);
}
@Scheduled(fixedRate = 300000) // 5분마다 실행
public void checkDataConsistency() {
List<Product> products = productRepository.findAll();
for (Product product : products) {
String redisStock = redisTemplate.opsForValue().get(STOCK_KEY_PREFIX + product.getId());
if (!product.getStock().equals(Integer.valueOf(redisStock))) {
log.error("재고 불일치 발생: productId={}, DBStock={}, RedisStock={}",
product.getId(), product.getStock(), redisStock);
// 알림 발송
alertService.sendAlert(new StockMismatchAlert(product.getId()));
}
}
}
Redis 캐시는 잘 설계될 경우 데이터베이스 부담을 줄이고 응답 속도를 크게 개선할 수 있습니다.
하지만 잘못된 캐싱 전략은 오히려 병목 현상을 초래할 수 있으므로, 성능 측정과 적절한 설계가 중요합니다.
이번 사례를 통해 캐싱 설계의 중요성과 성능 최적화를 경험할 수 있었습니다.