-
Notifications
You must be signed in to change notification settings - Fork 44
[Volume 5] 상품 좋아요 MV 도입 및 상품 조회 Redis 캐싱 적용 #209
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: kimjunyoung90
Are you sure you want to change the base?
Changes from all commits
49d22a3
4e12f9a
525b4eb
7ea1c2b
1dbd658
142c684
037b833
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 |
|---|---|---|
|
|
@@ -3,24 +3,27 @@ | |
| import com.loopers.application.product.command.ProductCreateCommand; | ||
| import com.loopers.application.product.command.ProductUpdateCommand; | ||
| import com.loopers.application.product.result.ProductResult; | ||
| import com.loopers.application.product.result.ProductWithLikeCountResult; | ||
| import com.loopers.domain.product.Product; | ||
| import com.loopers.domain.product.ProductCacheRepository; | ||
| import com.loopers.domain.product.ProductRepository; | ||
| import com.loopers.domain.product.ProductWithLikeCount; | ||
| import com.loopers.support.error.CoreException; | ||
| import com.loopers.support.error.ErrorType; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.data.domain.Page; | ||
| import org.springframework.data.domain.Pageable; | ||
|
|
||
| import java.util.List; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @RequiredArgsConstructor | ||
| @Service | ||
| public class ProductService { | ||
|
|
||
| private final ProductRepository productRepository; | ||
| private final ProductCacheRepository productCacheRepository; | ||
|
|
||
| @Transactional | ||
| public ProductResult registerProduct(Long brandId, ProductCreateCommand command) { | ||
|
|
@@ -31,27 +34,66 @@ public ProductResult registerProduct(Long brandId, ProductCreateCommand command) | |
| .stock(command.stock()) | ||
| .build(); | ||
|
|
||
| return ProductResult.from(productRepository.save(product)); | ||
| Product saved = productRepository.save(product); | ||
| productCacheRepository.evictAllProductsCache(); | ||
| return ProductResult.from(saved); | ||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public ProductResult getProduct(Long productId) { | ||
| Product product = productRepository.findByIdAndDeletedAtIsNull(productId) | ||
| .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다.")); | ||
|
|
||
| Product product = productCacheRepository.getProduct(productId) | ||
| .orElseGet(() -> { | ||
| Product origin = productRepository.findByIdAndDeletedAtIsNull(productId) | ||
| .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다.")); | ||
| productCacheRepository.putProduct(productId, origin); | ||
| return origin; | ||
| }); | ||
| return ProductResult.from(product); | ||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public ProductWithLikeCountResult getProductWithLikeCount(Long productId) { | ||
| ProductWithLikeCount productWithLikeCount = productCacheRepository.getProductWithLikeCount(productId) | ||
| .orElseGet(() -> { | ||
| ProductWithLikeCount origin = productRepository.findWithLikeCountByIdAndDeletedAtIsNull(productId) | ||
| .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다.")); | ||
| productCacheRepository.putProductWithLikeCount(productId, origin); | ||
| return origin; | ||
| }); | ||
| return ProductWithLikeCountResult.from(productWithLikeCount); | ||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public Page<ProductResult> getProducts(Pageable pageable) { | ||
| return productRepository.findAllByDeletedAtIsNull(pageable) | ||
| .map(ProductResult::from); | ||
| Page<Product> pages = productCacheRepository.getProducts(pageable) | ||
| .orElseGet(() -> { | ||
| Page<Product> origin = productRepository.findAllByDeletedAtIsNull(pageable); | ||
| productCacheRepository.putProducts(pageable, origin); | ||
| return origin; | ||
| }); | ||
| return pages.map(ProductResult::from); | ||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public Page<ProductWithLikeCountResult> getProductsWithLikeCount(Pageable pageable) { | ||
| Page<ProductWithLikeCount> pages = productCacheRepository.getProductsWithLikeCount(pageable) | ||
| .orElseGet(() -> { | ||
| Page<ProductWithLikeCount> origin = productRepository.findAllWithLikeCountByDeletedAtIsNull(pageable); | ||
| productCacheRepository.putProductsWithLikeCount(pageable, origin); | ||
| return origin; | ||
| }); | ||
| return pages.map(ProductWithLikeCountResult::from); | ||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public Page<ProductResult> getProducts(Long brandId, Pageable pageable) { | ||
| return productRepository.findAllByBrandIdAndDeletedAtIsNull(brandId, pageable) | ||
| .map(ProductResult::from); | ||
| public Page<ProductWithLikeCountResult> getProductsWithLikeCount(Long brandId, Pageable pageable) { | ||
| Page<ProductWithLikeCount> pages = productCacheRepository.getProductsWithLikeCount(brandId, pageable) | ||
| .orElseGet(() -> { | ||
| Page<ProductWithLikeCount> origin = productRepository.findAllWithLikeCountByBrandIdAndDeletedAtIsNull(brandId, pageable); | ||
| productCacheRepository.putProductsWithLikeCount(brandId, pageable, origin); | ||
| return origin; | ||
| }); | ||
| return pages.map(ProductWithLikeCountResult::from); | ||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
|
|
@@ -67,6 +109,8 @@ public ProductResult modifyProduct(Long productId, Long brandId, ProductUpdateCo | |
| .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다.")); | ||
|
|
||
| product.changeInfo(brandId, command.name(), command.price(), command.stock()); | ||
| productCacheRepository.evictProduct(productId); | ||
| productCacheRepository.evictAllProductsCache(); | ||
|
|
||
|
Comment on lines
+112
to
114
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. 트랜잭션 내에서 캐시를 무효화하면, 롤백 시 캐시 불일치가 발생한다. 현재 더 심각한 경우, 다른 요청이 캐시 삭제 후 ~ 트랜잭션 롤백 전 사이에 DB를 조회하여 캐시에 저장하면, 롤백 후에도 변경 전 데이터가 캐시에 남아있게 된다. 수정안: ♻️ 트랜잭션 커밋 후 캐시 무효화 예시`@Transactional`
public ProductResult modifyProduct(Long productId, Long brandId, ProductUpdateCommand command) {
Product product = productRepository.findByIdAndDeletedAtIsNull(productId)
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다."));
product.changeInfo(brandId, command.name(), command.price(), command.stock());
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
`@Override`
public void afterCommit() {
productCacheRepository.evictProduct(productId);
productCacheRepository.evictAllProductsCache();
}
});
return ProductResult.from(product);
}Also applies to: 123-125, 133-135, 142-144 🤖 Prompt for AI Agents |
||
| return ProductResult.from(product); | ||
| } | ||
|
|
@@ -76,6 +120,8 @@ public ProductResult deductStock(Long productId, int quantity) { | |
| Product product = productRepository.findByIdWithLockAndDeletedAtIsNull(productId) | ||
| .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다.")); | ||
| product.deductStock(quantity); | ||
| productCacheRepository.evictProduct(productId); | ||
| productCacheRepository.evictAllProductsCache(); | ||
| return ProductResult.from(product); | ||
| } | ||
|
|
||
|
|
@@ -84,36 +130,26 @@ public void restoreStock(Long productId, int quantity) { | |
| Product product = productRepository.findByIdWithLockAndDeletedAtIsNull(productId) | ||
| .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다.")); | ||
| product.restoreStock(quantity); | ||
| } | ||
|
|
||
| @Transactional | ||
| public void incrementLikeCount(Long productId) { | ||
| int updatedCount = productRepository.incrementLikeCount(productId); | ||
| if (updatedCount == 0) { | ||
| throw new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다."); | ||
| } | ||
| } | ||
|
|
||
| @Transactional | ||
| public void decrementLikeCount(Long productId) { | ||
| int updatedCount = productRepository.decrementLikeCount(productId); | ||
| if (updatedCount == 0) { | ||
| throw new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다."); | ||
| } | ||
| productCacheRepository.evictProduct(productId); | ||
| productCacheRepository.evictAllProductsCache(); | ||
| } | ||
|
|
||
| @Transactional | ||
| public void deleteProduct(Long productId) { | ||
| Product product = productRepository.findById(productId) | ||
| .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "상품을 찾을 수 없습니다.")); | ||
|
|
||
| product.delete(); | ||
| productCacheRepository.evictProduct(productId); | ||
| productCacheRepository.evictAllProductsCache(); | ||
| } | ||
|
|
||
| @Transactional | ||
| public void deleteProducts(Long brandId) { | ||
| List<Product> products = productRepository.findAllByBrandId(brandId); | ||
| products.forEach(Product::delete); | ||
| products.forEach(product -> { | ||
| product.delete(); | ||
| productCacheRepository.evictProduct(product.getId()); | ||
| }); | ||
| productCacheRepository.evictAllProductsCache(); | ||
| } | ||
|
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package com.loopers.application.product.result; | ||
|
|
||
| import com.loopers.domain.product.ProductWithLikeCount; | ||
|
|
||
| import java.time.ZonedDateTime; | ||
|
|
||
| public record ProductWithLikeCountResult( | ||
| Long id, | ||
| Long brandId, | ||
| String name, | ||
| int price, | ||
| int stock, | ||
| int likeCount, | ||
| ZonedDateTime createdAt, | ||
| ZonedDateTime updatedAt | ||
| ) { | ||
| public static ProductWithLikeCountResult from(ProductWithLikeCount product) { | ||
| return new ProductWithLikeCountResult( | ||
| product.id(), | ||
| product.brandId(), | ||
| product.name(), | ||
| product.price(), | ||
| product.stock(), | ||
| product.likeCount(), | ||
| product.createdAt(), | ||
| product.updatedAt() | ||
| ); | ||
| } | ||
| } |
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.
ProductService 의존성 제거로 결합도가 낮아졌다.
다만, 기존에 ProductService를 통해 처리하던 좋아요 수 갱신 로직이 제거되었으나, 대체 로직이 구현되지 않았다.
ProductLikeCount엔티티의 증감 로직이 없으므로 좋아요 수는 갱신되지 않는다.LikeFacade또는LikeService에서 카운트 갱신 책임을 명확히 해야 한다.🤖 Prompt for AI Agents