-
Notifications
You must be signed in to change notification settings - Fork 44
[Volume 5] 인덱스 캐시 추가 #218
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: nuobasic
Are you sure you want to change the base?
[Volume 5] 인덱스 캐시 추가 #218
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package com.loopers.application.product; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonTypeInfo; | ||
| import org.springframework.data.domain.Page; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) | ||
| public record ProductPageInfo( | ||
| List<ProductInfo> content, | ||
| int page, | ||
| int size, | ||
| long totalElements, | ||
| int totalPages | ||
| ) { | ||
| public static ProductPageInfo from(Page<ProductInfo> page) { | ||
| return new ProductPageInfo( | ||
| page.getContent(), | ||
| page.getNumber(), | ||
| page.getSize(), | ||
| page.getTotalElements(), | ||
| page.getTotalPages() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,8 @@ | |
| import com.loopers.support.error.CoreException; | ||
| import com.loopers.support.error.ErrorType; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.cache.annotation.CacheEvict; | ||
| import org.springframework.cache.annotation.Caching; | ||
| import org.springframework.dao.DataIntegrityViolationException; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
@@ -20,8 +22,12 @@ public class LikeService { | |
| private final ProductRepository productRepository; | ||
|
|
||
| @Transactional | ||
| @Caching(evict = { | ||
| @CacheEvict(cacheNames = "productDetail", key = "'product:detail:' + #productId"), | ||
| @CacheEvict(cacheNames = "productList", allEntries = true) | ||
|
Comment on lines
+26
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 좋아요 한 번마다 운영에서는 인기 상품의 like/unlike 한 번마다 모든 브랜드·페이지·정렬 조합의 list cache가 함께 비워져 read burst가 다시 DB로 쏠린다. 관리자 저빈도 수정과 달리 사용자 좋아요는 고빈도이므로, 수정안은 brand 범위나 versioned key 방식으로 eviction 범위를 좁히는 것이다. 추가 테스트로 brand A 상품의 like가 brand B의 list cache key를 보존하는지 검증해달라. Based on learnings, full cache invalidation was accepted only for low-frequency admin writes with a short TTL. Also applies to: 50-51 🤖 Prompt for AI Agents |
||
| }) | ||
|
Comment on lines
24
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result:
Recommended way to evict strictly after commitUse Spring’s transaction-aware cache support so evictions are deferred to the transaction after-commit callback:
Citations:
🏁 Script executed: cat apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 2994 🏁 Script executed: rg "TransactionAwareCacheManagerProxy|setTransactionAware|@TransactionalEventListener" --type javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 67 🏁 Script executed: fd "CacheConfig|CacheManager|cache" --extension java | xargs grep -l "setTransactionAware\|TransactionAware" 2>/dev/nullRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 69 🏁 Script executed: fd "application\.(yml|yaml|properties)" --type f | xargs cat 2>/dev/null | grep -A 10 -i cacheRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 67 🏁 Script executed: fd ".*Config\\.java" --type f | xargs grep -l "cache\|Cache" 2>/dev/null | head -5 | xargs catRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 4146 🏁 Script executed: rg "@Bean.*CacheManager|@Bean.*CacheConfig|setTransactionAware" --type java -A 3Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 67 트랜잭션 commit 이후 무효화를 보장하도록 after-commit 패턴으로 변경해야 한다.
수정안은 두 가지다:
추가로 commit 전 재캐시 방지 검증: 쓰기 트랜잭션 진행 중 동시 조회를 붙여 캐시가 먼저 비워진 후 old value로 재캐시되지 않음을 테스트해달라. Also applies to: 48-52 🤖 Prompt for AI Agents |
||
| public LikeModel like(Long userId, Long productId) { | ||
| ProductModel product = productRepository.findById(productId) | ||
| ProductModel product = productRepository.findByIdForUpdate(productId) | ||
| .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 상품입니다.")); | ||
|
Comment on lines
+30
to
31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
fd 'ProductJpaRepository.java' -x sed -n '1,220p' {}Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 2327 🏁 Script executed: fd 'ProductJpaRepository.java' -x wc -l {}Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 182 🏁 Script executed: fd 'ProductRepositoryImpl.java' -x head -100 {}Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 1643 🏁 Script executed: rg '@Lock' apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java -A 2Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 265 🏁 Script executed: fd 'ProductJpaRepository.java' -x cat {}Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 2327 🏁 Script executed: fd 'LikeService.java' -x sed -n '25,60p' {}Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 1686 🏁 Script executed: fd 'OrderJpaRepository.java' -x rg '@QueryHints|@Lock' -A 3 {}Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 67 🏁 Script executed: fd 'OrderJpaRepository.java' -x head -100 {}Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 1602 🏁 Script executed: rg '@QueryHints' -A 2 --type java | head -40Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 67 🏁 Script executed: rg 'jakarta\.persistence\.lock\.timeout' -A 1 -B 1 --type javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 67 🏁 Script executed: rg '@QueryHints' -B 2 -A 3 --type javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 67 🏁 Script executed: rg '@Lock' -B 1 -A 3 --type javaRepository: Loopers-dev-lab/loop-pack-be-l2-vol3-java Length of output: 1472
이 메서드는 🤖 Prompt for AI Agents |
||
|
|
||
| likeRepository.findByUserIdAndProductId(userId, productId) | ||
|
|
@@ -31,18 +37,28 @@ public LikeModel like(Long userId, Long productId) { | |
|
|
||
| LikeModel like = new LikeModel(userId, product); | ||
| try { | ||
| return likeRepository.save(like); | ||
| LikeModel savedLike = likeRepository.save(like); | ||
| product.increaseLikeCount(); | ||
| return savedLike; | ||
| } catch (DataIntegrityViolationException e) { | ||
| throw new CoreException(ErrorType.CONFLICT, "이미 좋아요한 상품입니다."); | ||
| } | ||
| } | ||
|
|
||
| @Transactional | ||
| @Caching(evict = { | ||
| @CacheEvict(cacheNames = "productDetail", key = "'product:detail:' + #productId"), | ||
| @CacheEvict(cacheNames = "productList", allEntries = true) | ||
| }) | ||
| public void unlike(Long userId, Long productId) { | ||
| ProductModel product = productRepository.findByIdForUpdate(productId) | ||
| .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "존재하지 않는 상품입니다.")); | ||
|
|
||
| LikeModel like = likeRepository.findByUserIdAndProductId(userId, productId) | ||
| .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "좋아요가 존재하지 않습니다.")); | ||
|
|
||
| likeRepository.delete(like); | ||
| product.decreaseLikeCount(); | ||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 261
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2686
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 170
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 1333
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 162
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 4769
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 504
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 2413
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 783
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 728
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 335
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 11211
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 449
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
🏁 Script executed:
Repository: Loopers-dev-lab/loop-pack-be-l2-vol3-java
Length of output: 67
캐시 역직렬화 공격 표면을 줄이기 위해
JsonTypeInfo.Id.CLASS제거가 필요하다.운영 관점에서 Redis 캐시 값 변조 시 의도하지 않은 타입 역직렬화 위험이 있다. LINE 8의
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)어노테이션은CacheConfig의activateDefaultTyping(getPolymorphicTypeValidator(), NON_FINAL, ...)과 함께 작동하면서 캐시 페이로드의@class필드를 통해 임의 클래스 인스턴스화를 허용한다. Redis 접근 제어가 완벽하지 않은 환경에서는 캐시 데이터 변조로 보안 침해가 발생할 수 있다.수정안:
@JsonTypeInfo어노테이션 제거CacheConfig에서 명시적 패키지 화이트리스트 기반PolymorphicTypeValidator또는 캐시별 직렬화기 도입@class값을 포함한 캐시 페이로드 역직렬화가 거부되는지 검증하는 보안 회귀 테스트 추가ProductCacheTest.java는 캐시 히트/미스와 무효화만 검증하므로, 변조된 캐시 데이터에 대한 방어 테스트가 없다.
🤖 Prompt for AI Agents