Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.EnableAsync;

import java.util.TimeZone;

@EnableFeignClients
@ConfigurationPropertiesScan
@SpringBootApplication
@EnableScheduling
@EnableAsync
public class CommerceApiApplication {
Comment on lines +17 to 18
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | πŸ”΄ Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for custom async executor configuration
rg -n --type=java -C3 'AsyncConfigurer|TaskExecutor|@Bean.*Executor' --max-count=50

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

Length of output: 6652


🏁 Script executed:

#!/bin/bash
# Search for @Async method usage in the codebase
rg -n --type=java '@Async' --max-count=50

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

Length of output: 1692


🏁 Script executed:

#!/bin/bash
# Search for configurations or config classes
fd -e java | xargs rg -l 'Configuration|Config' | head -20

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

Length of output: 1816


🏁 Script executed:

#!/bin/bash
# Check if executor bean is defined anywhere
rg -n --type=java 'getAsyncExecutor|setAsyncExecutor|@Bean.*executor' -i --max-count=50

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

Length of output: 70


🏁 Script executed:

#!/bin/bash
# Search for eventTaskExecutor bean definition
rg -n --type=java 'eventTaskExecutor' --max-count=50

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

Length of output: 985


🏁 Script executed:

#!/bin/bash
# Search for configuration classes that might define the executor
rg -n --type=java 'AsyncConfigurer|@EnableAsync|TaskExecutor' -B5 -A10 --max-count=100

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

Length of output: 12161


🏁 Script executed:

#!/bin/bash
# Look in config directories for AsyncConfiguration
fd -e java -path '*/config/*' | xargs rg -l 'AsyncConfigurer\|TaskExecutor' 2>/dev/null

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

Length of output: 258


🏁 Script executed:

#!/bin/bash
# Search the entire apps/commerce-api for eventTaskExecutor definition
find apps/commerce-api -name "*.java" -type f | xargs grep -l "eventTaskExecutor" | head -20

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

Length of output: 364


🏁 Script executed:

#!/bin/bash
# Search for @Bean definitions with executor
rg -n --type=java '@Bean' -A5 | grep -i 'executor\|task' | head -30

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

Length of output: 524


🏁 Script executed:

#!/bin/bash
# List Java files in commerce-api config directories
find apps/commerce-api -type d -name config -o -name config -type d 2>/dev/null | xargs find -name "*.java" 2>/dev/null

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

Length of output: 17299


🏁 Script executed:

#!/bin/bash
# Search for AsyncConfigurer implementation
rg -n --type=java 'implements AsyncConfigurer' -B3 -A20

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

Length of output: 70


🏁 Script executed:

#!/bin/bash
# Search entire repository for eventTaskExecutor definition
rg -n 'eventTaskExecutor' --type=java

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

Length of output: 985


🏁 Script executed:

#!/bin/bash
# Search for @Bean with getAsyncExecutor or custom executor methods
rg -n '@Bean' -A3 --type=java | grep -B3 -i 'executor\|async' | head -50

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

Length of output: 70


🏁 Script executed:

#!/bin/bash
# Check if there's a configuration class defining TaskExecutor bean
fd -name "*.java" -type f | xargs rg -l 'TaskExecutor|ThreadPoolTaskExecutor' | head -20

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

Length of output: 612


🏁 Script executed:

#!/bin/bash
# Search properties and yml files for executor configuration
find apps/commerce-api -name "*.yml" -o -name "*.yaml" -o -name "*.properties" | xargs grep -i 'executor\|async' 2>/dev/null

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

Length of output: 72


🏁 Script executed:

#!/bin/bash
# Broader search for any configuration with @Bean that might define eventTaskExecutor
rg -n 'eventTaskExecutor|@Bean.*Executor|ThreadPoolTaskExecutor' --type=java --max-count=20

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

Length of output: 985


🏁 Script executed:

#!/antml:bin/bash
# Check if there are any config files in the entire project for async
find . -name "*Async*Config*.java" -o -name "*Executor*Config*.java" 2>/dev/null | head -10

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

Length of output: 70


eventTaskExecutor 빈이 μ •μ˜λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. λŸ°νƒ€μž„ 였λ₯˜κ°€ λ°œμƒν•©λ‹ˆλ‹€.

@Async("eventTaskExecutor")λ₯Ό μ‚¬μš©ν•˜λŠ” 7개 λ©”μ„œλ“œ(ProductEventHandler, PaymentEventHandler, DataPlatformEventHandler)μ—μ„œ μ°Έμ‘°ν•˜λŠ” eventTaskExecutor 빈이 μ •μ˜λ˜μ–΄ μžˆμ§€ μ•ŠμŠ΅λ‹ˆλ‹€. 이둜 인해 λŸ°νƒ€μž„μ— NoSuchBeanDefinitionException이 λ°œμƒν•©λ‹ˆλ‹€.

λ‹€μŒ 쀑 ν•˜λ‚˜λ₯Ό μˆ˜ν–‰ν•˜μ„Έμš”:

  • eventTaskExecutor λΉˆμ„ μ •μ˜ν•˜λŠ” μ„€μ • 클래슀λ₯Ό μž‘μ„±ν•˜μ—¬ ThreadPoolTaskExecutor둜 ꡬ성
  • λ˜λŠ” @Async μ£Όμ„μ—μ„œ λͺ…μ‹œμ  executor 이름을 μ œκ±°ν•˜κ³  κΈ°λ³Έ executor μ„€μ • μΆ”κ°€
πŸ€– Prompt for AI Agents
In apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java
around lines 17-18, several methods use @Async("eventTaskExecutor") but no bean
named eventTaskExecutor is defined, causing NoSuchBeanDefinitionException at
runtime; fix by either (A) adding a configuration class that defines a
ThreadPoolTaskExecutor bean named "eventTaskExecutor" (configure corePoolSize,
maxPoolSize, queueCapacity, setThreadNamePrefix, call initialize(), and return
the executor) or (B) remove the explicit executor name from @Async annotations
and add a default async executor bean (e.g., a ThreadPoolTaskExecutor bean named
"taskExecutor") or enable the default by providing a @Bean of type
AsyncConfigurer/TaskExecutor so @Async uses it.


@PostConstruct
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;

import static org.springframework.transaction.event.TransactionPhase.AFTER_COMMIT;
Expand All @@ -20,6 +22,7 @@ public class LikeEventHandler {

@Async
@TransactionalEventListener(phase = AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleLikeCreated(LikeCreatedEvent event) {
try {
productRepository.findByIdForUpdate(event.productId())
Expand All @@ -38,6 +41,7 @@ public void handleLikeCreated(LikeCreatedEvent event) {

@Async
@TransactionalEventListener(phase = AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleLikeDeleted(LikeDeletedEvent event) {
try {
productRepository.findByIdForUpdate(event.productId())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;

Expand All @@ -25,7 +26,7 @@ public class PaymentEventHandler {
*/
@TransactionalEventListener(phase = AFTER_COMMIT)
@Async("eventTaskExecutor")
@Transactional
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handlePaymentSuccess(PaymentSuccessEvent event) {
log.info("결제 성곡 이벀트 처리 μ‹œμž‘ - orderId: {}", event.orderId());
try {
Expand All @@ -50,7 +51,7 @@ public void handlePaymentSuccess(PaymentSuccessEvent event) {
*/
@TransactionalEventListener(phase = AFTER_COMMIT)
@Async("eventTaskExecutor")
@Transactional
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handlePaymentFailed(PaymentFailedEvent event) {
log.info("결제 μ‹€νŒ¨ 이벀트 처리 μ‹œμž‘ - orderId: {}, reason: {}", event.orderId(), event.failureReason());
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,35 @@

import com.loopers.domain.product.Product;
import com.loopers.domain.product.ProductRepository;
import com.loopers.infrastructure.rank.MonthlyRankJpaRepository;
import com.loopers.infrastructure.rank.WeeklyRankJpaRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.ToIntFunction;

@Component
@RequiredArgsConstructor
public class RankingFacade {

private final RankingService rankingService;
private final ProductRepository productRepository;
private final WeeklyRankJpaRepository weeklyRankJpaRepository;
private final MonthlyRankJpaRepository monthlyRankJpaRepository;
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");

@Transactional(readOnly = true)
public List<RankingProductInfo> getDailyRanking(String yyyymmdd, int page, int size) {
Expand Down Expand Up @@ -48,27 +59,101 @@ public List<RankingProductInfo> getDailyRanking(String yyyymmdd, int page, int s
productMap.put(pdt.getId(), pdt);
}

int baseRank = (int) start + 1;
AtomicInteger rankCounter = new AtomicInteger(baseRank);
for (ZSetOperations.TypedTuple<String> t : tuples) {
String member = t.getValue();
if (member == null || member.isBlank()) continue;
Long productId = Long.valueOf(member);
Product product = productMap.get(productId);
if (product == null) continue;
result.add(toInfo(productId, product));
int rank = rankCounter.getAndIncrement();
Double score = t.getScore();
result.add(toInfo(productId, product, rank, score));
}
return result;
}

private RankingProductInfo toInfo(Long productId, Product product) {
@Transactional(readOnly = true)
public List<RankingProductInfo> getWeeklyRanking(String weekStartYyyymmdd, int page, int size) {
int p = Math.max(1, page);
int s = Math.max(1, size);
LocalDate periodStart = LocalDate.parse(weekStartYyyymmdd, DATE_FORMATTER);

var rows = weeklyRankJpaRepository.findByPeriodStartOrderByRankPositionAsc(
periodStart, PageRequest.of(p - 1, s)
);
return buildRanking(
rows,
r -> r.getProductId(),
r -> r.getRankPosition() != null ? r.getRankPosition() : 0,
r -> r.getTotalScore()
);
}
Comment on lines +77 to +92
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(weekStartYyyymmdd, DATE_FORMATTER)λŠ” 잘λͺ»λœ ν˜•μ‹μ˜ μž…λ ₯에 λŒ€ν•΄ DateTimeParseException을 λ°œμƒμ‹œν‚΅λ‹ˆλ‹€. API λ ˆμ΄μ–΄μ—μ„œ μ μ ˆν•œ μ˜ˆμ™Έ 처리 λ˜λŠ” μž…λ ₯ 검증이 ν•„μš”ν•©λ‹ˆλ‹€.

πŸ”Ž μ˜ˆμ™Έ 처리 μΆ”κ°€ μ œμ•ˆ

Facade λ ˆμ΄μ–΄μ—μ„œ μ²˜λ¦¬ν•˜κ±°λ‚˜, Controller λ ˆμ΄μ–΄μ—μ„œ @DateTimeFormat 등을 ν™œμš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€:

 public List<RankingProductInfo> getWeeklyRanking(String weekStartYyyymmdd, int page, int size) {
     int p = Math.max(1, page);
     int s = Math.max(1, size);
-    LocalDate periodStart = LocalDate.parse(weekStartYyyymmdd, DATE_FORMATTER);
+    LocalDate periodStart;
+    try {
+        periodStart = LocalDate.parse(weekStartYyyymmdd, DATE_FORMATTER);
+    } catch (DateTimeParseException e) {
+        throw new IllegalArgumentException("Invalid date format. Expected yyyyMMdd", e);
+    }
     ...
 }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Transactional(readOnly = true)
public List<RankingProductInfo> getWeeklyRanking(String weekStartYyyymmdd, int page, int size) {
int p = Math.max(1, page);
int s = Math.max(1, size);
LocalDate periodStart = LocalDate.parse(weekStartYyyymmdd, DATE_FORMATTER);
var rows = weeklyRankJpaRepository.findByPeriodStartOrderByRankPositionAsc(
periodStart, PageRequest.of(p - 1, s)
);
return buildRanking(
rows,
r -> r.getProductId(),
r -> r.getRankPosition() != null ? r.getRankPosition() : 0,
r -> r.getTotalScore()
);
}
@Transactional(readOnly = true)
public List<RankingProductInfo> getWeeklyRanking(String weekStartYyyymmdd, int page, int size) {
int p = Math.max(1, page);
int s = Math.max(1, size);
LocalDate periodStart;
try {
periodStart = LocalDate.parse(weekStartYyyymmdd, DATE_FORMATTER);
} catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid date format. Expected yyyyMMdd", e);
}
var rows = weeklyRankJpaRepository.findByPeriodStartOrderByRankPositionAsc(
periodStart, PageRequest.of(p - 1, s)
);
return buildRanking(
rows,
r -> r.getProductId(),
r -> r.getRankPosition() != null ? r.getRankPosition() : 0,
r -> r.getTotalScore()
);
}
πŸ€– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/ranking/RankingFacade.java
around lines 77-92, LocalDate.parse(...) can throw DateTimeParseException for
invalid input; wrap the parse call in a try-catch that catches
DateTimeParseException, validate the input format if desired, and rethrow a
clear runtime exception (e.g., IllegalArgumentException or a custom
BadRequest/InvalidRequestException) with a descriptive message that includes the
offending weekStartYyyymmdd value so the controller can map it to a 400
response.


@Transactional(readOnly = true)
public List<RankingProductInfo> getMonthlyRanking(String monthStartYyyymmdd, int page, int size) {
int p = Math.max(1, page);
int s = Math.max(1, size);
LocalDate periodStart = LocalDate.parse(monthStartYyyymmdd, DATE_FORMATTER);

var rows = monthlyRankJpaRepository.findByPeriodStartOrderByRankPositionAsc(
periodStart, PageRequest.of(p - 1, s)
);
return buildRanking(
rows,
r -> r.getProductId(),
r -> r.getRankPosition() != null ? r.getRankPosition() : 0,
r -> r.getTotalScore()
);
}

private RankingProductInfo toInfo(Long productId, Product product, int rank, Double score) {
return new RankingProductInfo(
rank,
score,
productId,
product.getName(),
product.getPrice() != null ? product.getPrice().getAmount() : BigDecimal.ZERO,
product.getStockQuantity(),
product.getLikeCount() != null ? product.getLikeCount() : 0L
);
}


private <T> List<RankingProductInfo> buildRanking(
List<T> rows,
Function<T, Long> productIdExtractor,
ToIntFunction<T> rankExtractor,
Function<T, Double> scoreExtractor
) {
if (rows == null || rows.isEmpty()) {
return List.of();
}
List<Long> productIds = rows.stream()
.map(productIdExtractor)
.toList();

List<Product> products = productRepository.findByIdIn(productIds);
Map<Long, Product> productMap = new HashMap<>();
for (Product prd : products) {
productMap.put(prd.getId(), prd);
}

List<RankingProductInfo> result = new ArrayList<>();
for (T row : rows) {
Long productId = productIdExtractor.apply(row);
Product product = productMap.get(productId);
if (product == null) {
continue;
}
int rank = rankExtractor.applyAsInt(row);
Double score = scoreExtractor.apply(row);
result.add(toInfo(productId, product, rank, score));
}
return result;
}


}


Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import java.math.BigDecimal;

public record RankingProductInfo(
int rank,
Double score,
Long productId,
String name,
BigDecimal price,
Integer stock,
Long likeCount
) {}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.loopers.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
public class AsyncConfig {

@Bean(name = "eventTaskExecutor")
public Executor eventTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(1000);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("event-async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(10);
executor.initialize();
return executor;
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.loopers.domain.rank;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Immutable;

@Entity
@Table(name = "mv_product_rank_monthly")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Immutable
public class MonthlyProductRankView {

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

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

@Column(name = "period_start", nullable = false)
private LocalDate periodStart;

@Column(name = "rank_position", nullable = false)
private Integer rankPosition;

@Column(name = "total_score", nullable = false)
private Double totalScore;

@Column(name = "like_count", nullable = false)
private Integer likeCount;

@Column(name = "view_count", nullable = false)
private Integer viewCount;

@Column(name = "order_count", nullable = false)
private Integer orderCount;

@Column(name = "sales_amount", nullable = false, precision = 15, scale = 2)
private BigDecimal salesAmount;

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

@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.loopers.domain.rank;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Immutable;

@Entity
@Table(name = "mv_product_rank_weekly")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Immutable
public class WeeklyProductRankView {

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

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

@Column(name = "period_start", nullable = false)
private LocalDate periodStart;

@Column(name = "rank_position", nullable = false)
private Integer rankPosition;

@Column(name = "total_score", nullable = false)
private Double totalScore;

@Column(name = "like_count", nullable = false)
private Integer likeCount;

@Column(name = "view_count", nullable = false)
private Integer viewCount;

@Column(name = "order_count", nullable = false)
private Integer orderCount;

@Column(name = "sales_amount", nullable = false, precision = 15, scale = 2)
private BigDecimal salesAmount;

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

@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.loopers.infrastructure.rank;

import com.loopers.domain.rank.MonthlyProductRankView;
import java.time.LocalDate;
import java.util.List;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface MonthlyRankJpaRepository extends JpaRepository<MonthlyProductRankView, Long> {
List<MonthlyProductRankView> findByPeriodStartOrderByRankPositionAsc(LocalDate periodStart, Pageable pageable);
}


Loading