-
Notifications
You must be signed in to change notification settings - Fork 35
[volume-9] Product Ranking with Redis #220
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
Merged
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
2f797fb
feat: ๋ญํน ์ค์ ์ถ๊ฐ
sylee6529 d313560
feat: ๋ญํน ZSET ์บ์ ๊ตฌํ (commerce-streamer)
sylee6529 6ab67c3
feat: ๋ฉํธ๋ฆญ ์ง๊ณ ์๋น์ค ๋ญํน ์ฐ๋
sylee6529 d4e6aef
feat: ๋ญํน ์บ์ ์กฐํ ๊ธฐ๋ฅ ๊ตฌํ (commerce-api)
sylee6529 bac5304
feat: ๋ญํน Facade ๋ฐ Info ๊ตฌํ
sylee6529 b3b3270
feat: ๋ญํน ์กฐํ API ์ปจํธ๋กค๋ฌ ๊ตฌํ
sylee6529 8626727
feat: ์ํ ์์ธ์ ๋ญํน ์ ๋ณด ์ถ๊ฐ
sylee6529 bd05e11
refactor: ์ํ ์์ธ ์กฐํ ์ต์ ํ
sylee6529 376903f
test: ๋ญํน ์์คํ
ํ
์คํธ ์ถ๊ฐ
sylee6529 57fe5a7
fix: TTL CAS ๋ก์ง TOCTOU ์ด์ ์์
sylee6529 d5117fb
feat: ๋ญํน API date ํ๋ผ๋ฏธํฐ ์ ํจ์ฑ ๊ฒ์ฆ ์ถ๊ฐ
sylee6529 c9c03b2
feat: ๋ญํน API page/size ํ๋ผ๋ฏธํฐ ์ ํจ์ฑ ๊ฒ์ฆ ์ถ๊ฐ
sylee6529 31047f5
feat: IllegalArgumentException, ConstraintViolationException ํธ๋ค๋ฌ ์ถ๊ฐ
sylee6529 836b809
fix: ๊ธฐ์กด ํ
์คํธ ์คํจ ์์
sylee6529 9c3b0e0
test: ์ํ ์์ธ ์กฐํ ์ ๋ญํน ์ ๋ณด ํ
์คํธ ์ถ๊ฐ
sylee6529 ee508d0
refactor: Info ํด๋์ค @Getter ์ ๋ํ
์ด์
ํต์ผ
sylee6529 e89e5c4
refactor: ๋ญํน ์ฝ๋ ๋ถํ์ํ ์ฃผ์ ์ ๋ฆฌ
sylee6529 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
24 changes: 5 additions & 19 deletions
24
apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,39 +1,25 @@ | ||
| package com.loopers.application.product; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
| import com.fasterxml.jackson.databind.annotation.JsonDeserialize; | ||
| import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; | ||
| import com.loopers.domain.common.vo.Money; | ||
| import com.loopers.domain.product.vo.Stock; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| @Builder | ||
| @JsonDeserialize(builder = ProductDetailInfo.ProductDetailInfoBuilder.class) | ||
| public class ProductDetailInfo { | ||
|
|
||
| private final Long id; | ||
| private final String name; | ||
| private final String description; | ||
| private final Long brandId; | ||
| private final String brandName; | ||
| private final String brandDescription; | ||
| private final Money price; | ||
| private final Stock stock; | ||
| private final int likeCount; | ||
| private final boolean isLikedByMember; | ||
|
|
||
| public Long getId() { return id; } | ||
| public String getName() { return name; } | ||
| public String getDescription() { return description; } | ||
| public String getBrandName() { return brandName; } | ||
| public String getBrandDescription() { return brandDescription; } | ||
| public Money getPrice() { return price; } | ||
| public Stock getStock() { return stock; } | ||
| public int getLikeCount() { return likeCount; } | ||
|
|
||
| @JsonProperty("likedByMember") | ||
| public boolean isLikedByMember() { return isLikedByMember; } | ||
|
|
||
| @JsonPOJOBuilder(withPrefix = "") | ||
| public static class ProductDetailInfoBuilder { | ||
| } | ||
| private final boolean isLikedByMember; | ||
| private final Integer ranking; // ์์ (1-based), ์์๊ถ ๋ฐ์ด๋ฉด null | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
19 changes: 3 additions & 16 deletions
19
apps/commerce-api/src/main/java/com/loopers/application/product/ProductSummaryInfo.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,32 +1,19 @@ | ||
| package com.loopers.application.product; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
| import com.fasterxml.jackson.databind.annotation.JsonDeserialize; | ||
| import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; | ||
| import com.loopers.domain.common.vo.Money; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| @Builder | ||
| @JsonDeserialize(builder = ProductSummaryInfo.ProductSummaryInfoBuilder.class) | ||
| public class ProductSummaryInfo { | ||
|
|
||
| private final Long id; | ||
| private final String name; | ||
| private final String brandName; | ||
| private final Money price; | ||
| private final int likeCount; | ||
| private final boolean isLikedByMember; | ||
|
|
||
| public Long getId() { return id; } | ||
| public String getName() { return name; } | ||
| public String getBrandName() { return brandName; } | ||
| public Money getPrice() { return price; } | ||
| public int getLikeCount() { return likeCount; } | ||
|
|
||
| @JsonProperty("likedByMember") | ||
| public boolean isLikedByMember() { return isLikedByMember; } | ||
|
|
||
| @JsonPOJOBuilder(withPrefix = "") | ||
| public static class ProductSummaryInfoBuilder { | ||
| } | ||
| private final boolean isLikedByMember; | ||
| } |
111 changes: 111 additions & 0 deletions
111
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| package com.loopers.application.ranking; | ||
|
|
||
| import com.loopers.application.ranking.RankingInfo.RankingItemInfo; | ||
| import com.loopers.application.ranking.RankingInfo.RankingPageInfo; | ||
| import com.loopers.domain.brand.Brand; | ||
| import com.loopers.domain.brand.repository.BrandRepository; | ||
| import com.loopers.domain.product.Product; | ||
| import com.loopers.domain.product.repository.ProductRepository; | ||
| import com.loopers.infrastructure.cache.ProductRankingCache; | ||
| import com.loopers.infrastructure.cache.ProductRankingCache.RankingEntry; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.format.DateTimeFormatter; | ||
| import java.time.format.DateTimeParseException; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.function.Function; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| @Slf4j | ||
| @RequiredArgsConstructor | ||
| @Component | ||
| @Transactional(readOnly = true) | ||
| public class RankingFacade { | ||
|
|
||
| private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); | ||
|
|
||
| private final ProductRankingCache productRankingCache; | ||
| private final ProductRepository productRepository; | ||
| private final BrandRepository brandRepository; | ||
|
|
||
| /** @param page 0-based */ | ||
| public RankingPageInfo getRankings(String date, int page, int size) { | ||
| // ๋ ์ง ๊ฒ์ฆ ๋ฐ ๊ธฐ๋ณธ๊ฐ ์ฒ๋ฆฌ | ||
| String targetDate = validateAndNormalizeDate(date); | ||
|
|
||
| // 1. ZSET์์ ๋ญํน ์กฐํ | ||
| List<RankingEntry> rankingEntries = productRankingCache.getTopRankings(targetDate, page, size); | ||
|
|
||
| if (rankingEntries.isEmpty()) { | ||
| return RankingPageInfo.of(Collections.emptyList(), targetDate, page, size, 0); | ||
| } | ||
|
|
||
| // 2. ์ํ ID ๋ชฉ๋ก ์ถ์ถ | ||
| List<Long> productIds = rankingEntries.stream() | ||
| .map(RankingEntry::productId) | ||
| .toList(); | ||
|
|
||
| // 3. ์ํ ์ ๋ณด ์กฐํ | ||
| List<Product> products = productRepository.findByIdIn(productIds); | ||
| Map<Long, Product> productMap = products.stream() | ||
| .collect(Collectors.toMap(Product::getId, Function.identity())); | ||
|
|
||
| // 4. ๋ธ๋๋ ์ ๋ณด ์กฐํ (N+1 ๋ฐฉ์ง) | ||
| List<Long> brandIds = products.stream() | ||
| .map(Product::getBrandId) | ||
| .distinct() | ||
| .toList(); | ||
| List<Brand> brands = brandRepository.findByIdIn(brandIds); | ||
| Map<Long, Brand> brandMap = brands.stream() | ||
| .collect(Collectors.toMap(Brand::getId, Function.identity())); | ||
|
|
||
| // 5. ์๋ต ์์ฑ | ||
| List<RankingItemInfo> rankings = rankingEntries.stream() | ||
| .map(entry -> { | ||
| Product product = productMap.get(entry.productId()); | ||
| if (product == null) { | ||
| log.warn("[Ranking] Product not found - productId: {}", entry.productId()); | ||
| return null; | ||
| } | ||
| Brand brand = brandMap.get(product.getBrandId()); | ||
| String brandName = brand != null ? brand.getName() : "Unknown"; | ||
|
|
||
| return new RankingItemInfo( | ||
| entry.rank(), | ||
| product.getId(), | ||
| product.getName(), | ||
| brandName, | ||
| product.getPrice(), | ||
| product.getLikeCount(), | ||
| entry.score() | ||
| ); | ||
| }) | ||
| .filter(item -> item != null) | ||
| .toList(); | ||
|
|
||
| // 6. ์ ์ฒด ๊ฐ์ ์กฐํ | ||
| long totalCount = productRankingCache.getTotalCount(targetDate); | ||
|
|
||
| return RankingPageInfo.of(rankings, targetDate, page, size, totalCount); | ||
| } | ||
|
|
||
| /** @throws IllegalArgumentException ์ ํจํ์ง ์์ ๋ ์ง ํ์ */ | ||
| private String validateAndNormalizeDate(String date) { | ||
| if (date == null || date.isBlank()) { | ||
| return productRankingCache.getTodayDate(); | ||
| } | ||
|
|
||
| try { | ||
| LocalDate.parse(date, DATE_FORMATTER); | ||
| return date; | ||
| } catch (DateTimeParseException e) { | ||
| throw new IllegalArgumentException("Invalid date format. Expected yyyyMMdd, got: " + date); | ||
| } | ||
| } | ||
| } |
40 changes: 40 additions & 0 deletions
40
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingInfo.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package com.loopers.application.ranking; | ||
|
|
||
| import com.loopers.domain.common.vo.Money; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public class RankingInfo { | ||
|
|
||
| public record RankingPageInfo( | ||
| List<RankingItemInfo> rankings, | ||
| String date, | ||
| int page, | ||
| int size, | ||
| long totalCount, | ||
| int totalPages, | ||
| boolean hasNext | ||
| ) { | ||
| public static RankingPageInfo of( | ||
| List<RankingItemInfo> rankings, | ||
| String date, | ||
| int page, | ||
| int size, | ||
| long totalCount | ||
| ) { | ||
| int totalPages = size > 0 ? (int) Math.ceil((double) totalCount / size) : 0; | ||
| boolean hasNext = page < totalPages - 1; | ||
| return new RankingPageInfo(rankings, date, page, size, totalCount, totalPages, hasNext); | ||
| } | ||
| } | ||
|
|
||
| public record RankingItemInfo( | ||
| int rank, | ||
| Long productId, | ||
| String productName, | ||
| String brandName, | ||
| Money price, | ||
| int likeCount, | ||
| Double score | ||
| ) {} | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.