-
Notifications
You must be signed in to change notification settings - Fork 35
[volume-9] Product Ranking with Redis #221
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
34 commits
Select commit
Hold shift + click to select a range
514452c
feat: carry over λ³μ μΆκ°
rnqhstmd 2b4e367
feat: μ€μΌμ₯΄λ¬ νμ± μ€μ μΆκ°
rnqhstmd 544c506
feat: ProductDetailInfoμ rank νλ μΆκ° λ° κ΄λ ¨ λ©μλ μ€λ²λ‘λ
rnqhstmd 2cc76d3
feat: ProductDetailInfoμ λνΉ μ 보 μΆκ°
rnqhstmd 86642b4
feat: ProductMetricsFacadeμ λνΉ μ μ μ
λ°μ΄νΈ λ‘μ§ μΆκ°
rnqhstmd 3728d07
feat: RankingFacade ν΄λμ€ μΆκ° λ° μ΄λ²€νΈ μ²λ¦¬ λ©μλ ꡬν
rnqhstmd 3b834f1
feat: RankingKey ν΄λμ€ μΆκ° λ° λνΉ ν€ μμ± λ©μλ ꡬν
rnqhstmd 47901f0
feat: RankingScheduler ν΄λμ€ μΆκ° λ° λ§€μΌ λνΉ μ€λΉ λ‘μ§ κ΅¬ν
rnqhstmd 3a72f74
feat: RankingService ν΄λμ€ μΆκ° λ° λνΉ μ μ κ΄λ¦¬ λ‘μ§ κ΅¬ν
rnqhstmd a1c1536
feat: RankingWeight ν΄λμ€ μΆκ° λ° λνΉ κ°μ€μΉ κ΄λ¦¬ λ‘μ§ κ΅¬ν
rnqhstmd a7c749a
feat: ClockConfig ν΄λμ€ μΆκ° λ° μμ€ν
κΈ°λ³Έ μκ° μ€μ λ‘μ§ κ΅¬ν
rnqhstmd 0ce4a25
feat: μν μ‘°ν API ꡬν λ° DTO ν΄λμ€ μΆκ°
rnqhstmd 4291ab4
feat: λνΉ κ°μ€μΉ μ€μ κ΄λ¦¬ API ꡬν
rnqhstmd 1855152
feat: RankingCommand ν΄λμ€ μΆκ° λ° μ ν¨μ± κ²μ¦ λ‘μ§ κ΅¬ν
rnqhstmd ae41a7a
feat: μν λνΉ API ꡬν λ° DTO ν΄λμ€ μΆκ°
rnqhstmd 6c51a58
feat: RankingEntry ν΄λμ€ μΆκ°
rnqhstmd d9f23f0
feat: RankingFacade ν΄λμ€ μΆκ° λ° λνΉ μ‘°ν κΈ°λ₯ ꡬν
rnqhstmd 695fe6e
feat: RankingInfo ν΄λμ€ μΆκ° λ° λνΉ μ 보 ννμ μν λ μ½λ ꡬν
rnqhstmd c5ff2ee
feat: RankingKey ν΄λμ€ μΆκ° λ° Redis ZSET ν€ μμ± μ νΈλ¦¬ν° ꡬν
rnqhstmd ee72acc
feat: RankingPageInfo ν΄λμ€ μΆκ° λ° νμ΄μ§ μ 보 μ²λ¦¬ μ νΈλ¦¬ν° ꡬν
rnqhstmd 3a33ab2
feat: RankingService ν΄λμ€ μΆκ° λ° λνΉ μ‘°ν κΈ°λ₯ ꡬν
rnqhstmd 81c7deb
test: ProductMetricsConsumerTestμ μ€λ³΅ μ΄λ²€νΈ λ° λ©±λ±μ± μ²λ¦¬ ν
μ€νΈ μΆκ°
rnqhstmd 43d49a1
test: RankingIntegrationTest ν΄λμ€ μΆκ° λ° λνΉ κ΄λ ¨ κΈ°λ₯ ν΅ν© ν
μ€νΈ ꡬν
rnqhstmd 205e26f
test: RankingSchedulerTest ν΄λμ€ μΆκ° λ° λνΉ μ€μΌμ€λ¬ κΈ°λ₯ ν
μ€νΈ ꡬν
rnqhstmd ff536c6
test: RankingServiceTest ν΄λμ€ μΆκ° λ° λνΉ μλΉμ€ κΈ°λ₯ ν
μ€νΈ ꡬν
rnqhstmd f5a6e25
test: RankingServiceTest ν΄λμ€ μΆκ° λ° μ‘°ν, μ’μμ, μ£Όλ¬Έ μ μ μ¦κ° κΈ°λ₯ ν
μ€νΈ ꡬν
rnqhstmd b99c7b9
test: RankingV1ApiE2ETest ν΄λμ€ μΆκ° λ° λνΉ API κΈ°λ₯ ν
μ€νΈ ꡬν
rnqhstmd fd11c00
test: RankingWeightTest ν΄λμ€ μΆκ° λ° κ°μ€μΉ κ΄λ ¨ κΈ°λ₯ ν
μ€νΈ ꡬν
rnqhstmd 328b783
feat: validation import μΆκ°
rnqhstmd e3b1b4e
test: displayname λ³κ²½
rnqhstmd 2f2917a
feat: μ ν¨μ± κ²μ¦ μΆκ°
rnqhstmd 151393f
refactor: μ¬μ©νμ§ μλ λ©μλ μ κ±°
rnqhstmd ea4e6f0
test: assertTrue μμ
rnqhstmd 2699b7d
feat: κ°μ€μΉ μΌκ΄ μ
λ°μ΄νΈ μ μλ¬ νΈλ€λ§ μΆκ°
rnqhstmd 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
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
49 changes: 49 additions & 0 deletions
49
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingCommand.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,49 @@ | ||
| package com.loopers.application.ranking; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.format.DateTimeFormatter; | ||
| import java.time.format.DateTimeParseException; | ||
|
|
||
| public record RankingCommand( | ||
| LocalDate date, | ||
| int page, | ||
| int size | ||
| ) { | ||
| private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); | ||
| private static final int MAX_PAGE_SIZE = 100; | ||
| private static final int DEFAULT_PAGE_SIZE = 20; | ||
|
|
||
| public RankingCommand { | ||
| // μ ν¨μ± κ²μ¦ | ||
| if (page < 0) { | ||
| page = 0; | ||
| } | ||
| if (size <= 0 || size > MAX_PAGE_SIZE) { | ||
| size = DEFAULT_PAGE_SIZE; | ||
| } | ||
| if (date == null) { | ||
| date = LocalDate.now(); | ||
| } | ||
| } | ||
|
|
||
| public static RankingCommand of(String dateString, int page, int size) { | ||
| LocalDate date = parseDate(dateString); | ||
| return new RankingCommand(date, page, size); | ||
| } | ||
|
|
||
| public static RankingCommand today(int page, int size) { | ||
| return new RankingCommand(LocalDate.now(), page, size); | ||
| } | ||
|
|
||
| private static LocalDate parseDate(String dateString) { | ||
| if (dateString == null || dateString.isBlank()) { | ||
| return LocalDate.now(); | ||
| } | ||
| try { | ||
| return LocalDate.parse(dateString, DATE_FORMATTER); | ||
| } catch (DateTimeParseException e) { | ||
| return LocalDate.now(); | ||
| } | ||
| } | ||
| } | ||
|
|
134 changes: 134 additions & 0 deletions
134
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,134 @@ | ||
| package com.loopers.application.ranking; | ||
|
|
||
| import com.loopers.domain.product.Product; | ||
| import com.loopers.domain.product.ProductRepository; | ||
| import com.loopers.domain.ranking.RankingEntry; | ||
| import com.loopers.domain.ranking.RankingInfo; | ||
| import com.loopers.domain.ranking.RankingService; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.time.Clock; | ||
| import java.time.LocalDate; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| @Slf4j | ||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class RankingFacade { | ||
|
|
||
| private final RankingService rankingService; | ||
| private final ProductRepository productRepository; | ||
| private final Clock clock; | ||
|
|
||
| /** | ||
| * λνΉ νμ΄μ§ μ‘°ν | ||
| */ | ||
| @Transactional(readOnly = true) | ||
| public RankingPageInfo getRankingPage(RankingCommand command) { | ||
| List<RankingEntry> entries = rankingService.getRankingPage( | ||
| command.date(), | ||
| command.page(), | ||
| command.size() | ||
| ); | ||
|
|
||
| if (entries.isEmpty()) { | ||
| return RankingPageInfo.empty(command.date(), command.page(), command.size()); | ||
| } | ||
|
|
||
| // μν μ 보 μ‘°ν | ||
| List<Long> productIds = entries.stream() | ||
| .map(RankingEntry::productId) | ||
| .collect(Collectors.toList()); | ||
|
|
||
| Map<Long, Product> productMap = productRepository.findAllByIds(productIds).stream() | ||
| .collect(Collectors.toMap(Product::getId, p -> p)); | ||
|
|
||
| // λνΉ μ 보 μ‘°ν© | ||
| List<RankingInfo> rankings = new ArrayList<>(); | ||
| long startRank = (long) command.page() * command.size() + 1; | ||
|
|
||
| for (int i = 0; i < entries.size(); i++) { | ||
| RankingEntry entry = entries.get(i); | ||
| Product product = productMap.get(entry.productId()); | ||
|
|
||
| if (product != null) { | ||
| rankings.add(RankingInfo.of( | ||
| product.getId(), | ||
| product.getName(), | ||
| product.getPriceValue(), | ||
| product.getBrand().getName(), | ||
| startRank + i, | ||
| entry.score() | ||
| )); | ||
| } | ||
| } | ||
|
|
||
| Long totalCount = rankingService.getRankingSize(command.date()); | ||
|
|
||
| return RankingPageInfo.of( | ||
| rankings, | ||
| command.date(), | ||
| command.page(), | ||
| command.size(), | ||
| totalCount | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Top-N λνΉ μ‘°ν | ||
| */ | ||
| @Transactional(readOnly = true) | ||
| public List<RankingInfo> getTopN(LocalDate date, int n) { | ||
| List<RankingEntry> entries = rankingService.getTopNWithScores(date, n); | ||
|
|
||
| if (entries.isEmpty()) { | ||
| return List.of(); | ||
| } | ||
|
|
||
| List<Long> productIds = entries.stream() | ||
| .map(RankingEntry::productId) | ||
| .collect(Collectors.toList()); | ||
|
|
||
| Map<Long, Product> productMap = productRepository.findAllByIds(productIds).stream() | ||
| .collect(Collectors.toMap(Product::getId, p -> p)); | ||
|
|
||
| List<RankingInfo> rankings = new ArrayList<>(); | ||
| for (int i = 0; i < entries.size(); i++) { | ||
| RankingEntry entry = entries.get(i); | ||
| Product product = productMap.get(entry.productId()); | ||
|
|
||
| if (product != null) { | ||
| rankings.add(RankingInfo.of( | ||
| product.getId(), | ||
| product.getName(), | ||
| product.getPriceValue(), | ||
| product.getBrand().getName(), | ||
| (long) (i + 1), | ||
| entry.score() | ||
| )); | ||
| } | ||
| } | ||
|
|
||
| return rankings; | ||
| } | ||
|
|
||
| /** | ||
| * νΉμ μνμ μμ μ‘°ν | ||
| */ | ||
| public Long getProductRank(Long productId, LocalDate date) { | ||
| return rankingService.getRank(productId, date); | ||
| } | ||
|
|
||
| /** | ||
| * νΉμ μνμ μμ μ‘°ν (μ€λ κΈ°μ€) | ||
| */ | ||
| public Long getProductRankToday(Long productId) { | ||
| return rankingService.getRank(productId, LocalDate.now(clock)); | ||
| } | ||
| } |
30 changes: 30 additions & 0 deletions
30
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingPageInfo.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,30 @@ | ||
| package com.loopers.application.ranking; | ||
|
|
||
| import com.loopers.domain.ranking.RankingInfo; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.util.List; | ||
|
|
||
| public record RankingPageInfo( | ||
| List<RankingInfo> rankings, | ||
| LocalDate date, | ||
| int page, | ||
| int size, | ||
| Long totalCount, | ||
| int totalPages | ||
| ) { | ||
| public static RankingPageInfo of( | ||
| List<RankingInfo> rankings, | ||
| LocalDate date, | ||
| int page, | ||
| int size, | ||
| Long totalCount | ||
| ) { | ||
| int totalPages = (int) Math.ceil((double) totalCount / size); | ||
| return new RankingPageInfo(rankings, date, page, size, totalCount, totalPages); | ||
| } | ||
rnqhstmd marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| public static RankingPageInfo empty(LocalDate date, int page, int size) { | ||
| return new RankingPageInfo(List.of(), date, page, size, 0L, 0); | ||
| } | ||
| } | ||
7 changes: 7 additions & 0 deletions
7
apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingEntry.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,7 @@ | ||
| package com.loopers.domain.ranking; | ||
|
|
||
| public record RankingEntry( | ||
| Long productId, | ||
| Double score | ||
| ) { | ||
| } |
21 changes: 21 additions & 0 deletions
21
apps/commerce-api/src/main/java/com/loopers/domain/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,21 @@ | ||
| package com.loopers.domain.ranking; | ||
|
|
||
| public record RankingInfo( | ||
| Long productId, | ||
| String productName, | ||
| Long price, | ||
| String brandName, | ||
| Long rank, | ||
| Double score | ||
| ) { | ||
| public static RankingInfo of( | ||
| Long productId, | ||
| String productName, | ||
| Long price, | ||
| String brandName, | ||
| Long rank, | ||
| Double score | ||
| ) { | ||
| return new RankingInfo(productId, productName, price, brandName, rank, score); | ||
| } | ||
| } |
20 changes: 20 additions & 0 deletions
20
apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingKey.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,20 @@ | ||
| package com.loopers.domain.ranking; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.time.format.DateTimeFormatter; | ||
|
|
||
| /** | ||
| * Redis ZSET ν€ μμ± μ νΈλ¦¬ν° | ||
| */ | ||
| public class RankingKey { | ||
|
|
||
| private static final String KEY_PREFIX = "ranking:all:"; | ||
| private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); | ||
|
|
||
| private RankingKey() { | ||
| } | ||
|
|
||
| public static String daily(LocalDate date) { | ||
| return KEY_PREFIX + date.format(DATE_FORMATTER); | ||
| } | ||
| } |
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.