[volume-5] 5주차 인덱스와 캐시를 사용한 성능 최적화 - #192
Conversation
- spring-boot-starter-cache + Caffeine 의존성 추가 - CacheConfig: TTL 5분, 최대 100개 엔트리 설정 - ProductFacade: 상품 목록 조회에 @Cacheable 적용 - 상품 등록/수정/삭제 시 @CacheEvict로 캐시 무효화 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 인덱스 적용 전 슬로우 쿼리 분석 (EXPLAIN, 측정 결과) - 복합 인덱스 설계 원칙 및 적용 결과 - 캐시 전략 (Caffeine 선택 이유, TTL, CacheEvict) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
📝 WalkthroughWalkthrough캐싱 기능을 상품 모듈에 추가한다. Caffeine 기반 캐시 매니저를 설정하고, ProductFacade의 상품 조회, 등록, 수정, 삭제 메서드에 캐싱 어노테이션을 적용한다. Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~15 minutes 운영 관점 검토사항캐시 정책의 적절성 5분의 만료 시간과 100개 항목의 최대 크기 설정이 실제 트래픽 패턴에 맞는지 검증이 필요하다. 상품 정보의 변경 빈도와 조회 빈도를 기반으로 TTL을 조정해야 한다. 캐시 키 설계 getProducts와 getProductsByBrandId 메서드의 캐시 키가 페이지네이션 파라미터(page, size, sort 등)를 포함하는지 확인이 필요하다. 서로 다른 페이징 요청이 캐시 충돌을 일으키지 않도록 보장해야 한다. 캐시 일관성 문제 registerProduct, updateProduct, deleteProduct 메서드에서 "products" 및 "products:brand" 캐시를 동시에 무효화한다. 그러나 부분 업데이트 시나리오에서 불필요한 캐시 무효화가 발생할 수 있으므로, 실제 비즈니스 요구사항에 따라 세분화된 무효화 전략 검토를 권장한다. 성능 테스트 캐시 적중률(hit ratio), 응답 시간 개선, 데이터베이스 조회 감소 정도를 측정하는 성능 테스트 작성이 필요하다. 또한 동시성 환경에서 캐시 스탬피드(cache stampede) 현상을 모니터링해야 한다. 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java`:
- Around line 30-31: The current CacheEvict on ProductFacade misses other write
paths so list caches stay stale; update the cache eviction to a single
centralized write path or add eviction hooks in the other mutating flows: add
eviction when likes change and when brand deletes affect products. Concretely,
either move `@CacheEvict`(value = {"products","products:brand"}, allEntries=true)
out of ProductFacade into the shared product write entry (or a new
ProductCacheEvictor invoked by all mutators), and ensure
ProductService.increaseLikes, ProductService.decreaseLikes and
ProductService.deleteByBrandId call that evictor (or publish a domain event
handled by the evictor). Also add integration tests that assert by-likes
reflects immediate like changes and that latest/by-brand lists drop products
after deleteByBrandId.
In `@apps/commerce-api/src/main/java/com/loopers/config/CacheConfig.java`:
- Around line 16-23: In CacheConfig update the cacheManager() bean so the
CaffeineCacheManager is transaction-aware by calling setTransactionAware(true)
on the CaffeineCacheManager instance (inside the cacheManager() method in class
CacheConfig), ensuring cache evictions occur after transaction commit; also add
an integration test targeting ProductFacade methods registerProduct,
updateProduct, and deleteProduct that simulates a concurrent read during a
modifying transaction to verify the "products" and "products:brand" caches are
not reloaded with pre-commit state.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 1c1a1567-8465-434f-ad6b-a9c2d7fc4253
⛔ Files ignored due to path filters (3)
doc/index-cache/cache-strategy.mdis excluded by!**/*.mdand included by**doc/index-cache/index-design-and-result.mdis excluded by!**/*.mdand included by**doc/index-cache/slow-query-before-index.mdis excluded by!**/*.mdand included by**
📒 Files selected for processing (3)
apps/commerce-api/build.gradle.ktsapps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.javaapps/commerce-api/src/main/java/com/loopers/config/CacheConfig.java
| @CacheEvict(value = {"products", "products:brand"}, allEntries = true) | ||
| @Transactional |
There was a problem hiding this comment.
목록 캐시 무효화가 일부 상품 변경 경로를 놓치고 있다.
현재 무효화는 ProductFacade의 등록·수정·삭제에만 걸려 있다. 그런데 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java:92-110의 increaseLikes/decreaseLikes는 좋아요 정렬 결과를 바꾸고, apps/commerce-api/src/main/java/com/loopers/application/brand/BrandFacade.java:45-54 → apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java:112-116의 deleteByBrandId는 목록 포함 대상을 바꾸는데도 이 캐시를 비우지 않는다. 운영에서는 by-likes, latest, by-brand 응답이 최대 5분간 stale 상태로 남는다. 무효화를 ProductFacade가 아니라 상품 쓰기 공통 경로로 올리거나, 도메인 이벤트/전용 evictor로 모아 모든 변경이 한 지점을 통과하게 해야 한다. 좋아요 증가·감소 직후 by-likes 정렬이 즉시 반영되는지, 브랜드 삭제 후 latest와 by-brand 목록에서 해당 상품이 사라지는지 통합 테스트를 추가해야 한다.
Also applies to: 44-45, 51-52, 66-67, 83-84
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java`
around lines 30 - 31, The current CacheEvict on ProductFacade misses other write
paths so list caches stay stale; update the cache eviction to a single
centralized write path or add eviction hooks in the other mutating flows: add
eviction when likes change and when brand deletes affect products. Concretely,
either move `@CacheEvict`(value = {"products","products:brand"}, allEntries=true)
out of ProductFacade into the shared product write entry (or a new
ProductCacheEvictor invoked by all mutators), and ensure
ProductService.increaseLikes, ProductService.decreaseLikes and
ProductService.deleteByBrandId call that evictor (or publish a domain event
handled by the evictor). Also add integration tests that assert by-likes
reflects immediate like changes and that latest/by-brand lists drop products
after deleteByBrandId.
| @Bean | ||
| public CacheManager cacheManager() { | ||
| CaffeineCacheManager cacheManager = new CaffeineCacheManager("products", "products:brand"); | ||
| cacheManager.setCaffeine(Caffeine.newBuilder() | ||
| .expireAfterWrite(5, TimeUnit.MINUTES) | ||
| .maximumSize(100) | ||
| ); | ||
| return cacheManager; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In Spring Framework, when a @transactionalmethod also uses@CacheEvict with CaffeineCacheManager, are evictions deferred until transaction commit by default? What is the default behavior and effect of CaffeineCacheManager#setTransactionAware?
💡 Result:
No. With Spring’s caching abstraction, a @CacheEvict on a @Transactional method is not deferred to transaction commit by default; the eviction is performed when the cache advice runs (by default after the method returns, but still before the surrounding transaction commits). If the transaction later rolls back, the cache eviction is not automatically rolled back.
What setTransactionAware does (and its default):
setTransactionAware(false)(default) means cacheput/evict/clearhappen immediately (not synchronized with the transaction). [1]setTransactionAware(true)makes Spring expose transaction-aware cache decorators so thatput/evict/clearare executed only in the “after-commit” phase of a successful Spring-managed transaction; if there is no transaction, they execute immediately. [1][2]
So enabling transaction awareness is what makes evictions effectively “deferred until commit” (and skipped on rollback). [1][2]
Sources
[1] Spring Framework Javadoc — AbstractTransactionSupportingCacheManager#setTransactionAware (default false; after-commit semantics) (docs.spring.io)
[2] Spring Framework Javadoc — TransactionAwareCacheDecorator (put/evict/clear performed after commit) (docs.spring.io)
Citations:
- 1: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/cache/transaction/AbstractTransactionSupportingCacheManager.html
- 2: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/cache/transaction/TransactionAwareCacheDecorator.html?utm_source=openai
🏁 Script executed:
# Search for ProductFacade and its use of `@CacheEvict`
fd -t f "ProductFacade" | head -20Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 167
🏁 Script executed:
# Search for `@CacheEvict` usage in the codebase
rg "@CacheEvict" --type java -A 2 -B 2Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1869
🏁 Script executed:
# Check CaffeineCacheManager inheritance and setTransactionAware availability
rg "CaffeineCacheManager" --type java -A 5 -B 1Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1640
트랜잭션 커밋 전에 캐시가 비워져 오래된 목록이 다시 적재될 수 있다.
현재 구조에서는 ProductFacade의 registerProduct, updateProduct, deleteProduct 메서드가 @Transactional과 @CacheEvict를 함께 사용하므로, 캐시 무효화가 커밋 이후로 지연되지 않는다. 기본값으로는 메서드 반환 직후 캐시가 즉시 비워지지만 여전히 트랜잭션 커밋 전이므로, 이 사이에 다른 요청이 "products"와 "products:brand" 캐시를 조회하면 이전 DB 상태를 캐시에 재적재하게 된다. 운영에서는 수정·삭제 직후에도 최대 TTL(5분) 동안 stale 목록이 제공되는 문제로 이어진다.
CaffeineCacheManager에 setTransactionAware(true)를 설정하여 커밋 후 단계에서만 무효화가 수행되도록 보장해야 한다. 이와 함께 register/update/delete 중 하나를 의도적으로 지연시키는 통합 테스트를 추가하여, 동시 조회가 커밋 전 상태를 캐시에 재적재하지 않는지 검증해야 한다.
수정 예시
`@Bean`
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("products", "products:brand");
+ cacheManager.setTransactionAware(true);
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(100)
);
return cacheManager;
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/commerce-api/src/main/java/com/loopers/config/CacheConfig.java` around
lines 16 - 23, In CacheConfig update the cacheManager() bean so the
CaffeineCacheManager is transaction-aware by calling setTransactionAware(true)
on the CaffeineCacheManager instance (inside the cacheManager() method in class
CacheConfig), ensuring cache evictions occur after transaction commit; also add
an integration test targeting ProductFacade methods registerProduct,
updateProduct, and deleteProduct that simulates a concurrent read during a
modifying transaction to verify the "products" and "products:brand" caches are
not reloaded with pre-commit state.
|
@coderabbitai 아래 체크리스트에 충족될 수 있도록 구현되었는지 확인해. 🔖 Index
❤️ Structure
⚡ Cache
|
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 78375
🔖 Index
❤️ Structure
⚡ Cache
종합
🧠 Learnings used |
📌 Summary
🧭 Context & Decision
문제 정의
선택지와 결정
🏗️ Design Overview
변경 범위
CacheConfig, Caffeine 의존성,@Cacheable/@CacheEvict적용주요 컴포넌트 책임
CacheConfig: Caffeine 캐시 매니저 설정 (TTL 5분, 최대 100개)ProductFacade: 조회 시@Cacheable로 캐시 hit/miss 처리, 변경 시@CacheEvict로 즉시 무효화DB 인덱스 (DDL 직접 적용)
🔁 Flow Diagram
Main Flow — 캐시 hit
Main Flow — 캐시 miss
캐시 무효화 Flow
sequenceDiagram autonumber participant Admin participant Controller participant ProductFacade participant CaffeineCache participant ProductService participant DB Admin->>Controller: PUT /api/admin/v1/products/{id} Controller->>ProductFacade: updateProduct(id, command) ProductFacade->>ProductService: update(id, command) ProductService->>DB: UPDATE products DB-->>ProductService: 완료 ProductFacade->>CaffeineCache: @CacheEvict 전체 삭제 ProductFacade-->>Controller: ProductInfo Controller-->>Admin: 200 OK목적: 50M 행 products 테이블의 상품 조회 쿼리 성능 개선 (응답 시간 23초 → 1초 이하 단축).
핵심 변경점: Caffeine 로컬 캐시(TTL 5분, 최대 100개) 도입으로 ProductFacade의 getProducts, getProductsByBrandId 메서드에 @Cacheable 적용하고, 등록/수정/삭제 시 @CacheEvict로 캐시 전체 무효화. DB 인덱스 3개(deleted_at+created_at, brand_id+deleted_at+price, deleted_at+likes_count) 생성 필요.
리스크/주의사항: (1) PR 목표에서 DB 인덱스 DDL 적용을 언급했으나 코드에 마이그레이션 파일(Flyway V*.sql)이 없는데, 인덱스가 별도로 적용되었는지 확인 필요인지요? (2) Caffeine 로컬 캐시는 다중 인스턴스 환경에서 인스턴스 간 일관성 미보장 - Redis 전환 시기는 언제를 계획 중인지요? (3) allEntries=true로 전체 캐시 무효화하므로 대량 업데이트 시 성능 영향 가능.
테스트/검증: ProductFacadeTest 추가 작성 필요 (캐시 hit/miss 시나리오)하고 기존 ProductV1ApiE2ETest 실행 확인 필요. 실제 DB 인덱스 생성 및 쿼리 실행 계획(EXPLAIN) 검증 완료되었는지 확인 필요한지요?