Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
61b0df6
docs: 9์ฃผ์ฐจ ์š”๊ตฌ์‚ฌํ•ญ ๋ถ„์„ ๋ฌธ์„œ ์ž‘์„ฑ
jeonga1022 Dec 21, 2025
77225a4
docs: 9์ฃผ์ฐจ ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ ์ž‘์„ฑ
jeonga1022 Dec 21, 2025
bca67df
feat: ๋žญํ‚น Redis ์„œ๋น„์Šค ๊ธฐ๋ณธ ๊ตฌํ˜„ (getTopProducts, incrementScoreForView)
jeonga1022 Dec 22, 2025
7275f5b
feat: ์ข‹์•„์š” ์‹œ ๋žญํ‚น ์ ์ˆ˜ ์ฆ๊ฐ€ ๊ธฐ๋Šฅ ๊ตฌํ˜„
jeonga1022 Dec 23, 2025
bc18cc4
feat: ์ฃผ๋ฌธ ์‹œ ๋žญํ‚น ์ ์ˆ˜ ์ฆ๊ฐ€ ๊ธฐ๋Šฅ ๊ตฌํ˜„
jeonga1022 Dec 23, 2025
d47efeb
feat: ์ƒํ’ˆ ์กฐํšŒ ๋กœ๊ทธ ์—”ํ‹ฐํ‹ฐ ๋ฐ Repository ์ƒ์„ฑ
jeonga1022 Dec 23, 2025
e38d21b
feat: ๋žญํ‚น ์‹œ์Šคํ…œ์„ ์œ„ํ•œ ์ƒํ’ˆ ์กฐํšŒ ์ด๋ฒคํŠธ ์ถ”๊ฐ€
jeonga1022 Dec 23, 2025
3cf0dd6
feat: ์ƒํ’ˆ ์กฐํšŒ ์ด๋ฒคํŠธ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ Consumer ์ถ”๊ฐ€
jeonga1022 Dec 23, 2025
dc137cf
feat: ZSET TTL 2์ผ ์„ค์ •
jeonga1022 Dec 23, 2025
82bd67b
feat: ์ƒํ’ˆ ์กฐํšŒ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ๊ตฌํ˜„
jeonga1022 Dec 23, 2025
e4a6149
feat: ์ข‹์•„์š” ์ด๋ฒคํŠธ ์‹œ ๋žญํ‚น ์ ์ˆ˜ ๋ฐ˜์˜
jeonga1022 Dec 25, 2025
569b5ae
refactor: ์ด๋ฒคํŠธ๋ณ„ Consumer ๋ถ„๋ฆฌ ๋ฐ ํ† ํ”ฝ ๋ณ€๊ฒฝ
jeonga1022 Dec 25, 2025
045e6cb
feat: Ranking API ๊ตฌํ˜„
jeonga1022 Dec 25, 2025
c5a89f6
feat: ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ์‹œ ๋žญํ‚น ์ˆœ์œ„ ๋ฐ˜ํ™˜
jeonga1022 Dec 25, 2025
f994a91
fix: ProductViewedEvent Jackson ์—ญ์ง๋ ฌํ™” ํ˜ธํ™˜์„ฑ ์ˆ˜์ •
jeonga1022 Dec 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
import com.loopers.domain.product.ProductSortType;
import com.loopers.infrastructure.cache.ProductCacheService;
import com.loopers.infrastructure.cache.ProductDetailCache;
import com.loopers.infrastructure.event.ViewEventPublisher;
import com.loopers.infrastructure.ranking.RankingRedisService;
import com.loopers.interfaces.api.product.ProductDto;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
Expand All @@ -26,6 +29,8 @@ public class ProductFacade {
private final ProductDomainService productDomainService;
private final BrandDomainService brandDomainService;
private final ProductCacheService productCacheService;
private final ViewEventPublisher viewEventPublisher;
private final RankingRedisService rankingRedisService;

/**
* ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ (Cache-Aside ํŒจํ„ด)
Expand Down Expand Up @@ -77,11 +82,18 @@ public ProductDto.ProductDetailResponse getProduct(Long productId) {
// 1. ์บ์‹œ ์กฐํšŒ ์‹œ๋„
Optional<ProductDetailCache> cachedDetail = productCacheService.getProductDetail(productId);

// 2. ์กฐํšŒ ์ด๋ฒคํŠธ ๋ฐœํ–‰ (์บ์‹œ ํžˆํŠธ ์—ฌ๋ถ€์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ)
viewEventPublisher.publish(productId);

// 3. ์ˆœ์œ„ ์กฐํšŒ
Long rank = rankingRedisService.getRankingPosition(LocalDate.now(), productId);
Comment on lines +85 to +89
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸ  Major

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

# Find RankingRedisService implementation
find . -name "*RankingRedisService*" -type f

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 279


๐Ÿ Script executed:

# Search for getRankingPosition method and surrounding code
rg -n -B 5 -A 15 "getRankingPosition" --type java | head -100

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 10577


๐Ÿ Script executed:

# Look at the ProductFacade.java file around line 85-89
fd "ProductFacade.java" --type f | xargs wc -l

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 174


๐Ÿ Script executed:

# Check ProductFacade structure around line 89 for any surrounding error handling
cat -n apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java | sed -n '75,110p'

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 1600


๐Ÿ Script executed:

# Check if there's any exception handling or try-catch at the method level
cat -n apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java | sed -n '80,111p'

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 1426


๐Ÿ Script executed:

# Search for any error handling patterns in ProductFacade class
rg -n "try|catch|throws|Exception" apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java

Repository: Loopers-dev-lab/loopers-spring-java-template

Length of output: 70


Redis ์žฅ์•  ์‹œ ์—๋Ÿฌ ํ•ธ๋“ค๋ง์ด ์—†์Šต๋‹ˆ๋‹ค.

getRankingPosition ํ˜ธ์ถœ์ด try-catch๋กœ ๊ฐ์‹ธ์ ธ ์žˆ์ง€ ์•Š์œผ๋ฉฐ, Redis ์—ฐ๊ฒฐ ์‹คํŒจ ์‹œ ์˜ˆ์™ธ๊ฐ€ ๊ทธ๋Œ€๋กœ ์ „ํŒŒ๋˜์–ด ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ API ์ „์ฒด๊ฐ€ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค. rank ๊ฐ’์ด null์ผ ์ˆ˜๋Š” ์žˆ์ง€๋งŒ(์ƒํ’ˆ์ด ์ˆœ์œ„์— ์—†๋Š” ๊ฒฝ์šฐ), Redis ์˜ˆ์™ธ์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ ์ค‘ ํ•˜๋‚˜์˜ ๋ฐฉ์‹์œผ๋กœ graceful degradation์„ ์ ์šฉํ•ด ์ฃผ์„ธ์š”:

  • getRankingPosition ํ˜ธ์ถœ์„ try-catch๋กœ ๊ฐ์‹ธ๊ณ  ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ null ๋ฐ˜ํ™˜
  • RankingRedisService์—์„œ Redis ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ์•ˆ์ „ํ•˜๊ฒŒ null ๋ฐ˜ํ™˜
  • Redis ์žฅ์•  ์‹œ์—๋„ ์ƒํ’ˆ ์ •๋ณด๋Š” ์ˆœ์œ„ ์—†์ด ๋ฐ˜ํ™˜๋˜๋„๋ก ๊ตฌํ˜„
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
around lines 85-89, the call to rankingRedisService.getRankingPosition is not
protected against Redis failures which can bubble up and break the whole product
detail API; wrap the getRankingPosition call in a try-catch (or handle inside
RankingRedisService) so that on any Redis/connection exception you log a warning
and set rank to null (graceful degradation) and continue returning the product
without rank instead of letting the exception propagate.


if (cachedDetail.isPresent()) {
// Cache Hit: ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ ์ง์ ‘ ๋ฐ˜ํ™˜
return ProductDto.ProductDetailResponse.from(
productId,
cachedDetail.get()
cachedDetail.get(),
rank
);
}

Expand All @@ -94,6 +106,6 @@ public ProductDto.ProductDetailResponse getProduct(Long productId) {
productCacheService.setProductDetail(productId, cache);

// Response ๋ฐ˜ํ™˜
return ProductDto.ProductDetailResponse.from(productId, cache);
return ProductDto.ProductDetailResponse.from(productId, cache, rank);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.loopers.application.ranking;

import com.loopers.domain.product.Product;
import com.loopers.domain.product.ProductRepository;
import com.loopers.infrastructure.ranking.RankingEntry;
import com.loopers.infrastructure.ranking.RankingRedisService;
import com.loopers.interfaces.api.ranking.RankingDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Component
@RequiredArgsConstructor
public class RankingFacade {

private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");

private final RankingRedisService rankingRedisService;
private final ProductRepository productRepository;

public RankingDto.RankingListResponse getRankings(String dateStr, int page, int size) {
LocalDate date = parseDate(dateStr);
int offset = page * size;

List<RankingEntry> entries = rankingRedisService.getTopProducts(date, offset, size);

if (entries.isEmpty()) {
return new RankingDto.RankingListResponse(List.of(), page, size, 0);
}

long totalCount = rankingRedisService.getTotalCount(date);

List<Long> productIds = entries.stream()
.map(RankingEntry::productId)
.toList();

Map<Long, Product> productMap = productRepository.findAllByIdIn(productIds).stream()
.collect(Collectors.toMap(Product::getId, p -> p));

List<RankingDto.RankingResponse> rankings = new ArrayList<>();
int rank = offset + 1;
for (RankingEntry entry : entries) {
Product product = productMap.get(entry.productId());
if (product != null) {
rankings.add(new RankingDto.RankingResponse(
rank++,
product.getId(),
product.getName(),
product.getPrice(),
entry.score()
));
}
}

return new RankingDto.RankingListResponse(rankings, page, size, totalCount);
}

private LocalDate parseDate(String dateStr) {
if (dateStr == null || dateStr.isBlank()) {
return LocalDate.now();
}
return LocalDate.parse(dateStr, DATE_FORMATTER);
}
Comment on lines +64 to +69
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue | ๐ŸŸก Minor

์ž˜๋ชป๋œ ๋‚ ์งœ ํ˜•์‹์— ๋Œ€ํ•œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ํ•„์š”

LocalDate.parse()๊ฐ€ ์ž˜๋ชป๋œ ํ˜•์‹์˜ dateStr์— ๋Œ€ํ•ด DateTimeParseException์„ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. ํ˜„์žฌ๋Š” ์ด ์˜ˆ์™ธ๊ฐ€ ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์•„ 500 ์—๋Ÿฌ๋กœ ์ „ํŒŒ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ ์ˆ˜์ •
 private LocalDate parseDate(String dateStr) {
     if (dateStr == null || dateStr.isBlank()) {
         return LocalDate.now();
     }
-    return LocalDate.parse(dateStr, DATE_FORMATTER);
+    try {
+        return LocalDate.parse(dateStr, DATE_FORMATTER);
+    } catch (DateTimeParseException e) {
+        throw new IllegalArgumentException("์ž˜๋ชป๋œ ๋‚ ์งœ ํ˜•์‹์ž…๋‹ˆ๋‹ค. yyyyMMdd ํ˜•์‹์„ ์‚ฌ์šฉํ•ด์ฃผ์„ธ์š”: " + dateStr, e);
+    }
 }
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java
around lines 64 to 69, LocalDate.parse(dateStr, DATE_FORMATTER) can throw
DateTimeParseException for invalid formats; wrap the parse call in a try/catch
that catches DateTimeParseException, logs the invalid input with context, and
returns a safe default (e.g., LocalDate.now()) or rethrows a controlled
exception (e.g., IllegalArgumentException or a custom BadRequest) depending on
API semantics; ensure you do not let the raw DateTimeParseException propagate
and cause a 500.

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@

public class ProductLikedEvent {

private final Long productId;
private final Long userId;
private final boolean liked;
private final LocalDateTime occurredAt;
private Long productId;
private Long userId;
private boolean liked;
private LocalDateTime occurredAt;

protected ProductLikedEvent() {
}

private ProductLikedEvent(Long productId, Long userId, boolean liked) {
this.productId = productId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,30 @@
import com.loopers.domain.order.Order;

import java.time.LocalDateTime;
import java.util.List;

public class OrderCompletedEvent {

private final Long orderId;
private final String userId;
private final long totalAmount;
private final long discountAmount;
private final long paymentAmount;
private final LocalDateTime occurredAt;
private Long orderId;
private String userId;
private long totalAmount;
private long discountAmount;
private long paymentAmount;
private List<OrderItemInfo> items;
private LocalDateTime occurredAt;

protected OrderCompletedEvent() {
}

private OrderCompletedEvent(Order order) {
this.orderId = order.getId();
this.userId = order.getUserId();
this.totalAmount = order.getTotalAmount();
this.discountAmount = order.getDiscountAmount();
this.paymentAmount = order.getPaymentAmount();
this.items = order.getOrderItems().stream()
.map(item -> new OrderItemInfo(item.getProductId(), item.getQuantity()))
.toList();
this.occurredAt = LocalDateTime.now();
}

Expand Down Expand Up @@ -46,7 +54,32 @@ public long getPaymentAmount() {
return paymentAmount;
}

public List<OrderItemInfo> getItems() {
return items;
}

public LocalDateTime getOccurredAt() {
return occurredAt;
}

public static class OrderItemInfo {
private Long productId;
private Long quantity;

protected OrderItemInfo() {
}

public OrderItemInfo(Long productId, Long quantity) {
this.productId = productId;
this.quantity = quantity;
}

public Long getProductId() {
return productId;
}

public Long getQuantity() {
return quantity;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.loopers.domain.product;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.PrePersist;
import jakarta.persistence.Table;

import java.time.LocalDateTime;

@Entity
@Table(name = "product_view_logs", indexes = {
@Index(name = "idx_product_id_created_at", columnList = "product_id, created_at")
})
public class ProductViewLog {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "product_id", nullable = false)
private Long productId;

@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;

protected ProductViewLog() {
}

private ProductViewLog(Long productId) {
this.productId = productId;
}

public static ProductViewLog create(Long productId) {
return new ProductViewLog(productId);
}

@PrePersist
private void prePersist() {
this.createdAt = LocalDateTime.now();
}

public Long getId() {
return id;
}

public Long getProductId() {
return productId;
}

public LocalDateTime getCreatedAt() {
return createdAt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.loopers.domain.product;

import java.util.List;

public interface ProductViewLogRepository {

List<ProductViewLog> saveAll(List<ProductViewLog> logs);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.loopers.domain.product.event;

import java.time.LocalDateTime;

public class ProductViewedEvent {

private Long productId;
private LocalDateTime occurredAt;

protected ProductViewedEvent() {
}

private ProductViewedEvent(Long productId) {
this.productId = productId;
this.occurredAt = LocalDateTime.now();
}

public static ProductViewedEvent of(Long productId) {
return new ProductViewedEvent(productId);
}

public Long getProductId() {
return productId;
}

public LocalDateTime getOccurredAt() {
return occurredAt;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

public class StockDepletedEvent {

private final Long productId;
private final LocalDateTime occurredAt;
private Long productId;
private LocalDateTime occurredAt;

protected StockDepletedEvent() {
}

private StockDepletedEvent(Long productId) {
this.productId = productId;
Expand Down
Loading