[volume - 5] 인덱스 추가 및 캐시 설계 - 김윤선#217
[volume - 5] 인덱스 추가 및 캐시 설계 - 김윤선#217kimyam1008 wants to merge 4 commits intoLoopers-dev-lab:kimyam1008from
Conversation
- 단건/목록 조회 캐시 구현 (단건 TTL 1시간, 목록 TTL 1분) - 단건/전체/브랜드별 목록 조회 캐시 적용, 변경/삭제 시 evict - 좋아요 증가/감소 시 상품 단건 캐시 evict - 목록 likeCount는 TTL 허용, 단건은 즉시 evict로 정합성 수준 분리 - 캐시 Miss/장애 시 DB fallback으로 서비스 정상 동작 보장
📝 WalkthroughWalkthrough이 PR은 상품 및 좋아요 캐싱을 위해 Redis 기반 ProductCacheStore를 도입하고, LikeFacade와 ProductFacade에 캐시 무효화 로직을 추가하며, 데이터베이스 성능 최적화를 위해 Product 테이블에 복합 인덱스 3개를 추가한다. Changes
Sequence DiagramsequenceDiagram
participant Client
participant ProductFacade
participant ProductCacheStore
participant ProductRepository
participant Database
Client->>ProductFacade: getProduct(productId)
ProductFacade->>ProductCacheStore: getProduct(productId)
alt Cache Hit
ProductCacheStore-->>ProductFacade: Optional<ProductInfo>
else Cache Miss
ProductCacheStore-->>ProductFacade: Optional.empty()
ProductFacade->>ProductRepository: findById(productId)
ProductRepository->>Database: SELECT * FROM products
Database-->>ProductRepository: ProductEntity
ProductRepository-->>ProductFacade: ProductEntity
ProductFacade->>ProductCacheStore: putProduct(productId, ProductInfo)
ProductCacheStore-->>ProductFacade: cached
end
ProductFacade-->>Client: ProductInfo
Client->>ProductFacade: updateProduct(productId, ...)
ProductFacade->>ProductRepository: update(productId, ...)
ProductRepository->>Database: UPDATE products SET ...
Database-->>ProductRepository: success
ProductRepository-->>ProductFacade: updated
ProductFacade->>ProductCacheStore: evictProduct(productId)
ProductCacheStore-->>ProductFacade: evicted
ProductFacade-->>Client: success
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30분 Possibly related PRs
운영/성능 관점 검토사항1. 캐시 일관성 전략 확인 필요
2. 캐시 Warmup 전략 부재
3. Redis 연결 장애 처리 검증
4. 데이터베이스 인덱스 생성 전 성능 검증
5. 동시성 테스트 범위 확대
🚥 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)
📝 Coding Plan
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 Tip CodeRabbit can generate a title for your PR based on the changes with custom instructions.Set the |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java (1)
43-50:⚠️ Potential issue | 🟡 MinorLikeFacade 캐시 무효화 동작에 대한 단위 테스트 검증이 없다
운영 관점에서 좋아요/취소 후 캐시가 무효화되지 않아도 현재 테스트는 이를 잡아내지 못한다. 수정안으로
ProductCacheStore목을 멤버로 두고 like/unlike 성공 경로에서evictProduct(productId)호출을 검증해야 한다. 추가 테스트로 중복 좋아요/좋아요 없음 취소 같은 no-op 경로에서는evictProduct가 호출되지 않음을 함께 검증해야 한다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java` around lines 43 - 50, The tests for LikeFacade lack verification that ProductCacheStore eviction occurs; replace the inline mock(ProductCacheStore.class) with a member mock field (e.g., productCacheStore) and inject it into the LikeFacade constructor used in LikeFacadeTest; then add/modify tests to assert that on successful like/unlike flows the mock's evictProduct(productId) is invoked (verify(evictProduct) called with the expected productId after calling LikeFacade.like/unlike), and add tests for no-op paths (duplicate like or removing non-existent like) to verify evictProduct is NOT called (verifyNoInteractions or verify(..., never())). Ensure tests exercise the actual service paths (using LikeService and repository mocks) so the eviction verification is meaningful.
🤖 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/like/LikeFacade.java`:
- Line 85: productCacheStore.evictProduct is being called immediately (e.g., in
the methods where productCacheStore.evictProduct is invoked) which can cause
stale DB reads to be re-cached before the write transaction commits; change
those immediate evictions to run in an after-commit callback (use
TransactionSynchronizationManager.registerSynchronization or
TransactionSynchronization.afterCompletion / afterCommit equivalent) so eviction
executes only when the transaction successfully commits, and ensure evictions
are skipped on rollback; update the places currently calling
productCacheStore.evictProduct (the two occurrences) to register an afterCommit
task that calls productCacheStore.evictProduct(productId), and add integration
tests covering “concurrent read during pending commit re-caching” and
“transaction rollback should not delete cache” scenarios to verify correct
behavior.
In
`@apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java`:
- Around line 32-33: Right now cache invalidation is only in ProductFacade
(productCacheStore) so direct saves via OrderFacade, OrderPlacementTxService,
BrandFacade (calls to productRepository.save(...)) can leave stale single-item
caches; emit a ProductChangedEvent from every place that mutates a product
(replace direct productCacheStore.invalidate calls), implement a single
`@TransactionalEventListener`(phase = AFTER_COMMIT) listener that clears
productCacheStore for the given product IDs, and add integration tests covering
order creation/cancellation and brand deletion/restoration asserting getProduct
returns the updated value immediately after these flows.
- Around line 63-64: ProductFacade currently calls
productCacheStore.evictProduct immediately in multiple write methods, which can
cause stale-cache races before DB commit; replace those immediate evict calls
with a shared helper (e.g., evictAfterCommit or registerEvictAfterCommit) that
defers productCacheStore.evictProduct(productId) to run only after the
transaction successfully commits (use transaction synchronization/afterCommit
hook used in your stack) and call this helper from all affected write methods
(update, delete, restore, increase, decrease) instead of direct evictProduct
calls; also add tests for update/delete/restore/increase/decrease that simulate
concurrent reads during the pre-commit window to assert no long-lived stale
cache remains.
In `@apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java`:
- Around line 15-22: The `@Table` index annotations on Product (indexes
idx_products_brand_id_like_count, idx_products_brand_id_created_at,
idx_products_brand_id_price) are not applied when ddl-auto is disabled, so add
an explicit migration to create these indexes (e.g., Flyway/Liquibase SQL
migration with CREATE INDEX statements for the three names) and include a
deployment pipeline verification step that queries information_schema.statistics
to assert the three index names exist for the products table; update migration
files and CI/CD checks accordingly.
In
`@apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductCacheStore.java`:
- Around line 51-53: evictProduct currently calls
redisTemplate.delete(productKey(productId)) without handling exceptions, so
Redis failures will bubble up and rollback surrounding `@Transactional` writes;
change evictProduct to mirror
getProduct/putProduct/getProductList/putProductList by wrapping the delete in a
try-catch that catches RuntimeException (or Redis-specific exceptions), logs the
error with context (productId and key) and suppresses it (do not rethrow),
ensuring cache failures don’t fail the DB transaction; also add tests in
ProductFacade.update, increaseStock, and decreaseStock to simulate Redis failure
and assert the transaction still commits.
In
`@apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeTest.java`:
- Around line 42-47: The test must assert cache interactions to prevent
regressions: change the inline mock to a field-level mock ProductCacheStore and
inject it into the ProductFacade constructor, then add verifications for cache
hit/miss behavior for ProductFacade.getProduct, getProducts, and
getProductsByBrand (verify the cache read methods were called and that writes
occur on misses); also verify evictProduct is called for write paths that should
invalidate cache and verify evictProduct is NOT called for idempotent/no-op
update paths. Reference the ProductFacade constructor injection, the
ProductCacheStore mock, the getProduct/getProducts/getProductsByBrand methods,
and the evictProduct method when adding these verify assertions.
---
Outside diff comments:
In
`@apps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.java`:
- Around line 43-50: The tests for LikeFacade lack verification that
ProductCacheStore eviction occurs; replace the inline
mock(ProductCacheStore.class) with a member mock field (e.g., productCacheStore)
and inject it into the LikeFacade constructor used in LikeFacadeTest; then
add/modify tests to assert that on successful like/unlike flows the mock's
evictProduct(productId) is invoked (verify(evictProduct) called with the
expected productId after calling LikeFacade.like/unlike), and add tests for
no-op paths (duplicate like or removing non-existent like) to verify
evictProduct is NOT called (verifyNoInteractions or verify(..., never())).
Ensure tests exercise the actual service paths (using LikeService and repository
mocks) so the eviction verification is meaningful.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 673a66e2-b397-44c3-99b4-a1a743070cfe
⛔ Files ignored due to path filters (2)
CLAUDE.mdis excluded by!**/*.mdand included by**docs/performance/load-test-report.mdis excluded by!**/*.mdand included by**
📒 Files selected for processing (7)
apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.javaapps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.javaapps/commerce-api/src/main/java/com/loopers/domain/product/Product.javaapps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductCacheStore.javaapps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeIntegrationTest.javaapps/commerce-api/src/test/java/com/loopers/application/like/LikeFacadeTest.javaapps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeTest.java
| product.increaseLikeCount(); | ||
| Like saved = likeRepository.save(result.like()); | ||
| productRepository.save(product); | ||
| productCacheStore.evictProduct(productId); |
There was a problem hiding this comment.
트랜잭션 커밋 전에 캐시를 삭제하면 오래된 값이 장시간 재캐시될 수 있다
Line 85와 Line 115에서 캐시를 즉시 삭제하면, 커밋 전 동시 조회가 DB의 이전 값을 읽어 1시간 TTL로 다시 캐시할 수 있어 운영 중 데이터 신뢰도가 떨어진다. 수정안으로 캐시 무효화는 afterCommit 훅에서 실행해야 한다. 추가 테스트로 “쓰기 트랜잭션 커밋 지연 중 동시 조회” 시나리오와 “롤백 시 캐시 미삭제” 시나리오를 통합 테스트에 넣어야 한다.
수정 예시 (after-commit eviction)
+import org.springframework.transaction.support.TransactionSynchronization;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
@@
- productCacheStore.evictProduct(productId);
+ evictProductAfterCommit(productId);
@@
- productCacheStore.evictProduct(productId);
+ evictProductAfterCommit(productId);
}
+
+ private void evictProductAfterCommit(Long productId) {
+ if (TransactionSynchronizationManager.isSynchronizationActive()) {
+ TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+ `@Override`
+ public void afterCommit() {
+ productCacheStore.evictProduct(productId);
+ }
+ });
+ return;
+ }
+ productCacheStore.evictProduct(productId);
+ }Also applies to: 115-115
🤖 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/like/LikeFacade.java`
at line 85, productCacheStore.evictProduct is being called immediately (e.g., in
the methods where productCacheStore.evictProduct is invoked) which can cause
stale DB reads to be re-cached before the write transaction commits; change
those immediate evictions to run in an after-commit callback (use
TransactionSynchronizationManager.registerSynchronization or
TransactionSynchronization.afterCompletion / afterCommit equivalent) so eviction
executes only when the transaction successfully commits, and ensure evictions
are skipped on rollback; update the places currently calling
productCacheStore.evictProduct (the two occurrences) to register an afterCommit
task that calls productCacheStore.evictProduct(productId), and add integration
tests covering “concurrent read during pending commit re-caching” and
“transaction rollback should not delete cache” scenarios to verify correct
behavior.
| private final ProductCacheStore productCacheStore; | ||
|
|
There was a problem hiding this comment.
캐시 무효화가 ProductFacade 경로에만 묶여 있어 다른 쓰기 경로에서 정합성이 깨진다
운영 관점에서 현재 무효화는 이 파사드 경로에만 있어, OrderFacade(apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java Line 84-92), OrderPlacementTxService(Line 92-98), BrandFacade(Line 76-80, 94-100)처럼 productRepository.save(...)를 직접 호출하는 경로에서는 단건 캐시가 오래 남을 수 있다. 수정안으로 제품 변경 후행 이벤트(예: ProductChangedEvent)를 발행하고 단일 리스너에서 after-commit 무효화를 통합해야 한다. 추가 테스트로 주문 생성/취소, 브랜드 삭제/복구 후 getProduct가 즉시 최신 값을 반환하는 통합 테스트를 보강해야 한다.
🤖 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 32 - 33, Right now cache invalidation is only in ProductFacade
(productCacheStore) so direct saves via OrderFacade, OrderPlacementTxService,
BrandFacade (calls to productRepository.save(...)) can leave stale single-item
caches; emit a ProductChangedEvent from every place that mutates a product
(replace direct productCacheStore.invalidate calls), implement a single
`@TransactionalEventListener`(phase = AFTER_COMMIT) listener that clears
productCacheStore for the given product IDs, and add integration tests covering
order creation/cancellation and brand deletion/restoration asserting getProduct
returns the updated value immediately after these flows.
| productCacheStore.evictProduct(productId); | ||
| return ProductDto.ProductInfo.of(saved, brand.getName()); |
There was a problem hiding this comment.
쓰기 메서드 전반에서 커밋 전 캐시 무효화가 동일하게 반복된다
Line 63, 132, 148, 158, 174의 즉시 삭제는 커밋 전 재캐시 경쟁 상태를 만들어 오래된 단건 캐시가 1시간 유지될 수 있다. 수정안으로 공통 헬퍼를 두고 afterCommit에서만 evictProduct를 실행해야 한다. 추가 테스트로 update/delete/restore/increase/decrease 각각에 대해 “커밋 전 동시 조회 시 오래된 캐시 고착이 없는지”를 검증해야 한다.
Also applies to: 132-133, 148-149, 158-159, 174-175
🤖 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 63 - 64, ProductFacade currently calls
productCacheStore.evictProduct immediately in multiple write methods, which can
cause stale-cache races before DB commit; replace those immediate evict calls
with a shared helper (e.g., evictAfterCommit or registerEvictAfterCommit) that
defers productCacheStore.evictProduct(productId) to run only after the
transaction successfully commits (use transaction synchronization/afterCommit
hook used in your stack) and call this helper from all affected write methods
(update, delete, restore, increase, decrease) instead of direct evictProduct
calls; also add tests for update/delete/restore/increase/decrease that simulate
concurrent reads during the pre-commit window to assert no long-lived stale
cache remains.
| @Table( | ||
| name = "products", | ||
| indexes = { | ||
| @Index(name = "idx_products_brand_id_like_count", columnList = "brand_id, like_count DESC"), | ||
| @Index(name = "idx_products_brand_id_created_at", columnList = "brand_id, created_at DESC"), | ||
| @Index(name = "idx_products_brand_id_price", columnList = "brand_id, price ASC") | ||
| } | ||
| ) |
There was a problem hiding this comment.
인덱스 애노테이션만으로는 운영 스키마에 반영되지 않는다
운영 관점에서 ddl-auto: none 환경이면 Line 15-22의 인덱스 정의가 실제 DB에 생성되지 않아 성능 개선이 적용되지 않는다. 수정안으로 명시적 마이그레이션 SQL(Flyway/Liquibase/수동 DDL)을 추가해야 한다. 추가 테스트로 배포 파이프라인에서 information_schema.statistics 검증을 넣어 세 인덱스 존재를 자동 확인해야 한다.
운영 반영용 SQL 예시
CREATE INDEX idx_products_brand_id_like_count
ON products (brand_id, like_count DESC);
CREATE INDEX idx_products_brand_id_created_at
ON products (brand_id, created_at DESC);
CREATE INDEX idx_products_brand_id_price
ON products (brand_id, price ASC);🤖 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/domain/product/Product.java`
around lines 15 - 22, The `@Table` index annotations on Product (indexes
idx_products_brand_id_like_count, idx_products_brand_id_created_at,
idx_products_brand_id_price) are not applied when ddl-auto is disabled, so add
an explicit migration to create these indexes (e.g., Flyway/Liquibase SQL
migration with CREATE INDEX statements for the three names) and include a
deployment pipeline verification step that queries information_schema.statistics
to assert the three index names exist for the products table; update migration
files and CI/CD checks accordingly.
| public void evictProduct(Long productId) { | ||
| redisTemplate.delete(productKey(productId)); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) evictProduct가 예외를 처리하지 않는지 확인
rg -n "public void evictProduct|redisTemplate.delete" apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductCacheStore.java -C2
# 2) evictProduct가 트랜잭션 메서드에서 직접 호출되는지 확인
rg -n "(`@Transactional`|evictProduct\\()" apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java -C2
rg -n "(`@Transactional`|evictProduct\\()" apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java -C2Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 4218
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) View full ProductCacheStore.java to see all methods
cat apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductCacheStore.java
# 2) Check if get/put methods have try/catch
grep -n "try\|catch\|public.*get\|public.*put" apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductCacheStore.java || true
# 3) Search for other evict methods in the codebase
find apps/commerce-api -name "*.java" -type f | xargs grep -l "public.*evict" 2>/dev/null || trueRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 4468
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Search for all evict methods in the codebase
find apps/commerce-api -name "*.java" -type f -exec grep -l "evict" {} \; 2>/dev/null
# 2) Check ProductService to see if there are other eviction patterns
rg -n "evict|clear\(\)" apps/commerce-api/src/main/java/com/loopers/application/product/ProductService.java -C2 || true
# 3) Check if there are transactional boundaries after evictProduct call
rg -B5 "evictProduct\(" apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java | head -30Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1961
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Verify the exact line where evictProduct is called in a `@Transactional` context
# to confirm transaction boundary
rg -B10 "evictProduct\(productId\)" apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java | head -40Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1986
Redis 삭제 예외 전파로 쓰기 트랜잭션이 실패할 수 있다
Line 51-53의 evictProduct() 메서드는 예외 처리가 없어 Redis 장애 시 예외가 그대로 전파된다. 호출 지점이 모두 @Transactional 메서드 내에 있고 productRepository.save() 이후에 실행되므로, Redis 연결 실패 시 캐시 삭제 예외가 전체 트랜잭션을 롤백시킨다. 캐시는 부가 기능이므로 인프라 장애가 핵심 쓰기 작업을 실패하게 하면 안 된다.
같은 클래스의 getProduct(), putProduct(), getProductList(), putProductList() 메서드는 모두 예외를 처리하므로 evictProduct()도 동일하게 수정해야 한다.
수정 예시
public void evictProduct(Long productId) {
- redisTemplate.delete(productKey(productId));
+ try {
+ redisTemplate.delete(productKey(productId));
+ } catch (Exception e) {
+ log.warn("캐시 삭제 실패 - product:{}", productId, e);
+ }
}추가로 ProductFacade.update(), increaseStock(), decreaseStock() 등 evictProduct()를 호출하는 @Transactional 메서드에서 Redis 장애 시에도 트랜잭션이 성공하는지 테스트해야 한다.
🤖 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/infrastructure/product/ProductCacheStore.java`
around lines 51 - 53, evictProduct currently calls
redisTemplate.delete(productKey(productId)) without handling exceptions, so
Redis failures will bubble up and rollback surrounding `@Transactional` writes;
change evictProduct to mirror
getProduct/putProduct/getProductList/putProductList by wrapping the delete in a
try-catch that catches RuntimeException (or Redis-specific exceptions), logs the
error with context (productId and key) and suppresses it (do not rethrow),
ensuring cache failures don’t fail the DB transaction; also add tests in
ProductFacade.update, increaseStock, and decreaseStock to simulate Redis failure
and assert the transaction still commits.
| productFacade = new ProductFacade( | ||
| productRepository, | ||
| brandRepository, | ||
| new ProductService() | ||
| new ProductService(), | ||
| mock(ProductCacheStore.class) | ||
| ); |
There was a problem hiding this comment.
캐시 연동 회귀를 막는 검증이 누락되어 있다
운영 관점에서 캐시 로직이 깨져도 현재 테스트는 통과할 수 있어 장애를 사전에 차단하기 어렵다. 수정안으로 ProductCacheStore를 필드 목으로 보관하고 getProduct/getProducts/getProductsByBrand의 hit/miss 및 write 메서드의 evictProduct 호출 여부를 verify로 검증해야 한다. 추가 테스트로 idempotent/no-op 경로에서 evictProduct가 호출되지 않는 케이스까지 포함해야 한다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeTest.java`
around lines 42 - 47, The test must assert cache interactions to prevent
regressions: change the inline mock to a field-level mock ProductCacheStore and
inject it into the ProductFacade constructor, then add verifications for cache
hit/miss behavior for ProductFacade.getProduct, getProducts, and
getProductsByBrand (verify the cache read methods were called and that writes
occur on misses); also verify evictProduct is called for write paths that should
invalidate cache and verify evictProduct is NOT called for idempotent/no-op
update paths. Reference the ProductFacade constructor injection, the
ProductCacheStore mock, the getProduct/getProducts/getProductsByBrand methods,
and the evictProduct method when adding these verify assertions.
📌 Summary
🧭 Context & Decision
문제 정의
선택지와 결정
인덱스 설계
brand_id단독 인덱스 → 필터는 되지만 like_count 정렬 filesort 여전히 발생(brand_id, like_count DESC)복합 인덱스 → 필터 + 정렬 모두 인덱스로 처리(brand_id, created_at DESC),(brand_id, price ASC)인덱스 추가캐시 전략
🏗️ Design Overview
변경 범위
commerce-api- product, like 도메인ProductCacheStore(infrastructure 레이어)주요 컴포넌트 책임
Product:@Table에 복합 인덱스 3개 정의ProductCacheStore: Redis 기반 단건/목록 캐시 read/write/evict, Miss/장애 시Optional.empty()반환으로 DB fallback 트리거ProductFacade: 조회 시 Cache-Aside 패턴 적용, 변경/삭제 시 단건 캐시 evictLikeFacade: 좋아요 증가/감소 시 단건 캐시 evict🔁 Flow Diagram
Main Flow (캐시 Hit)
Fallback Flow (캐시 Miss / Redis 장애)
Evict Flow (좋아요 변경)
변경 목적
10만 건 상품 환경에서 브랜드별/전체 목록 조회 시 Full Scan 및 filesort 발생으로 인한 성능 저하(평균 응답시간 1,348ms) 해결을 위해 DB 인덱스 추가 및 Redis 캐시 도입.
핵심 변경점
리스크/주의사항
테스트/검증 방법