-
Notifications
You must be signed in to change notification settings - Fork 34
[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
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Walkthrough์ํ ์์ ์์คํ ์ ๊ตฌํํ์ต๋๋ค. commerce-api์์๋ ์ํ ์์ธ ์กฐํ ์ Redis์์ ๋น์ผ ์์๋ฅผ ์กฐํํ์ฌ ์๋ต์ ํฌํจํ๊ณ , commerce-streamer์์๋ ์กฐํ/์ข์์/์ฃผ๋ฌธ ์ด๋ฒคํธ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ ๋ ฌ ์งํฉ์ ์ ์๋ฅผ ๋์ ํฉ๋๋ค. ๋งค์ผ ๋ฐค 11:50์ ์ ์๋ฅผ ๋ค์ ๋ ๋ก ์ด์ํ๋ ์ค์ผ์ค๋ฌ๋ฅผ ํตํด ์ฝ๋ ์คํํธ๋ฅผ ์ง์ํฉ๋๋ค. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client
participant API as ProductV1Controller
participant PF as ProductFacade
participant PRep as ProductRepository
participant RF as RankingFacade
participant RS as RankingService
participant Redis
Client->>API: GET /api/v1/products/{id}
activate API
API->>PF: getProductDetail(productId)
activate PF
PF->>PRep: findById(productId)
activate PRep
PRep-->>PF: Product
deactivate PRep
PF->>PF: ์ํ ์ ๋ณด โ ProductDetailInfo
PF->>RF: getProductRankToday(productId)
activate RF
RF->>RS: getRank(productId, today)
activate RS
RS->>Redis: ZREVRANK key(today), productId
Redis-->>RS: rank (1-based)
RS-->>RF: rank
deactivate RS
RF-->>PF: rank
deactivate RF
PF->>PF: withRank(rank) ์ ์ฉ
PF-->>API: ProductDetailInfo (rank ํฌํจ)
deactivate PF
API->>API: ProductV1Dto.ProductDetailResponse๋ก ๋ณํ
API-->>Client: ApiResponse (ProductDetailResponse)
deactivate API
sequenceDiagram
autonumber
participant Kafka as Kafka Topics
participant Consumer as ProductMetricsConsumer
participant PMF as ProductMetricsFacade
participant RF as RankingFacade
participant RS as RankingService
participant Redis
Kafka->>Consumer: product-view-metrics
activate Consumer
Consumer->>PMF: processViewMetrics(command)
activate PMF
PMF->>PMF: ์กฐํ ๋ฉํธ๋ฆญ ์ฒ๋ฆฌ
PMF->>RF: processViewEvent(productId)
activate RF
RF->>RS: incrementViewScore(productId, today)
activate RS
RS->>Redis: ZINCRBY key(today), viewScore, productId
RS->>Redis: EXPIRE key(today), 48h
Redis-->>RS: ์ฑ๊ณต
RS-->>RF: ์๋ฃ
deactivate RS
RF-->>PMF: ์๋ฃ
deactivate RF
PMF-->>Consumer: ์๋ฃ
deactivate PMF
Consumer->>Kafka: ack
deactivate Consumer
Note over Redis: ์ ์ ๋์ (view, like, order ๋ชจ๋ ๋์ผํ ํ๋ฆ)
Estimated code review effort๐ฏ 4 (Complex) | โฑ๏ธ ~45 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touchesโ Failed checks (1 warning)
โ Passed checks (2 passed)
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 |
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.
Actionable comments posted: 14
๐งน Nitpick comments (22)
apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingKey.java (1)
21-32: ํ ์คํธ ๊ฐ๋ฅ์ฑ์ ์ํด Clock ์ฌ์ฉ ๊ถ์ฅ
today()์yesterday()๋ฉ์๋๊ฐLocalDate.now()๋ฅผ ์ง์ ํธ์ถํ์ฌ ๋จ์ ํ ์คํธ๊ฐ ์ด๋ ต์ต๋๋ค.ClockConfig์์ ์ ๊ณตํ๋Clock๋น์ ํ์ฉํ๊ฑฐ๋,LocalDate๋ฅผ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ๋ ๋ฐฉ์์ผ๋ก ๋ฆฌํฉํ ๋งํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.๐ ๊ถ์ฅ ๋ฆฌํฉํ ๋ง
์ต์ 1: Clock์ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ๊ธฐ
- public static String today() { - return daily(LocalDate.now()); + public static String today(Clock clock) { + return daily(LocalDate.now(clock)); } - public static String yesterday() { - return daily(LocalDate.now().minusDays(1)); + public static String yesterday(Clock clock) { + return daily(LocalDate.now(clock).minusDays(1)); }์ต์ 2: ์ด ๋ฉ์๋๋ค์ ์ ๊ฑฐํ๊ณ ํธ์ถํ๋ ์ชฝ์์
daily(LocalDate.now(clock))๋ฅผ ์ง์ ํธ์ถapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingCommand.java (2)
16-27: ๋ฌต์์ ๊ฒ์ฆ ๋ฐฉ์ ์ฌ๊ฒํ ๊ถ์ฅ์ ํจํ์ง ์์ ์ ๋ ฅ๊ฐ์ ์๋์ผ๋ก ๋ณด์ ํ๊ณ ์์ต๋๋ค. ์ด๋ ํธ์ถ์์ ๋ฒ๊ทธ๋ฅผ ์จ๊ธธ ์ ์์ต๋๋ค. ๋ค์ ์ฌํญ์ ๊ณ ๋ คํด์ฃผ์ธ์:
- ์๋ชป๋ ์ ๋ ฅ์ ๋ํด ์์ธ๋ฅผ ๋์ง๊ฑฐ๋
- ์ต์ํ ๊ฒฝ๊ณ ๋ก๊ทธ๋ฅผ ๋จ๊ธฐ๊ฑฐ๋
- ํ์ฌ ๋ฐฉ์์ ์ ์งํ๋ ๋ฌธ์ํ
๋ํ
LocalDate.now()๋ฅผ ์ง์ ํธ์ถํ์ฌ ํ ์คํธ๊ฐ ์ด๋ ต์ต๋๋ค.Clock์ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.๐ ๊ฐ์ ์ ์
์ต์ 1: ์์ธ ๋์ง๊ธฐ
public RankingCommand { - if (page < 0) { - page = 0; + if (page < 0) { + throw new IllegalArgumentException("Page must be non-negative: " + page); } - if (size <= 0 || size > MAX_PAGE_SIZE) { - size = DEFAULT_PAGE_SIZE; + if (size <= 0 || size > MAX_PAGE_SIZE) { + throw new IllegalArgumentException("Size must be between 1 and " + MAX_PAGE_SIZE); } if (date == null) { - date = LocalDate.now(); + throw new IllegalArgumentException("Date must not be null"); } }์ต์ 2: Clock ํ๋ผ๋ฏธํฐ ์ถ๊ฐ
public RankingCommand { if (date == null) { throw new IllegalArgumentException("Use today() factory method instead of passing null date"); } // ... validation } public static RankingCommand today(int page, int size, Clock clock) { return new RankingCommand(LocalDate.now(clock), page, size); }
38-47: ํ์ฑ ์คํจ ์ ๋ก๊น ๊ณ ๋ ค
parseDate๋ฉ์๋๊ฐ ํ์ฑ ์คํจ ์ ํ์ฌ ๋ ์ง๋ฅผ ๋ฐํํ์ฌ ์ค๋ฅ๋ฅผ ์จ๊น๋๋ค. ๋๋ฒ๊น ์ ์ํด ํ์ฑ ์คํจ๋ฅผ ๋ก๊น ํ๋ ๊ฒ์ ๊ณ ๋ คํด์ฃผ์ธ์.๐ ๋ก๊น ์ถ๊ฐ ์ ์
+ private static final Logger log = LoggerFactory.getLogger(RankingCommand.class); + private static LocalDate parseDate(String dateString) { if (dateString == null || dateString.isBlank()) { return LocalDate.now(); } try { return LocalDate.parse(dateString, DATE_FORMATTER); } catch (DateTimeParseException e) { + log.warn("Failed to parse date string: {}, using current date", dateString, e); return LocalDate.now(); } }apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingKey.java (1)
21-22: ํ ์คํธ ๊ฐ๋ฅ์ฑ์ ์ํด Clock ์ฌ์ฉ ๊ถ์ฅ
today()๋ฉ์๋๊ฐLocalDate.now()๋ฅผ ์ง์ ํธ์ถํ์ฌ ๋จ์ ํ ์คํธ๊ฐ ์ด๋ ต์ต๋๋ค.ClockConfig์์ ์ ๊ณตํ๋Clock๋น์ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ๊ฑฐ๋, ํธ์ถํ๋ ์ชฝ์์daily(LocalDate.now(clock))๋ฅผ ์ง์ ์ฌ์ฉํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingScheduler.java (1)
21-22: carryOverWeight ์ ํจ์ฑ ๊ฒ์ฆ์ ๊ณ ๋ คํ์ธ์.
carryOverWeight์ ๋ํ ๋ฒ์ ๊ฒ์ฆ์ด ์์ต๋๋ค. ์์๋ 1.0์ ์ด๊ณผํ๋ ๊ฐ์ด ์ค์ ๋๋ฉด ์์์น ๋ชปํ ๋์์ด ๋ฐ์ํ ์ ์์ต๋๋ค.@Value์ฃผ์ ํ@PostConstruct์์ ๋ฒ์๋ฅผ ๊ฒ์ฆํ๊ฑฐ๋,@Validated์@DecimalMin/@DecimalMax์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.๐ ๊ถ์ฅ ์์ ์
+import jakarta.annotation.PostConstruct; + @Slf4j @Component @RequiredArgsConstructor public class RankingScheduler { private final RankingService rankingService; private final Clock clock; @Value("${ranking.carry-over.weight:0.1}") private double carryOverWeight; + + @PostConstruct + public void validateConfiguration() { + if (carryOverWeight < 0.0 || carryOverWeight > 1.0) { + throw new IllegalArgumentException( + "ranking.carry-over.weight must be between 0.0 and 1.0, but was: " + carryOverWeight + ); + } + }apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java (2)
25-34: ํ๋ผ๋ฏธํฐ ์ ํจ์ฑ ๊ฒ์ฆ ์ด๋ ธํ ์ด์ ์ ๊ณ ๋ คํ์ธ์.
page,sizeํ๋ผ๋ฏธํฐ์@Min(0)๋ฑ์ ์ ํจ์ฑ ๊ฒ์ฆ ์ด๋ ธํ ์ด์ ์ ์ถ๊ฐํ๋ฉด ์๋ชป๋ ์์ฒญ์ ์กฐ๊ธฐ์ ์ฐจ๋จํ ์ ์์ต๋๋ค. Controller ๋๋ Service ๊ณ์ธต์์ ๊ฒ์ฆํ๋ ๊ฒ๋ ๊ฐ๋ฅํ์ง๋ง, API ์คํ ๋ ๋ฒจ์์ ๋ช ์ํ๋ฉด ๋ฌธ์ํ ์ธก๋ฉด์์๋ ์ ๋ฆฌํฉ๋๋ค.
45-51: ํ๋ผ๋ฏธํฐ ์ ํจ์ฑ ๊ฒ์ฆ ์ด๋ ธํ ์ด์ ์ ๊ณ ๋ คํ์ธ์.
nํ๋ผ๋ฏธํฐ์@Min(1)๋ฑ์ ์ ํจ์ฑ ๊ฒ์ฆ ์ด๋ ธํ ์ด์ ์ ์ถ๊ฐํ๋ฉด ์์๋ 0 ๊ฐ์ ์กฐ๊ธฐ์ ์ฐจ๋จํ ์ ์์ต๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingConfigV1Dto.java (1)
5-9: ๊ฐ์ค์น ํ๋ผ๋ฏธํฐ์ ์ ํจ์ฑ ๊ฒ์ฆ์ ์ถ๊ฐํ์ธ์.
viewWeight,likeWeight,orderWeight์ ๋ํ ์ ํจ์ฑ ๊ฒ์ฆ์ด ์์ต๋๋ค. ์์ ๊ฐ์ด๋ ๋น์ ์์ ์ผ๋ก ํฐ ๊ฐ์ด ์ ๋ ฅ๋๋ฉด ๋ญํน ๊ณ์ฐ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.@Min,@Max,@Positive๋ฑ์ Bean Validation ์ด๋ ธํ ์ด์ ์ ์ถ๊ฐํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.๐ ๊ถ์ฅ ์์ ์
+import jakarta.validation.constraints.PositiveOrZero; + public class RankingConfigV1Dto { public record WeightConfigRequest( + @PositiveOrZero double viewWeight, + @PositiveOrZero double likeWeight, + @PositiveOrZero double orderWeight ) {}apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingV1ApiE2ETest.java (1)
51-70: ํ ์คํธ ์๊ฐ ์์กด์ฑ ๊ณ ๋ ค
LocalDate.now()๋ฅผ ์ง์ ์ฌ์ฉํ๋ฉด ์์ ๊ทผ์ฒ์์ ํ ์คํธ๊ฐ ์คํจํ ์ ์์ต๋๋ค.ClockConfig๊ฐ PR์ ํฌํจ๋์ด ์์ผ๋ฏ๋ก, ํ ์คํธ์์๋ ๊ณ ์ ๋Clock์ ์ฃผ์ ๋ฐ์ ์ฌ์ฉํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingConfigV1ApiSpec.java (1)
35-38: ์ ๋ ฅ ๊ฐ ๊ฒ์ฆ ์ถ๊ฐ ๊ณ ๋ ค
WeightConfigRequest์@Valid์ด๋ ธํ ์ด์ ์ด ์์ต๋๋ค. ๊ฐ์ค์น ๊ฐ์ ๋ฒ์ ๊ฒ์ฆ(์: ์์ ๋ถ๊ฐ, ํฉ๊ณ๊ฐ 1.0 ๋ฑ)์ด ํ์ํ ์ ์์ต๋๋ค.๐ ์ ์ํ๋ ์์
@PutMapping("/weights") RankingConfigV1Dto.WeightConfigResponse updateWeights( - @RequestBody RankingConfigV1Dto.WeightConfigRequest request + @Valid @RequestBody RankingConfigV1Dto.WeightConfigRequest request );apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java (1)
114-125: NumberFormatException ๊ฐ๋ฅ์ฑ
Long.parseLong(tuple.getValue())์์ Redis์ ์๋ชป๋ ๋ฐ์ดํฐ๊ฐ ์ ์ฅ๋ ๊ฒฝ์ฐNumberFormatException์ด ๋ฐ์ํ ์ ์์ต๋๋ค. ํ์ฌ ์์ ๋ฉ์๋์ catch ๋ธ๋ก์์ ์ฒ๋ฆฌ๋์ง๋ง, ์ผ๋ถ ์ ํจํ ์ํธ๋ฆฌ๊ฐ ์์ค๋ ์ ์์ต๋๋ค.๐ ์ ์ํ๋ ์์
private List<RankingEntry> convertToRankingEntries(Set<ZSetOperations.TypedTuple<String>> tuples) { List<RankingEntry> entries = new ArrayList<>(); for (ZSetOperations.TypedTuple<String> tuple : tuples) { if (tuple.getValue() != null && tuple.getScore() != null) { - entries.add(new RankingEntry( - Long.parseLong(tuple.getValue()), - tuple.getScore() - )); + try { + entries.add(new RankingEntry( + Long.parseLong(tuple.getValue()), + tuple.getScore() + )); + } catch (NumberFormatException e) { + log.warn("Invalid productId in ranking: {}", tuple.getValue()); + } } } return entries; }apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (2)
56-70: ์ญ์ ๋ ์ํ ์ฒ๋ฆฌ ์ ์์ ๊ณ์ฐ ๋ถ์ผ์น ๊ฐ๋ฅ์ฑ
productMap.get(entry.productId())๊ฐ null์ ๋ฐํํ๋ฉด ํด๋น ํญ๋ชฉ์ ๊ฑด๋๋ฐ์ง๋ง,startRank + i๋ ์ฌ์ ํ ์ฆ๊ฐํฉ๋๋ค. ์ด๋ก ์ธํด ๋ฐํ๋๋ ์์์ ๋น ๊ณต๊ฐ์ด ์๊ฒจ ์ฌ์ฉ์์๊ฒ ํผ๋์ ์ค ์ ์์ต๋๋ค.์: 1์ ์ํ์ด ์ญ์ ๋ ๊ฒฝ์ฐ, ๊ฒฐ๊ณผ๊ฐ 2์๋ถํฐ ์์ํ๊ฒ ๋ฉ๋๋ค.
๐ ์์ ์ ์
List<RankingInfo> rankings = new ArrayList<>(); - long startRank = (long) command.page() * command.size() + 1; + long currentRank = (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, + currentRank++, entry.score() )); + } else { + log.warn("๋ญํน์ ํฌํจ๋ ์ํ์ ์ฐพ์ ์ ์์: productId={}", entry.productId()); } }
101-116:getRankingPage์ ๋์ผํ null ์ํ ์ฒ๋ฆฌ ๋ฌธ์ ๋ฐ ์ฝ๋ ์ค๋ณต
getTopN์์๋ ๋์ผํ ์์ ๊ณ์ฐ ๋ฌธ์ ๊ฐ ์์ต๋๋ค. ๋ ๋ฉ์๋์ ๋งคํ ๋ก์ง์ ๋ณ๋ ํฌํผ ๋ฉ์๋๋ก ์ถ์ถํ๋ฉด ์ค๋ณต์ ์ค์ด๊ณ ์ผ๊ด์ฑ์ ์ ์งํ ์ ์์ต๋๋ค.apps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingWeightTest.java (1)
163-188: ๊ฐ๋ณ ๊ฐ์ค์น ์ ๋ฐ์ดํธ ๋ฉ์๋ ํ ์คํธ ๋๋ฝ
updateViewWeight,updateLikeWeight,updateOrderWeight๊ฐ๋ณ ๋ฉ์๋์ ๋ํ ํ ์คํธ๊ฐ ์์ต๋๋ค.updateAllWeights๋ง ํ ์คํธํ๊ณ ์์ด ๊ฐ๋ณ ๋ฉ์๋์ ๋์์ ๊ฒ์ฆํ์ง ๋ชปํฉ๋๋ค.๐ ์ถ๊ฐ ํ ์คํธ ์ ์
@Test @DisplayName("๊ฐ๋ณ ๊ฐ์ค์น๋ฅผ ์ ๋ฐ์ดํธํ๋ค") void shouldUpdateIndividualWeight() { // when rankingWeight.updateViewWeight(0.15); // then verify(hashOperations).put(WEIGHT_KEY, "view", "0.15"); }apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingWeight.java (2)
37-39:* 1.0์ฐ์ฐ์ด ๋ถํ์ํจ
getViewWeight() * 1.0์getViewWeight()์ ๋์ผํฉ๋๋ค. ๋ค๋ฅธ ๊ณ์ฐ ๋ฉ์๋์์ ์ผ๊ด์ฑ์ ์ํด ์๋์ ์ธ ๊ฒ์ผ ์ ์์ผ๋, ๋ถํ์ํ ์ฐ์ฐ์ ๋๋ค.
65-75: ๊ฐ์ค์น ๊ฐ ๊ฒ์ฆ ๋ถ์ฌ๊ฐ์ค์น ์ ๋ฐ์ดํธ ์ ์์๋ ๋น์ ์์ ์ผ๋ก ํฐ ๊ฐ์ ๋ํ ๊ฒ์ฆ์ด ์์ต๋๋ค. ์๋ชป๋ ๊ฐ์ค์น๊ฐ ์ค์ ๋๋ฉด ๋ญํน ๊ณ์ฐ์ ์ํฅ์ ์ค ์ ์์ต๋๋ค.
๐ ๊ฒ์ฆ ๋ก์ง ์ถ๊ฐ ์ ์
public void updateViewWeight(double weight) { + validateWeight(weight); updateWeight("view", weight); } + private void validateWeight(double weight) { + if (weight < 0 || weight > 1.0) { + throw new IllegalArgumentException("๊ฐ์ค์น๋ 0๊ณผ 1 ์ฌ์ด์ฌ์ผ ํฉ๋๋ค: " + weight); + } + }apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingService.java (2)
110-123: ๋์ฉ๋ ๋ฐ์ดํฐ์์ ์ฑ๋ฅ ์ ํ ๊ฐ๋ฅ์ฑ
rangeWithScores(fromKey, 0, -1)๋ก ๋ชจ๋ ๋ฉค๋ฒ๋ฅผ ์กฐํํ๊ณ , ๊ฐ๋ณadd()ํธ์ถ๋ก ๋ณต์ฌํฉ๋๋ค. ์ํ ์๊ฐ ๋ง์์ง๋ฉด ๋คํธ์ํฌ ๋ผ์ด๋ํธ๋ฆฝ์ด ์ฆ๊ฐํ์ฌ ์ฑ๋ฅ์ด ์ ํ๋ ์ ์์ต๋๋ค.๐ ํ์ดํ๋ผ์ด๋ ์ฌ์ฉ ์ ์
redisTemplate.executePipelined((RedisCallback<Object>) connection -> { for (ZSetOperations.TypedTuple<String> tuple : tuples) { if (tuple.getValue() != null && tuple.getScore() != null) { double newScore = tuple.getScore() * weight; connection.zSetCommands().zAdd( toKey.getBytes(), newScore, tuple.getValue().getBytes() ); } } return null; });
16-21: ์ ํ์ ๋ฆฌ๋ทฐ - ์ํคํ ์ฒ ์ค๊ณ ์ฌ๊ฒํ ํ์์ด ํ๋ก์ ํธ๋ ๋ ๊ฐ์ ๋ณ๋
RankingService๋ฅผ ๊ฐ์ง ๋ฉํฐ ๋ชจ๋ ๊ตฌ์กฐ์ ๋๋ค:
- commerce-api:
getTopNWithScores,getRankingPage,getRank,getScore๋ฑ ์ฟผ๋ฆฌ ๋ฉ์๋ ํฌํจ- commerce-streamer:
incrementViewScore,carryOverScores๋ฑ ์ฐ๊ธฐ ์์ ๋ง ํฌํจ์ ๊ณต๋ ์ฝ๋(commerce-streamer)์ ์ฟผ๋ฆฌ ๋ฉ์๋๊ฐ ์๋ ๊ฒ์ ์ค๊ณ์ ๋ฐ๋ฅธ ๊ฒ์ ๋๋ค. commerce-api์
RankingFacade๋ commerce-api์RankingService๋ฅผ ํธ์ถํ๋ฏ๋ก, ๋ฉ์๋ ๋๋ฝ์ด ์๋๋ผ ๋ชจ๋ ๊ฐ ์ญํ ๋ถ๋ด์ ๋๋ค. ๋ค๋ง ์ด๋ฌํ ๋ถ๋ฆฌ ๊ตฌ์กฐ๊ฐ ์๋๋ ์ค๊ณ์ธ์ง ํ์ธ์ด ํ์ํฉ๋๋ค.apps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingIntegrationTest.java (4)
56-62: ๋์จํ ๋จ์ธ๋ฌธ(assertion)์ผ๋ก ์ธํด ๊ณ์ฐ ๋ฒ๊ทธ๋ฅผ ๋์น ์ ์์ต๋๋ค.ํ์ฌ
isGreaterThanOrEqualTo(0.4)๋จ์ธ์ ์ค์ ์์๊ฐ์ธ ์ ํํ 0.4๋ฅผ ๊ฒ์ฆํ์ง ์์ต๋๋ค. ๊ฐ์ค์น๊ฐ ์ ํด์ ธ ์๋ค๋ฉด(view=0.1, like=0.2), ๋ถ๋์์์ ๋น๊ต๋ฅผ ์ํดisCloseTo๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ๋ ์ ํํฉ๋๋ค.๐ ์ ์๋ ์์
- assertThat(score).isGreaterThanOrEqualTo(0.4); + assertThat(score).isCloseTo(0.4, org.assertj.core.api.Assertions.within(0.01));
98-102: TTL ์ ํจ ๋ฒ์ ๊ฒ์ฆ์ ๊ถ์ฅํฉ๋๋ค.ํ์ฌ TTL์ด 0๋ณด๋ค ํฌ๋ค๋ ๊ฒ๋ง ํ์ธํฉ๋๋ค. ์ค์ ๋ TTL ๊ฐ(์: 2์ผ)์ด ์ฌ๋ฐ๋ฅด๊ฒ ์ ์ฉ๋์๋์ง ๋ฒ์ ๊ฒ์ฆ์ ์ถ๊ฐํ๋ฉด ์ค์ ์ค๋ฅ๋ฅผ ์กฐ๊ธฐ์ ๋ฐ๊ฒฌํ ์ ์์ต๋๋ค.
๐ ์ ์๋ ์์ ์์
Long ttl = redisTemplate.getExpire(key); assertThat(ttl).isNotNull(); - assertThat(ttl).isGreaterThan(0); + // ์์ TTL์ด 2์ผ(172800์ด)์ธ ๊ฒฝ์ฐ + assertThat(ttl).isGreaterThan(0); + assertThat(ttl).isLessThanOrEqualTo(172800L); // ์ต๋ 2์ผ
174-177: ๋ถ๋์์์ ๋น๊ต์isCloseTo์ฌ์ฉ์ ๊ถ์ฅํฉ๋๋ค.
isEqualTo๋ฅผ ์ฌ์ฉํ double ๋น๊ต๋ ๋ถ๋์์์ ์ ๋ฐ๋ ๋ฌธ์ ๋ก ์ธํด ๋ถ์์ ํ ์ ์์ต๋๋ค. ๋ค๋ฅธ ํ ์คํธ(line 131)์ ์ผ๊ด์ฑ์ ์ํดisCloseTo๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์ต๋๋ค.๐ ์ ์๋ ์์
- assertThat(rankingWeight.getViewWeight()).isEqualTo(newViewWeight); - assertThat(rankingWeight.getLikeWeight()).isEqualTo(newLikeWeight); - assertThat(rankingWeight.getOrderWeight()).isEqualTo(newOrderWeight); + assertThat(rankingWeight.getViewWeight()).isCloseTo(newViewWeight, within(0.001)); + assertThat(rankingWeight.getLikeWeight()).isCloseTo(newLikeWeight, within(0.001)); + assertThat(rankingWeight.getOrderWeight()).isCloseTo(newOrderWeight, within(0.001));
within์ ์ฌ์ฉํ๋ ค๋ฉด static import๋ฅผ ์ถ๊ฐํ์ธ์:import static org.assertj.core.api.Assertions.within;
189-193: ๊ธฐ๋ณธ๊ฐ ์์ํ๋ฅผ ๊ณ ๋ คํด ์ฃผ์ธ์.ํ ์คํธ์ ํ๋์ฝ๋ฉ๋ ๊ธฐ๋ณธ๊ฐ(0.1, 0.2, 0.7)์ด
RankingWeight๊ตฌํ์ฒด์ ์ค์ ๊ธฐ๋ณธ๊ฐ๊ณผ ๋๊ธฐํ๋์ด์ผ ํฉ๋๋ค. ๊ธฐ๋ณธ๊ฐ์ด ๋ณ๊ฒฝ๋๋ฉด ํ ์คํธ๊ฐ ์คํจํ๋ฏ๋ก, ์์๋ฅผ ๊ณต์ ํ๊ฑฐ๋RankingWeight์์ ๊ธฐ๋ณธ๊ฐ์ ์ ๊ณตํ๋ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ ์ง๋ณด์์ ์ ๋ฆฌํฉ๋๋ค.
๐ Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (34)
apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.javaapps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingCommand.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.javaapps/commerce-api/src/main/java/com/loopers/application/ranking/RankingPageInfo.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingEntry.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingInfo.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingKey.javaapps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.javaapps/commerce-api/src/main/java/com/loopers/support/config/ClockConfig.javaapps/commerce-api/src/test/java/com/loopers/domain/ranking/RankingServiceTest.javaapps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingV1ApiE2ETest.javaapps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.javaapps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsFacade.javaapps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingFacade.javaapps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingScheduler.javaapps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingKey.javaapps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingService.javaapps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingWeight.javaapps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingConfigV1ApiSpec.javaapps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingConfigV1Controller.javaapps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingConfigV1Dto.javaapps/commerce-streamer/src/main/resources/application.ymlapps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingIntegrationTest.javaapps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingSchedulerTest.javaapps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingServiceTest.javaapps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingWeightTest.javaapps/commerce-streamer/src/test/java/com/loopers/interfaces/ProductMetricsConsumerTest.java
๐งฐ Additional context used
๐ง Learnings (7)
๐ Common learnings
Learnt from: junoade
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 0
File: :0-0
Timestamp: 2025-12-02T08:12:06.383Z
Learning: ProductQueryService์์ ์ํ ๋ชฉ๋ก ์กฐํ ์ Redis ์บ์๋ฅผ ์ ์ฉํ์ผ๋ฉฐ, ์บ์ ํค๋ brandId, sortType, pageNumber, pageSize์ ์กฐํฉ์ผ๋ก ๊ตฌ์ฑ๋๊ณ TTL์ 5๋ถ์ผ๋ก ์ค์ ๋์ด ์๋ค.
๐ Learning: 2025-12-22T16:33:50.678Z
Learnt from: jikimee64
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 71
File: apps/commerce-streamer/src/main/kotlin/com/loopers/application/ranking/RankingFacade.kt:167-173
Timestamp: 2025-12-22T16:33:50.678Z
Learning: In the Loopers-dev-lab/loopers-spring-kotlin-template repository, using system default timezone (ZonedDateTime.now() without explicit ZoneId) in date key calculations is an intentional design choice. The deployment ensures all instances share the same timezone configuration.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/support/config/ClockConfig.java
๐ Learning: 2025-12-02T08:12:06.383Z
Learnt from: junoade
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 0
File: :0-0
Timestamp: 2025-12-02T08:12:06.383Z
Learning: ProductQueryService์์ ์ํ ๋ชฉ๋ก ์กฐํ ์ Redis ์บ์๋ฅผ ์ ์ฉํ์ผ๋ฉฐ, ์บ์ ํค๋ brandId, sortType, pageNumber, pageSize์ ์กฐํฉ์ผ๋ก ๊ตฌ์ฑ๋๊ณ TTL์ 5๋ถ์ผ๋ก ์ค์ ๋์ด ์๋ค.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
๐ Learning: 2025-11-27T09:09:24.961Z
Learnt from: sky980221
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 121
File: apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java:22-24
Timestamp: 2025-11-27T09:09:24.961Z
Learning: Product ์ํฐํฐ (apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java)๋ ์ ์ฆ์ผ์ด์ค๋ณ๋ก ์๋์ ์ผ๋ก ๋ค๋ฅธ ๋ฝ ์ ๋ต์ ์ฌ์ฉํ๋ค: ์ข์์ ๊ธฐ๋ฅ์๋ ๋น๊ด์ ๋ฝ(findByIdForUpdate)์, ์ฌ๊ณ ์ฐจ๊ฐ์๋ ๋๊ด์ ๋ฝ(Version + ์ฌ์๋)์ ์ฌ์ฉํ๋ค.
Applied to files:
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.javaapps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java
๐ Learning: 2025-12-19T20:59:57.713Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 68
File: docs/week8/round8-detailed-design.md:151-178
Timestamp: 2025-12-19T20:59:57.713Z
Learning: In the Loopers-dev-lab/loopers-spring-kotlin-template repository's Kafka event pipeline, only 5 domain events are intentionally published to Kafka via CloudEventEnvelopeFactory: OrderPaidEventV1, LikeCreatedEventV1, LikeCanceledEventV1, ProductViewedEventV1, and StockDepletedEventV1. Other domain events (OrderCreatedEventV1, OrderCanceledEventV1, PaymentCreatedEventV1, PaymentPaidEventV1, PaymentFailedEventV1) are internal-only and intentionally not mapped in resolveMetadata(), which correctly returns null for them to exclude them from Outbox publication.
Applied to files:
apps/commerce-streamer/src/test/java/com/loopers/interfaces/ProductMetricsConsumerTest.java
๐ Learning: 2025-12-18T13:24:54.339Z
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 190
File: apps/commerce-streamer/src/main/java/com/loopers/applications/streamer/consumer/product/IncreaseProductViewKafkaConsumer.java:25-35
Timestamp: 2025-12-18T13:24:54.339Z
Learning: In this codebase, Kafka consumers delegate error handling and event tracking to the service layer via EventInboxAspect. Service methods annotated with InboxEvent are intercepted by the aspect, which handles failures by saving failed EventInbox entries and logging errors. This centralized approach avoids duplicating error handling logic across multiple consumers.
Applied to files:
apps/commerce-streamer/src/test/java/com/loopers/interfaces/ProductMetricsConsumerTest.java
๐ Learning: 2025-12-19T23:39:20.851Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 68
File: apps/commerce-streamer/src/main/kotlin/com/loopers/interfaces/consumer/product/ProductEventConsumer.kt:0-0
Timestamp: 2025-12-19T23:39:20.851Z
Learning: In Loopers-dev-lab/loopers-spring-kotlin-template, toongri prefers pragmatic idempotency handling in Kafka consumers: when idempotency key persistence fails after successful business logic execution, log a warning and continue (accepting low risk of duplicates on retry) rather than rolling back business data. This keeps business logic decoupled from idempotency store (which might be Redis/external system, not RDB), avoids over-engineering for rare edge cases, and prioritizes core business value over auxiliary metadata. Transaction-based atomicity between business data and idempotency keys is considered excessive when duplicate event probability is very low.
Applied to files:
apps/commerce-streamer/src/test/java/com/loopers/interfaces/ProductMetricsConsumerTest.java
๐งฌ Code graph analysis (2)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java (1)
ProductV1Dto(8-75)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingConfigV1Controller.java (1)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingConfigV1Dto.java (1)
RankingConfigV1Dto(3-20)
๐ Additional comments (35)
apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java (1)
7-7: @EnableScheduling ํ์ฑํ๋ ์์ ํฉ๋๋คโcarryOverScores๊ฐ ๋ฉฑ๋ฑ์ฑ์ ์ ๊ณตํฉ๋๋ค.RankingScheduler์ carryOverScores ๋ฉ์๋๋ ์ด๋ฏธ ์ค๋น๋ ๋ญํน ๋ฐ์ดํฐ๊ฐ ์กด์ฌํ๋ฉด ์กฐ๊ธฐ์ ๋ฐํํ๋๋ก ์ค๊ณ๋์ด ์์ต๋๋ค(line 104-108). ๋ค์ค ์ธ์คํด์ค ํ๊ฒฝ์์ ์ฒซ ๋ฒ์งธ ์ธ์คํด์ค๊ฐ ์คํ๋์ด ๋ฐ์ดํฐ๋ฅผ ์ค๋นํ๋ฉด, ์ดํ ์ธ์คํด์ค๋ค์ ๋ฐ์ดํฐ ์กด์ฌ ์ฌ๋ถ๋ฅผ ํ์ธํ๊ณ ์์ ์ ๊ฑด๋๋๋๋ค. ๋ฐ๋ผ์ ์ค๋ณต ์คํ์ ๋ํ ์ฐ๋ ค๋ ์ ๊ฑฐ๋๋ฉฐ, ๋ถ์ฐ ๋ฝ์ด ํ์ํ์ง ์์ต๋๋ค.
Likely an incorrect or invalid review comment.
apps/commerce-streamer/src/main/resources/application.yml (1)
17-17: ์ ํ๋ฆฌ์ผ์ด์ ์ด๋ฆ ์์ ํ์ธ๋ชจ๋ ์ด๋ฆ๊ณผ ์ผ์นํ๋๋ก
commerce-streamer๋ก ์ฌ๋ฐ๋ฅด๊ฒ ์์ ๋์์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java (3)
14-16: rank ํ๋ ์ถ๊ฐ ํ์ธ์์ ์ ๋ณด๋ฅผ ๋ด๋
rankํ๋๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ์ถ๊ฐ๋์์ต๋๋ค. nullableLongํ์ ์ ์ฌ์ฉํ์ฌ ์์๊ฐ ์๋ ๊ฒฝ์ฐnull๋ก ํํํ ์ ์์ต๋๋ค.
17-41: ํฉํ ๋ฆฌ ๋ฉ์๋ ๊ตฌํ ํ์ธ๊ธฐ์กด
of๋ฉ์๋๋ฅผ ์ ์งํ๋ฉด์ rank๋ฅผ null๋ก ์ค์ ํ๊ณ , ์๋ก์ด ์ค๋ฒ๋ก๋ ๋ฉ์๋๋ฅผ ์ถ๊ฐํ์ฌ ํ์ ํธํ์ฑ๊ณผ ํ์ฅ์ฑ์ ๋ชจ๋ ํ๋ณดํ์ต๋๋ค.
43-54: withRank ๋ฉ์๋ ๊ตฌํ ํ์ธ๋ถ๋ณ ๋ ์ฝ๋ ํจํด์ ๋ฐ๋ผ ์ ์ธ์คํด์ค๋ฅผ ์์ฑํ๋
withRank๋ฉ์๋๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์์ต๋๋ค.apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingEntry.java (1)
1-7: LGTM!Redis ZSET ์ํธ๋ฆฌ๋ฅผ ํํํ๋ ๊ฐ๋จํ๊ณ ๋ช ํํ ๋๋ฉ์ธ ๋ ์ฝ๋์ ๋๋ค.
apps/commerce-api/src/main/java/com/loopers/support/config/ClockConfig.java (1)
1-15: Clock ๋น ์ค์ ํ์ธ
Clock.systemDefaultZone()์ ์ฌ์ฉํ์ฌ ์์คํ ๊ธฐ๋ณธ ํ์์กด์ผ๋ก Clock ๋น์ ์ ๊ณตํฉ๋๋ค. ํ์ต๋ ๋ด์ฉ์ ๋ฐ๋ฅด๋ฉด ๋ชจ๋ ์ธ์คํด์ค๊ฐ ๋์ผํ ํ์์กด ์ค์ ์ ๊ณต์ ํ๋๋ก ๋ฐฐํฌ ํ๊ฒฝ์ด ๊ตฌ์ฑ๋์ด ์์ด ์ด ์ค์ ์ด ์ ์ ํฉ๋๋ค. ์ด ๋น์ ํ์ฉํ์ฌ ๋ค๋ฅธ ์ปดํฌ๋ํธ๋ค์ ํ ์คํธ ๊ฐ๋ฅ์ฑ์ ๊ฐ์ ํ ์ ์์ต๋๋ค.Based on learnings, the deployment ensures all instances share the same timezone configuration.
apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingInfo.java (1)
1-21: ์์ ์ ๋ณด ๋ ์ฝ๋ ๊ตฌํ ํ์ธ์ํ ์์ ์ ๋ณด๋ฅผ ๋ด๋
RankingInfo๋ ์ฝ๋๊ฐ ์ ๊ตฌํ๋์์ต๋๋ค.ofํฉํ ๋ฆฌ ๋ฉ์๋๋ ๋จ์ํ ์์ฑ์๋ฅผ ํธ์ถํ์ง๋ง, API ์ผ๊ด์ฑ์ ์ํ ํจํด์ผ๋ก ์ ์ ํฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java (1)
1-75: LGTM!DTO ๊ตฌ์กฐ๊ฐ ๋ช ํํ๊ณ ,
ProductDetailResponse์rankํ๋๊ฐ nullableLongํ์ ์ผ๋ก ์ ์ธ๋์ด ์์๊ฐ ์์ ๋ null์ ๋ฐํํ๋ PR ๋ชฉํ์ ์ผ์นํฉ๋๋ค. ๋งคํ ๋ก์ง๋ ๊ฐ๊ฒฐํฉ๋๋ค.apps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingSchedulerTest.java (1)
1-71: LGTM!์ค์ผ์ค๋ฌ ํ ์คํธ๊ฐ ์ ์์ฑ๋์์ต๋๋ค. ๊ณ ์ ๋ ์๊ณ๋ฅผ ์ฌ์ฉํ์ฌ ๋ ์ง ๊ณ์ฐ์ ๊ฒ์ฆํ๊ณ , ์์ธ ๋ฐ์ ์์๋ ์ ์ ์ข ๋ฃ๋๋์ง ํ์ธํฉ๋๋ค.
apps/commerce-streamer/src/test/java/com/loopers/interfaces/ProductMetricsConsumerTest.java (2)
198-222: LGTM!๋ญํน ์ ๋ฐ์ดํธ ํ ์คํธ๊ฐ ์ ์ ํ๊ฒ ์์ฑ๋์์ต๋๋ค.
processViewMetricsํธ์ถ๊ณผproductId๊ฒ์ฆ์ด ์ ํํฉ๋๋ค.
224-243: LGTM!์๋ชป๋ JSON ๋ฉ์์ง๋ฅผ ๊ฑด๋๋ฐ๊ณ ๋ค์ ๋ฉ์์ง๋ฅผ ์ ์ ์ฒ๋ฆฌํ๋ ๋ก์ง์ ์ ๊ฒ์ฆํ๊ณ ์์ต๋๋ค.
apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingScheduler.java (1)
28-42: LGTM!์ค์ผ์ค๋ฌ ๋ก์ง์ด ๋ช ํํ๊ฒ ๊ตฌํ๋์์ต๋๋ค. ๋ช ์์ ์ผ๋ก today/tomorrow๋ฅผ ๊ณ์ฐํ๊ณ , ์์ธ ์ฒ๋ฆฌ๋ ์ ์ ํฉ๋๋ค. Clock ์ฃผ์ ์ ํตํ ํ ์คํธ ๊ฐ๋ฅ์ฑ๋ ์ฐ์ํฉ๋๋ค.
apps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingServiceTest.java (1)
1-203: LGTM!ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง๊ฐ ์ฐ์ํฉ๋๋ค. ์ ์ ์ผ์ด์ค, ์ฃ์ง ์ผ์ด์ค(TTL ์ค์ , ์ด๋ฏธ ์ค๋น๋ ๋ญํน), ์์/์์ ์ ์ ์ฒ๋ฆฌ ๋ฑ์ ๋ชจ๋ ๊ฒ์ฆํ๊ณ ์์ต๋๋ค. ์ค์ฒฉ ํด๋์ค๋ก ํ ์คํธ๋ฅผ ๊ทธ๋ฃนํํ ๊ตฌ์กฐ๋ ๊ฐ๋ ์ฑ์ด ์ข์ต๋๋ค.
apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingConfigV1Dto.java (1)
16-18: LGTM!์ ์ ํฉํ ๋ฆฌ ๋ฉ์๋๊ฐ ๊ฐ๋จํ๊ณ ๋ช ํํฉ๋๋ค.
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (2)
20-35: LGTM!
getProducts๋ฉ์๋ ๊ตฌํ์ด ๊น๋ํฉ๋๋ค.ProductGetListCommand๋ฅผ ํตํ ์ปค๋งจ๋ ํจํด ์ฌ์ฉ๊ณผ DTO ๋ณํ์ด ์ ์ ํฉ๋๋ค.
37-48: LGTM!
userIdnull/blank ์ฒดํฌ ๋ก์ง์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์ด ์์ต๋๋ค. ๋ก๊ทธ์ธ/๋น๋ก๊ทธ์ธ ์ฌ์ฉ์ ๋ถ๊ธฐ ์ฒ๋ฆฌ๊ฐ ๋ช ํํฉ๋๋ค.apps/commerce-api/src/test/java/com/loopers/interfaces/api/ranking/RankingV1ApiE2ETest.java (1)
186-210: LGTM!Top-N ๋ญํน ์กฐํ ํ ์คํธ๊ฐ ์ ์ ํ๊ฒ ๊ตฌํ๋์ด ์์ต๋๋ค.
apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)
25-30: LGTM!
RankingCommand.of()๋ฅผ ํตํ ์ปค๋งจ๋ ์์ฑ์ด ๊ฐ๊ฒฐํฉ๋๋ค. ๋ ์ง/ํ์ด์ง ํ์ฑ ๋ก์ง์ด ์ปค๋งจ๋ ๋ด๋ถ์์ ์ฒ๋ฆฌ๋๋ ๊ฒ์ผ๋ก ๋ณด์ ๋๋ค.apps/commerce-api/src/main/java/com/loopers/domain/ranking/RankingService.java (1)
25-41: LGTM!Top-N ์กฐํ ๋ก์ง์ด ์ ์ ํฉ๋๋ค. ์์ธ ๋ฐ์ ์ ๋น ๋ฆฌ์คํธ๋ฅผ ๋ฐํํ๋ graceful degradation ํจํด์ด ์ ์ ์ฉ๋์ด ์์ต๋๋ค.
apps/commerce-api/src/test/java/com/loopers/domain/ranking/RankingServiceTest.java (2)
22-37: LGTM!ํ ์คํธ ์ ์ ์ด ๊น๋ํฉ๋๋ค.
RedisTemplate๊ณผZSetOperations๋ชจํน์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค.
222-233: LGTM!
TypedTupleํฌํผ ๋ฉ์๋๊ฐ ํ ์คํธ ๊ฐ๋ ์ฑ์ ๋์ฌ์ค๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingPageInfo.java (1)
27-29: LGTM!
empty()ํฉํ ๋ฆฌ ๋ฉ์๋๊ฐ ๋น ๊ฒฐ๊ณผ์ ๋ํ ์ ์ ํ ๊ธฐ๋ณธ๊ฐ์ ์ ๊ณตํฉ๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingConfigV1Controller.java (1)
8-22: LGTM!์ปจํธ๋กค๋ฌ ๊ตฌํ์ด ๊น๋ํฉ๋๋ค.
RankingWeight๋๋ฉ์ธ ์๋น์ค์ ์์ํ๋ ํจํด์ด ์ ์ ํฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java (1)
121-133: LGTM!
Clock์ฃผ์ ์ ํตํด ํ ์คํธ ์ฉ์ด์ฑ์ ํ๋ณดํ ์ข์ ์ค๊ณ์ ๋๋ค.apps/commerce-api/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java (1)
10-71: LGTM!DTO ๊ตฌ์กฐ๊ฐ ๊น๋ํ๊ณ ,
DateTimeFormatter๋ฅผstatic final๋ก ์ ์ธํ์ฌ ์ค๋ ๋ ์์ ์ฑ์ ํ๋ณดํ์ต๋๋ค. Factory ๋ฉ์๋๋ฅผ ํตํ ๋งคํ ํจํด๋ ์ ์ ํฉ๋๋ค.apps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingWeightTest.java (1)
1-35: LGTM!ํ ์คํธ ๊ตฌ์กฐ๊ฐ
@Nestedํด๋์ค๋ก ์ ์ ๋ฆฌ๋์ด ์๊ณ , Redis ์์ธ ์ฒ๋ฆฌ ๋ฐ ๊ธฐ๋ณธ๊ฐ ํด๋ฐฑ ์๋๋ฆฌ์ค๋ฅผ ์ ์ ํ ๊ฒ์ฆํ๊ณ ์์ต๋๋ค.apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsFacade.java (2)
65-68: LGTM!์ฌ๊ณ ๊ฐ์(
DECREASED) ์์๋ง ์ฃผ๋ฌธ ์ด๋ฒคํธ๋ฅผ ๋ญํน์ ๋ฐ์ํ๋ ๋ก์ง์ด ์ฌ๋ฐ๋ฆ ๋๋ค.
46-49: ๋ญํน ์ ๋ฐ์ดํธ๋ ์ด๋ฏธ ํธ๋์ญ์ ์ํฅ์ ๊ณ ๋ คํ ์ค๊ณ์ ๋๋ค
RankingService.incrementScore()๋ฉ์๋์์ ๋ชจ๋ ์์ธ๊ฐ ์บ์น๋์ด ๋ก๊ทธ๋ง ๊ธฐ๋กํ๊ณ ์ฌ๋ฐ์๋์ง ์์ต๋๋ค. ๋ฐ๋ผ์ ๋ญํน ์ ๋ฐ์ดํธ ์คํจ ์ ๋ฉํธ๋ฆญ ์ ์ฅ ํธ๋์ญ์ ์ด ๋กค๋ฐฑ๋์ง ์์ผ๋ฏ๋ก ์ถ๊ฐ ์์ ์ด ๋ถํ์ํฉ๋๋ค.Likely an incorrect or invalid review comment.
apps/commerce-streamer/src/main/java/com/loopers/application/ranking/RankingFacade.java (2)
15-22: LGTM!
Clock์ฃผ์ ์ผ๋ก ํ ์คํธ ์ฉ์ด์ฑ์ ํ๋ณดํ๊ณ , ๋จ์ผ ์ฑ ์ ์์น์ ์ ๋ฐ๋ฅด๋ ๊น๋ํ Facade ์ค๊ณ์ ๋๋ค.
27-49:RankingService์ ์์ธ ์ฒ๋ฆฌ ํ์ธ๋จ๊ฒ์ฆ ๊ฒฐ๊ณผ,
RankingService๋ ๋ด๋ถ์ ์ผ๋ก ๋ชจ๋ ์ ์ ์ ๋ฐ์ดํธ ์์ (incrementScore๋ฉ์๋, 62-77์ค)์ ๋ํด ์์ธ๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค. ๋ฐ์ํ ์์ธ๋ ๋ก๊น ๋๊ณ ์ฌ๋ฐ์ํ์ง ์์ผ๋ฏ๋ก,ProductMetricsFacade์ ํธ๋์ญ์ ์ ์ํฅ์ ์ฃผ์ง ์์ต๋๋ค. ๋ฐ๋ผ์RankingFacade์์ ์ถ๊ฐ ์์ธ ์ฒ๋ฆฌ๋ ๋ถํ์ํฉ๋๋ค.apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java (1)
15-62: LGTM!OpenAPI ๋ช ์ธ๊ฐ ์ ์ ์๋์ด ์๊ณ , ํ๊ธ ์ค๋ช ์ด ํฌํจ๋์ด ์์ต๋๋ค. ๋ญํน ์ ๋ณด๊ฐ ์ํ ์์ธ ์กฐํ์ ํฌํจ๋๋ค๋ ์ ์ด ๋ฌธ์ํ๋์ด ์์ต๋๋ค.
apps/commerce-streamer/src/main/java/com/loopers/domain/ranking/RankingService.java (1)
80-93: LGTM!
getRankingSize๋ฉ์๋์ ์์ธ ์ฒ๋ฆฌ๊ฐ ์ ์ ํ๋ฉฐ, null ์ฒดํฌ์ ๊ธฐ๋ณธ๊ฐ ๋ฐํ์ด ์ ๋์ด ์์ต๋๋ค.apps/commerce-streamer/src/test/java/com/loopers/domain/ranking/RankingIntegrationTest.java (2)
13-39: LGTM!ํ ์คํธ ์ค์ ์ด ์ ๊ตฌ์ฑ๋์ด ์์ต๋๋ค.
@BeforeEach๋ก ๊ฐ ํ ์คํธ ์ Redis๋ฅผ ์ ๋ฆฌํ์ฌ ํ ์คํธ ๊ฒฉ๋ฆฌ๋ฅผ ๋ณด์ฅํ๊ณ , ๊ณ ์ ๋testDate๋ฅผ ์ฌ์ฉํ์ฌ ์ฌํ ๊ฐ๋ฅํ ํ ์คํธ๋ฅผ ๊ตฌํํ์ต๋๋ค.
105-157: LGTM!Carry-over ํ ์คํธ๊ฐ ์ ๊ตฌํ๋์ด ์์ต๋๋ค. ํนํ line 131์์ ๋ถ๋์์์ ๋น๊ต์
isCloseTo๋ฅผ ์ฌ์ฉํ ์ ๊ณผ, ๊ธฐ์กด ๋ฐ์ดํฐ๋ฅผ ๋ฎ์ด์ฐ์ง ์๋ ๋์์ ๊ฒ์ฆํ๋ ํ ์คํธ๊ฐ ์ข์ต๋๋ค.
๐ Summary
๐ฌ Review Points
โ Checklist
๐ Ranking Consumer
โพ Ranking API
Summary by CodeRabbit
Release Notes
โ๏ธ Tip: You can customize this high-level summary in your review settings.