-
Notifications
You must be signed in to change notification settings - Fork 34
[volume-10] Collect, Stack, Zip #229
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
base: kilian-develop
Are you sure you want to change the base?
[volume-10] Collect, Stack, Zip #229
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์ํ ๋ฉํธ๋ฆญ ์ถ์ ์ ์ผ์ผ/์ฃผ๊ฐ/์๊ฐ ๋จ์๋ก ๋ถ๋ฆฌํ๊ณ , ๋ฐฐ์น ์ฒ๋ฆฌ๋ก ์ฃผ๊ฐ/์๊ฐ ์ง๊ณ ๋ฐ์ดํฐ๋ฅผ ์์ฑํ๋ฉฐ, ๋ญํน ์กฐํ API์ ํ์ ํ๋ผ๋ฏธํฐ๋ฅผ ์ถ๊ฐํ์ฌ ์ ๋ต ๊ธฐ๋ฐ ๋ญํน ์ ํ์ ์ง์ํฉ๋๋ค. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant API as ProductV1Api
participant Service as GetProductRankingService
participant Selector as GetProductRankingsStrategySelector
participant Strategy as GetProductRankingsStrategy
participant Repository as Metric Repository
participant Cache as Cache Repository
User->>API: getProductRankings(date, "WEEKLY")
API->>Service: getRanking(GetProductRankingQuery)
Service->>Selector: select("WEEKLY")
Selector->>Strategy: [filter by support]
Strategy-->>Selector: GetWeeklyProductRankingsStrategy
Selector-->>Service: WeeklyStrategy
Service->>Strategy: getRankings(date, page, size)
Strategy->>Repository: findRankingsBy(date, weights, pageable)
Repository->>Repository: Query aggregated metrics
Repository-->>Strategy: ProductRankings
Strategy-->>Service: ProductRankings
Service-->>API: ApiResponse
API-->>User: Rankings JSON
sequenceDiagram
actor Scheduler
participant MonthlyScheduler as MonthlyProductMetricScheduler
participant JobLauncher
participant Job as monthlyProductMetricJob
participant Partitioner as MonthlyProductMetricBatchPartitioner
participant Reader as MonthlyProductMetricBatchReader
participant Writer as MonthlyProductMetricBatchWriter
participant DailyRepo as DailyProductMetricRepository
participant MonthlyRepo as MonthlyProductMetricRepository
rect rgb(220, 240, 255)
Note over MonthlyScheduler: ๋งค์ 1์ผ ์ค์ 2์
Scheduler->>MonthlyScheduler: ์ค์ผ์ค ์คํ
end
MonthlyScheduler->>MonthlyScheduler: ์ง๋๋ฌ startDate, endDate ๊ณ์ฐ
MonthlyScheduler->>JobLauncher: JobParameters ์ ๋ฌ (startDate, endDate)
JobLauncher->>Job: ์๊ฐ ๋ฐฐ์น ์์
์์
rect rgb(240, 255, 240)
Note over Job: ํํฐ์
๋ ๋จ๊ณ
Job->>Partitioner: partition(gridSize)
Partitioner->>DailyRepo: countDistinctProductIdsBy(date range)
DailyRepo-->>Partitioner: ์ด ์ํ ์
Partitioner-->>Job: ํํฐ์
๋งต (partition0...partitionN)
end
par Partition Processing
rect rgb(255, 240, 220)
Note over Reader: ๊ฐ ํํฐ์
๋ณ๋ ฌ ์ฒ๋ฆฌ
Job->>Reader: open(ExecutionContext)
Reader->>DailyRepo: findAggregatedBy(date, offset, limit)
DailyRepo-->>Reader: ProductMetricAggregation ๋ฆฌ์คํธ
end
loop ๊ฐ ์ง๊ณ ํญ๋ชฉ
Job->>Reader: read()
Reader-->>Job: ProductMetricAggregation
Job->>Writer: write(chunk)
Writer->>Writer: to(YearMonth) ๋ณํ
Writer->>MonthlyRepo: bulkUpsert(MonthlyProductMetric)
end
end
Job-->>JobLauncher: ์์
์๋ฃ
Estimated code review effort๐ฏ 4 (Complex) | โฑ๏ธ ~60 minutes Possibly related PRs
Suggested labels
Pre-merge checks and finishing touchesโ Failed checks (1 warning, 1 inconclusive)
โ Passed checks (1 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: 18
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and canโt be posted inline due to platform limitations.
โ ๏ธ Outside diff range comments (1)
core/infra/database/mysql/src/main/java/com/loopers/core/infra/database/mysql/product/entity/DailyProductMetricEntity.java (1)
62-72:to()๋ฉ์๋์์id๊ฐ null์ผ ๊ฒฝ์ฐ NPE ๋ฐ์ ๊ฐ๋ฅ์ฑ.
from()๋ฉ์๋์์๋Optional.ofNullable()๋ก null ID๋ฅผ ์ฒ๋ฆฌํ์ง๋ง,to()๋ฉ์๋์this.id.toString()(Line 64)์ ์ ์ฅ๋์ง ์์ ์ํฐํฐ์ ๊ฒฝ์ฐNullPointerException์ ๋ฐ์์ํฌ ์ ์์ต๋๋ค.๐ ์ ์๋ ์์ ์ฌํญ
public DailyProductMetric to() { return DailyProductMetric.mappedBy( - new DailyProductMetricId(this.id.toString()), + new DailyProductMetricId(Optional.ofNullable(this.id).map(Object::toString).orElse(null)), new ProductId(this.productId.toString()), new ProductLikeCount(this.likeCount), new ProductTotalSalesCount(this.totalSalesCount), new ProductDetailViewCount(this.viewCount), new CreatedAt(this.createdAt), new UpdatedAt(this.updatedAt) ); }
๐ก Minor comments (3)
core/infra/database/mysql/src/main/java/com/loopers/core/infra/database/mysql/product/MonthlyProductMetricBulkRepository.java-9-9 (1)
9-9: ์ธํฐํ์ด์ค ์์ค์์ null/empty ์ฒ๋ฆฌ ๊ณ์ฝ์ ๋ช ์์ ์ผ๋ก ๋ฌธ์ํํ์ธ์๊ตฌํ์ฒด์์ null ๋๋ ๋น ๋ฆฌ์คํธ์ ๋ํด ์ ์ ํ ์ฒ๋ฆฌํ๊ณ ์์ผ๋(๋ผ์ธ 22-24), ์ธํฐํ์ด์ค ๋ฉ์๋์
@NonNull์ด๋ ธํ ์ด์ ์ด๋ JavaDoc์ด ์์ด ํธ์ถ์๊ฐ ์ด ๋์์ ์ธ์ํ๊ธฐ ์ด๋ ต์ต๋๋ค. ๋ฉ์๋ ํ๋ผ๋ฏธํฐ์@NonNull์ ์ถ๊ฐํ๊ฑฐ๋ JavaDoc์ผ๋ก null/empty ๋ฆฌ์คํธ ์ ๋์์ ๋ช ์ํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.core/service/src/main/java/com/loopers/core/service/product/IncreaseProductTotalSalesService.java-40-43 (1)
40-43: ๋ฃจํ ์ธ๋ถ์์ ํ์์คํฌํ๋ฅผ ์ถ์ถํ์ธ์.
forEach๋ฃจํ ๋ด์์ ๋งค iteration๋ง๋คnew CreatedAt(LocalDateTime.now())๋ฅผ ํธ์ถํ๋ฉด, ๋์ผํ ์ฃผ๋ฌธ์ ์ฌ๋ฌ ์ํ๋ค์ด ์๋ก ๋ค๋ฅธ ์ผ์์ DailyProductMetric์ ์ง๊ณ๋ ์ ์์ต๋๋ค. ํ์์คํฌํ๋ฅผ ๋ฃจํ ์ ์ ํ ๋ฒ ์์ฑํ๊ณ ์ฌ์ฌ์ฉํ์ธ์:LocalDateTime now = LocalDateTime.now(); orderItemRepository.findAllByOrderId(order.getId()).forEach(orderItem -> { DailyProductMetric metric = dailyProductMetricRepository.findByWithLock(orderItem.getProductId(), new CreatedAt(now)) .orElse(DailyProductMetric.init(orderItem.getProductId())); dailyProductMetricRepository.save(metric.increaseSalesCount(orderItem.getQuantity())); });๋๋ ์ด๋ฏธ ์กด์ฌํ๋
CreatedAt.now()ํฌํผ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค.core/domain/src/main/java/com/loopers/core/domain/product/DailyProductMetric.java-62-66 (1)
62-66:increaseLikeCount()์์updatedAt๊ฐฑ์ ๋๋ฝ
increaseViewCount()์increaseSalesCount()๋updatedAt(UpdatedAt.now())๋ฅผ ํธ์ถํ์ฌ ์์ ์๊ฐ์ ๊ฐฑ์ ํ์ง๋ง,increaseLikeCount()๋ ์ด๋ฅผ ์ํํ์ง ์์ต๋๋ค. ์ผ๊ด์ฑ์ ์ํด ๋์ผํ๊ฒ ๊ฐฑ์ ํด์ผ ํฉ๋๋ค.๐ ์ ์๋ ์์
public DailyProductMetric increaseLikeCount() { return this.toBuilder() .likeCount(this.likeCount.increase()) + .updatedAt(UpdatedAt.now()) .build(); }
๐งน Nitpick comments (32)
settings.gradle.kts (1)
43-43: ๋ชจ๋ ์ ์ธ์ ๊ธฐ์กด include ๋ธ๋ก์ ํตํฉํ์ธ์.์๋ก์ด
commerce-batch๋ชจ๋์ด ๋ณ๋์include()๋ฌธ์ผ๋ก ์ถ๊ฐ๋์์ต๋๋ค. ์ผ๊ด์ฑ๊ณผ ์ ์ง๋ณด์์ฑ์ ์ํด 3-20๋ฒ์งธ ์ค์ ๊ธฐ์กดinclude()๋ธ๋ก์ ํจ๊ป ์ ์ธํ๋ ๊ฒ์ด ์ข์ต๋๋ค.๐ ๊ถ์ฅํ๋ ์์ ์
include( ":apps:commerce-api", ":apps:commerce-streamer", + ":apps:commerce-batch", ":supports:jackson", ":supports:logging", ":supports:monitoring", ":core:domain", ":core:service", ":core:infra:database:mysql:mysql-config", ":core:infra:database:mysql", ":core:infra:database:redis:redis-config", ":core:infra:database:redis", ":core:infra:http-client:http-client-config", ":core:infra:http-client", ":core:infra:event", ":core:infra:event:kafka-config", ":core:common" ) // configurations pluginManagement { val springBootVersion: String by settings val springDependencyManagementVersion: String by settings repositories { maven { url = uri("https://repo.spring.io/milestone") } maven { url = uri("https://repo.spring.io/snapshot") } gradlePluginPortal() } resolutionStrategy { eachPlugin { when (requested.id.id) { "org.springframework.boot" -> useVersion(springBootVersion) "io.spring.dependency-management" -> useVersion(springDependencyManagementVersion) } } } } - -include("apps:commerce-batch")apps/commerce-batch/src/main/resources/application.yml (2)
64-86: ํ๊ฒฝ๋ณ ์ค์ ์ถ๊ฐ๋ฅผ ๊ถ์ฅํฉ๋๋ค.ํ์ฌ ํ๋กํ ์น์ ๋ค์ด ๋๋ถ๋ถ ๋น์ด์์ต๋๋ค. ๊ฐ ํ๊ฒฝ(dev, qa, prd)์ ๋ง๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค URL, ์ธ๋ถ ์๋น์ค ์๋ํฌ์ธํธ, ๋ฐฐ์น ํํฐ์ ํฌ๊ธฐ ๋ฑ ํ๊ฒฝ๋ณ ์ค์ ์ ์ถ๊ฐํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.
์๋ฅผ ๋ค์ด:
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ์ ๋ณด
- ์ธ๋ถ API ์๋ํฌ์ธํธ (์์์ ์ธ๊ธํ PG ์๋ฎฌ๋ ์ดํฐ, ์ฝ๋ฐฑ URL)
- ๋ฐฐ์น ์ฒญํฌ ํฌ๊ธฐ ๋ฐ ํํฐ์ ๊ทธ๋ฆฌ๋ ํฌ๊ธฐ (ํ๊ฒฝ ๋ฆฌ์์ค์ ๋ฐ๋ผ ์กฐ์ )
- ๋ก๊น ๋ ๋ฒจ ์กฐ์
88-90: SpringDoc ์ค์ ์์น๋ฅผ ๋ช ํํ ํ์ธ์.
springdoc์ค์ ์ด ํ๋ก๋์ ํ๋กํ ์น์ ๋ค์ ์์นํ์ฌ ํผ๋์ ์ค ์ ์์ต๋๋ค. ํ์ฌ ๋ค์ฌ์ฐ๊ธฐ ๊ธฐ์ค์ผ๋ก๋ ์ ์ญ ์ค์ ์ด์ง๋ง, ์์น์ prd ํ๋กํ๊ณผ ์ฐ๊ด๋ ๊ฒ์ฒ๋ผ ๋ณด์ ๋๋ค.API ๋ฌธ์๋ฅผ ๋ชจ๋ ํ๊ฒฝ์์ ๋นํ์ฑํํ๋ ค๋ฉด ์ ์ญ ์ค์ ์์ญ(line 13-35 ๋ถ๊ทผ)์ผ๋ก ์ด๋ํ๊ฑฐ๋, ํน์ ํ๋กํ์์๋ง ๋นํ์ฑํํ๋ ค๋ฉด ํด๋น ํ๋กํ ์น์ ๋ด๋ถ๋ก ์ด๋ํ์ธ์.
๐ ์ ์ญ ์ค์ ์ผ๋ก ์ด๋ ์ ์
batch: jdbc: initialize-schema: always job: enabled: false + +springdoc: + api-docs: + enabled: false + http-client: pg-simulator:๊ทธ๋ฆฌ๊ณ ํ์ฌ ์์น์์ ์ ๊ฑฐ:
spring: config: activate: on-profile: prd - -springdoc: - api-docs: - enabled: falsecore/domain/src/testFixtures/java/com/loopers/core/domain/product/DailyProductMetricFixture.java (1)
14-66: ์ฝ๋ ์ค๋ณต ์ ๊ฑฐ๋ฅผ ํตํ ์ ์ง๋ณด์์ฑ ๊ฐ์ ๊ถ์ฅ๋ค ๊ฐ์ ํฉํ ๋ฆฌ ๋ฉ์๋๊ฐ ์ ์ฌํ Instancio ์ค์ ์ ๋ฐ๋ณตํ๊ณ ์์ต๋๋ค. ๊ณตํต ๋ก์ง์ private ํฌํผ ๋ฉ์๋๋ก ์ถ์ถํ๋ฉด ์ ์ง๋ณด์์ฑ์ด ํฅ์๋๊ณ ํฅํ ํ๋ ๋ณ๊ฒฝ ์ ์์ ๋ฒ์๋ฅผ ์ค์ผ ์ ์์ต๋๋ค.
๐ ๋ฆฌํฉํ ๋ง ์ ์
public class DailyProductMetricFixture { + + private static DailyProductMetric buildBase( + String productId, + long likeCount, + long viewCount, + long totalSalesCount, + LocalDateTime createdAt + ) { + return Instancio.of(DailyProductMetric.class) + .set(field(DailyProductMetric::getId), DailyProductMetricId.empty()) + .set(field(DailyProductMetric::getProductId), new ProductId(productId)) + .set(field(DailyProductMetric::getLikeCount), new ProductLikeCount(likeCount)) + .set(field(DailyProductMetric::getTotalSalesCount), new ProductTotalSalesCount(totalSalesCount)) + .set(field(DailyProductMetric::getViewCount), new ProductDetailViewCount(viewCount)) + .set(field(DailyProductMetric::getCreatedAt), new CreatedAt(createdAt)) + .set(field(DailyProductMetric::getUpdatedAt), new UpdatedAt(createdAt)) + .create(); + } public static DailyProductMetric create() { - return Instancio.of(DailyProductMetric.class) - .set(field(DailyProductMetric::getId), DailyProductMetricId.empty()) - .set(field(DailyProductMetric::getProductId), new ProductId("1")) - .set(field(DailyProductMetric::getLikeCount), new ProductLikeCount(0L)) - .set(field(DailyProductMetric::getTotalSalesCount), new ProductTotalSalesCount(0L)) - .set(field(DailyProductMetric::getViewCount), new ProductDetailViewCount(0L)) - .set(field(DailyProductMetric::getCreatedAt), new CreatedAt(LocalDateTime.now())) - .set(field(DailyProductMetric::getUpdatedAt), new UpdatedAt(LocalDateTime.now())) - .create(); + return buildBase("1", 0L, 0L, 0L, LocalDateTime.now()); } public static DailyProductMetric createWith(String productId) { - return Instancio.of(DailyProductMetric.class) - .set(field(DailyProductMetric::getId), DailyProductMetricId.empty()) - .set(field(DailyProductMetric::getProductId), new ProductId(productId)) - .set(field(DailyProductMetric::getLikeCount), new ProductLikeCount(0L)) - .set(field(DailyProductMetric::getTotalSalesCount), new ProductTotalSalesCount(0L)) - .set(field(DailyProductMetric::getViewCount), new ProductDetailViewCount(0L)) - .set(field(DailyProductMetric::getCreatedAt), new CreatedAt(LocalDateTime.now())) - .set(field(DailyProductMetric::getUpdatedAt), new UpdatedAt(LocalDateTime.now())) - .create(); + return buildBase(productId, 0L, 0L, 0L, LocalDateTime.now()); } public static DailyProductMetric createWith(String productId, LocalDateTime createdAt) { - return Instancio.of(DailyProductMetric.class) - .set(field(DailyProductMetric::getId), DailyProductMetricId.empty()) - .set(field(DailyProductMetric::getProductId), new ProductId(productId)) - .set(field(DailyProductMetric::getLikeCount), new ProductLikeCount(0L)) - .set(field(DailyProductMetric::getTotalSalesCount), new ProductTotalSalesCount(0L)) - .set(field(DailyProductMetric::getViewCount), new ProductDetailViewCount(0L)) - .set(field(DailyProductMetric::getCreatedAt), new CreatedAt(createdAt)) - .set(field(DailyProductMetric::getUpdatedAt), new UpdatedAt(createdAt)) - .create(); + return buildBase(productId, 0L, 0L, 0L, createdAt); } public static DailyProductMetric createWith( String productId, long likeCount, long viewCount, long totalSalesCount, LocalDateTime createdAt ) { - return Instancio.of(DailyProductMetric.class) - .set(field(DailyProductMetric::getId), DailyProductMetricId.empty()) - .set(field(DailyProductMetric::getProductId), new ProductId(productId)) - .set(field(DailyProductMetric::getLikeCount), new ProductLikeCount(likeCount)) - .set(field(DailyProductMetric::getTotalSalesCount), new ProductTotalSalesCount(totalSalesCount)) - .set(field(DailyProductMetric::getViewCount), new ProductDetailViewCount(viewCount)) - .set(field(DailyProductMetric::getCreatedAt), new CreatedAt(createdAt)) - .set(field(DailyProductMetric::getUpdatedAt), new UpdatedAt(createdAt)) - .create(); + return buildBase(productId, likeCount, viewCount, totalSalesCount, createdAt); } }core/service/src/main/java/com/loopers/core/service/product/command/IncreaseProductLikeMetricCommand.java (1)
3-7: eventId์ productId์ ๋ํ null ๊ฒ์ฆ ๊ณ ๋ ค๋ ์ฝ๋๊ฐ null ๊ฐ์ ํ์ฉํ๋ฉด ์๋น์ค ๋ ์ด์ด์์ NullPointerException์ด ๋ฐ์ํ ์ ์์ต๋๋ค. eventId๋ ๋ฉฑ๋ฑ์ฑ ๋ณด์ฅ์ ์ฌ์ฉ๋๊ณ productId๋ ๋ฉํธ๋ฆญ ์๋ณ์ ํ์์ ์ด๋ฏ๋ก, compact ์์ฑ์์์ null ๊ฒ์ฆ์ ์ถ๊ฐํ๋ ๊ฒ์ด ์์ ํฉ๋๋ค.
๐ null ๊ฒ์ฆ ์ถ๊ฐ ์ ์
public record IncreaseProductLikeMetricCommand( String eventId, String productId ) { + public IncreaseProductLikeMetricCommand { + if (eventId == null || eventId.isBlank()) { + throw new IllegalArgumentException("eventId must not be null or blank"); + } + if (productId == null || productId.isBlank()) { + throw new IllegalArgumentException("productId must not be null or blank"); + } + } }apps/commerce-batch/src/test/java/com/loopers/application/batch/IntegrationTest.java (1)
10-12: ๋ฒ ์ด์ค ํด๋์ค๋ก ์ฌ์ฉ๋ ๊ฒฝ์ฐ abstract ์ ์ธ ๊ณ ๋ คIntegrationTest๊ฐ ๋ค๋ฅธ ํตํฉ ํ ์คํธ ํด๋์ค๋ค(์: MonthlyProductMetricSchedulerTest, WeeklyProductMetricSchedulerTest)์ ๋ฒ ์ด์ค ํด๋์ค๋ก ์ฌ์ฉ๋ ๊ฒฝ์ฐ, abstract๋ก ์ ์ธํ์ฌ ์ง์ ์ธ์คํด์คํ๋๋ ๊ฒ์ ๋ฐฉ์งํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
๐ abstract ํด๋์ค๋ก ๋ณ๊ฒฝ ์ ์
@SpringBootTest @ExtendWith(MySqlTestContainersExtension.class) -public class IntegrationTest { +public abstract class IntegrationTest {apps/commerce-streamer/src/main/java/com/loopers/applications/streamer/consumer/product/dto/IncreaseProductLikeMetricEvent.java (1)
5-8: ์ปค๋งจ๋์ ๋์ผํ null ๊ฒ์ฆ ๊ณ ๋ คIncreaseProductLikeMetricCommand์ ๊ฒ์ฆ์ ์ถ๊ฐํ๋ ๊ฒฝ์ฐ, ์ด๋ฒคํธ ๋ ์ฝ๋์์๋ ์ผ๊ด์ฑ์ ์ํด ๋์ผํ ๊ฒ์ฆ์ ์ํํ๋ ๊ฒ์ด ์ข์ต๋๋ค. Kafka์์ ์ญ์ง๋ ฌํ๋ ๋ฐ์ดํฐ์ ๋ฌด๊ฒฐ์ฑ์ ๋ณด์ฅํ ์ ์์ต๋๋ค.
core/infra/database/mysql/src/main/java/com/loopers/core/infra/database/mysql/product/WeeklyProductMetricBulkRepository.java (1)
7-10: ๋ฐํ ํ์ ๋ฐ ๋ฌธ์ํ ๊ฐ์ ๊ณ ๋ คํ์ฌ void ๋ฐํ ํ์ ์ upsert ์์ ์ ๊ฒฐ๊ณผ(์: ์ํฅ๋ฐ์ ํ ์, ์ฑ๊ณต/์คํจ ์ฌ๋ถ)๋ฅผ ํ์ธํ ์ ์์ต๋๋ค. ๋ค์ ๊ฐ์ ์ฌํญ์ ๊ณ ๋ คํด๋ณด์ธ์:
- ๋ฉ์๋ JavaDoc ์ถ๊ฐ (๋น ๋ฆฌ์คํธ ์ฒ๋ฆฌ, ํธ๋์ญ์ ๋์, ์์ธ ๋ฐ์ ์กฐ๊ฑด ๋ฑ)
- ํ์์ ์ํฅ๋ฐ์ ํ ์๋ฅผ ๋ฐํํ๋๋ก ๋ฐํ ํ์ ๋ณ๊ฒฝ (์:
int๋๋BulkUpsertResult)๐ ๋ฌธ์ํ ์์
public interface WeeklyProductMetricBulkRepository { + /** + * ์ฃผ๊ฐ ์ํ ๋ฉํธ๋ฆญ ๋ชฉ๋ก์ ์ผ๊ด ์ฝ์ ๋๋ ์ ๋ฐ์ดํธํฉ๋๋ค. + * + * @param weeklyProductMetrics upsertํ ์ฃผ๊ฐ ๋ฉํธ๋ฆญ ์ํฐํฐ ๋ชฉ๋ก (๋น ๋ฆฌ์คํธ์ ๊ฒฝ์ฐ ์๋ฌด ์์ ๋ ์ํํ์ง ์์) + * @throws org.springframework.dao.DataAccessException ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๋ฅ ๋ฐ์ ์ + */ void bulkUpsert(List<WeeklyProductMetricEntity> weeklyProductMetrics); }core/service/src/main/java/com/loopers/core/service/product/query/GetProductRankingQuery.java (1)
7-7: type ํ๋ผ๋ฏธํฐ์ ์ ํจ์ฑ ๊ฒ์ฆ ๋๋ ๋ฌธ์ํ๋ฅผ ๊ณ ๋ คํ์ธ์.type ํ๋ผ๋ฏธํฐ๋ ์ ๋ต ์ ํ์ ์ฌ์ฉ๋์ง๋ง ํ์ฌ ๊ฒ์ฆ์ด๋ ์ ์ฝ์ด ์์ต๋๋ค. ๋ค์ ์ค ํ๋๋ฅผ ๊ณ ๋ คํด๋ณด์ธ์:
- ์ ํจํ ํ์ ๊ฐ๋ค์ enum์ผ๋ก ์ ์ (ํ์ ์์ ์ฑ)
- ์์ฑ์์์ ์ ํจ์ฑ ๊ฒ์ฆ ๋ก์ง ์ถ๊ฐ
- ์ต์ํ Javadoc์ผ๋ก ์ ํจํ ๊ฐ๋ค์ ๋ช ์ (์: "daily", "weekly", "monthly")
ํ์ฌ๋ ์๋ชป๋ type ๊ฐ์ด ์ ๋ฌ๋์ด๋ ์ฟผ๋ฆฌ ์์ฑ ์์ ์ ๊ฐ์ง๋์ง ์์ต๋๋ค.
๐ Enum ๊ธฐ๋ฐ ๋ฆฌํฉํ ๋ง ์ ์
package com.loopers.core.service.product.query; import java.time.LocalDate; +public enum RankingType { + DAILY, WEEKLY, MONTHLY +} + public record GetProductRankingQuery( LocalDate date, - String type, + RankingType type, int pageNo, int pageSize ) { }core/service/src/main/java/com/loopers/core/service/product/component/GetProductRankingsStrategy.java (1)
7-12: LGTM: ์ ๋ต ํจํด์ด ๊น๋ํ๊ฒ ๊ตฌํ๋์์ต๋๋ค.์ธํฐํ์ด์ค ์ค๊ณ๊ฐ ๋ช ํํ๊ณ ๋จ์ผ ์ฑ ์ ์์น์ ์ ๋ฐ๋ฅด๊ณ ์์ต๋๋ค.
supports()์getRankings()์ ๋ถ๋ฆฌ๊ฐ ์ ๋ต ์ ํ๊ณผ ์คํ์ ๋ช ํํ ๊ตฌ๋ถํฉ๋๋ค.์ ํ์ ์ผ๋ก, ๋ค์์ ๋ฌธ์ํํ๋ฉด ๋ ์ข์ต๋๋ค:
supports()๋ฉ์๋๊ฐ ์ด๋ค type ๊ฐ๋ค์ ์ง์ํด์ผ ํ๋์งgetRankings()์ ๋ฐํ๊ฐ์ธ ProductRankings์ ๊ตฌ์กฐ- ์ฌ๋ฌ ์ ๋ต์ด ๋์ผํ type์ ์ง์ํ ๊ฒฝ์ฐ์ ์ฐ์ ์์
apps/commerce-batch/src/test/resources/application-test.yml (1)
53-61: ๋ฐฐ์น ์ค์ ์ด ์ ์ ํฉ๋๋ค.chunk ํฌ๊ธฐ 100๊ณผ partition grid-size 4๋ ํ ์คํธ ํ๊ฒฝ์ ์ ํฉํ ๊ฐ์ ๋๋ค.
์ ํ์ ์ผ๋ก, weekly์ monthly์ ์ค์ ์ด ๋์ผํ๋ฏ๋ก ๊ณตํต ์ค์ ์ผ๋ก ์ถ์ถํ์ฌ ์ค๋ณต์ ์ ๊ฑฐํ ์ ์์ต๋๋ค.
apps/commerce-batch/src/main/java/com/loopers/application/batch/product/MonthlyProductMetricScheduler.java (2)
27-29: ํ์์กด ์ฒ๋ฆฌ๋ฅผ ๋ช ์์ ์ผ๋ก ์ง์ ํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์.
YearMonth.now()๋ ์์คํ ๊ธฐ๋ณธ ํ์์กด์ ์ฌ์ฉํ๋ฏ๋ก, ์๋ฒ ํ๊ฒฝ์ ๋ฐ๋ผ ๋ค๋ฅธ ๊ฒฐ๊ณผ๋ฅผ ์ด๋ํ ์ ์์ต๋๋ค. ๋ฐฐ์น ์์ ์ ์ผ๊ด์ฑ์ ์ํด ๋ช ์์ ์ธ ํ์์กด(์: UTC)์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์ต๋๋ค.๐ ๋ช ์์ ํ์์กด ์ฌ์ฉ ์ ์
+import java.time.ZoneId; + @Scheduled(cron = "0 0 2 1 * ?") public void run() { - YearMonth lastMonth = YearMonth.now().minusMonths(1); + YearMonth lastMonth = YearMonth.now(ZoneId.of("Asia/Seoul")).minusMonths(1); LocalDate startDate = lastMonth.atDay(1); LocalDate endDate = lastMonth.atEndOfMonth();๋๋ UTC๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด:
+import java.time.ZoneOffset; + - YearMonth lastMonth = YearMonth.now().minusMonths(1); + YearMonth lastMonth = YearMonth.now(ZoneOffset.UTC).minusMonths(1);
25-25: ์ค์ผ์ค ์คํ ํ์์กด์ ๋ช ์ํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์.cron ํํ์์ ํ์์กด์ด ์ง์ ๋์ง ์์ ์๋ฒ์ ๊ธฐ๋ณธ ํ์์กด์ ์ฌ์ฉํฉ๋๋ค. ์ผ๊ด๋ ์คํ์ ๋ณด์ฅํ๊ธฐ ์ํด
zone์์ฑ์ ์ถ๊ฐํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.๐ ํ์์กด ์ง์ ์ ์
- @Scheduled(cron = "0 0 2 1 * ?") + @Scheduled(cron = "0 0 2 1 * ?", zone = "Asia/Seoul") public void run() {core/service/src/main/java/com/loopers/core/service/product/component/GetProductRankingsStrategySelector.java (1)
14-19: null ์์ ์ฑ๊ณผ ์์ธ ์ฒ๋ฆฌ๋ฅผ ๊ฐ์ ํ์ธ์.ํ์ฌ ๊ตฌํ์ ๋ค์ ๊ฐ์ ์ด ํ์ํฉ๋๋ค:
- Null ํ๋ผ๋ฏธํฐ ์ฒ๋ฆฌ:
type์ด null์ธ ๊ฒฝ์ฐsupports()ํธ์ถ ์ NPE๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.- ์์ธ ํ์ : ํด๋ผ์ด์ธํธ ์ค๋ฅ์ ๋ํด
IllegalArgumentException๋์ ๋๋ฉ์ธ๋ณ ์์ธ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์ต๋๋ค.- ๊ด์ฐฐ์ฑ: ์ ๋ต ์ ํ ์คํจ ์ ๋ก๊น ์ด ์์ด ๋๋ฒ๊น ์ด ์ด๋ ต์ต๋๋ค.
๐ ๊ฐ์ ์ฌํญ ์ ์
+import lombok.extern.slf4j.Slf4j; +import java.util.Objects; + +@Slf4j @Component @RequiredArgsConstructor public class GetProductRankingsStrategySelector { private final List<GetProductRankingsStrategy> strategies; public GetProductRankingsStrategy select(String type) { + Objects.requireNonNull(type, "๋ญํน ํ์ ์ null์ผ ์ ์์ต๋๋ค."); + return strategies.stream() .filter(strategy -> strategy.supports(type)) .findFirst() - .orElseThrow(() -> new IllegalArgumentException("์ง์ํ์ง ์๋ ํ์ ์ ๋ญํน์ ๋๋ค.")); + .orElseThrow(() -> { + log.error("Unsupported ranking type requested: {}", type); + return new IllegalArgumentException("์ง์ํ์ง ์๋ ํ์ ์ ๋ญํน์ ๋๋ค: " + type); + }); } }๋ ๋์ ๋ฐฉ๋ฒ์ผ๋ก๋, ํ๋ก์ ํธ์ ๋๋ฉ์ธ ์์ธ ํด๋์ค(์:
UnsupportedRankingTypeException)๋ฅผ ์ ์ํ์ฌ ์ฌ์ฉํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์.apps/commerce-api/src/main/java/com/loopers/application/api/product/ProductV1ApiSpec.java (1)
44-44:typeํ๋ผ๋ฏธํฐ์ ๋ํ Swagger ๋ฌธ์๋ฅผ ์ถ๊ฐํ๊ณ , API ์์ค์์์ ์ ํจ์ฑ ๊ฒ์ฆ์ ๊ณ ๋ คํ์ธ์.
typeํ๋ผ๋ฏธํฐ๋ ํ์ฉ๋๋ ๊ฐ("DAILY", "WEEKLY", "MONTHLY")์ ๋ํ Swagger ๋ฌธ์๊ฐ ์์ต๋๋ค. ๋ํ ์ปจํธ๋กค๋ฌ ๋ ๋ฒจ์์ ์ ํจ์ฑ ๊ฒ์ฆ ์ด๋ ธํ ์ด์ ์ด ์์ด, API ์ฌ์ฉ์๊ฐ ์ ํจํ ๊ฐ์ ์ฌ์ ์ ํ์ ํ๊ธฐ ์ด๋ ต์ต๋๋ค. ์๋น์ค ๋ ์ด์ด์์ ๋ฐํ์ ๊ฒ์ฆ์ด ์กด์ฌํ์ง๋ง, API ๋ฌธ์ ๊ฐ์ ๊ณผ ์ ๋ ฅ๊ฐ ๊ฒ์ฆ ๊ฐํ๋ API ์ฌ์ฉ์ฑ์ ํฌ๊ฒ ๊ฐ์ ํ ์ ์์ต๋๋ค.๐ ๊ฐ์ ๋ฐฉ์
@Operation( summary = "์ํ ๋ญํน ์กฐํ", - description = "์ํ ๋ญํน์ ์กฐํํฉ๋๋ค." + description = "์ํ ๋ญํน์ ์กฐํํฉ๋๋ค. type์ 'DAILY', 'WEEKLY', 'MONTHLY' ์ค ํ๋๋ฅผ ์ง์ ํ ์ ์์ต๋๋ค." ) ApiResponse<GetProductRankingsResponse> getProductRankings(LocalDate date, String type, int pageNo, int pageSize);์ค์ ๊ตฌํ์ฒด(
ProductV1Api)์์@RequestParam์@Pattern์ด๋ ธํ ์ด์ ์ ์ถ๊ฐํ๊ฑฐ๋ enum์ ์ฌ์ฉํ์ฌ ์ปดํ์ผ ํ์์ ๊ฒ์ฆํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์.core/infra/database/mysql/src/main/java/com/loopers/core/infra/database/mysql/product/MonthlyProductMetricBulkRepositoryImpl.java (1)
21-52: ๋ฒํฌ ์ ์ํธ ๊ตฌํ์ด ์ฌ๋ฐ๋ฆ ๋๋ค.ON DUPLICATE KEY UPDATE๋ฅผ ์ฌ์ฉํ ๋ฒํฌ ์ ์ํธ ๋ก์ง์ด ์ ํํ๋ฉฐ,
createdAt์ ๋ณด์กดํ๊ณ ๋ฉํธ๋ฆญ ํ๋๋ค๊ณผupdatedAt๋ง ๊ฐฑ์ ํ๋ ๊ฒ์ด ์ ์ ํฉ๋๋ค.๋ค๋ง
WeeklyProductMetricBulkRepositoryImpl๊ณผ ๊ฑฐ์ ๋์ผํ ๊ตฌ์กฐ๋ก ์ฝ๋ ์ค๋ณต์ด ๋ฐ์ํ๊ณ ์์ต๋๋ค. ๋ค์๊ณผ ๊ฐ์ด ๊ณตํต ์ถ์ ํด๋์ค๋ ํ ํ๋ฆฟ ๋ฉ์๋ ํจํด์ ๊ณ ๋ คํด๋ณด์ธ์:๐ ์ ์: ์ถ์ ํด๋์ค๋ฅผ ํตํ ์ค๋ณต ์ ๊ฑฐ
๊ณตํต ๋ก์ง์ ์ถ์ํํ๋ ๋ฒ ์ด์ค ํด๋์ค๋ฅผ ๋ง๋ค ์ ์์ต๋๋ค:
public abstract class AbstractProductMetricBulkRepository<T> { protected final NamedParameterJdbcTemplate jdbcTemplate; protected AbstractProductMetricBulkRepository(NamedParameterJdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } protected void executeBulkUpsert(List<T> metrics, String sql, Function<T, SqlParameterSource> mapper) { if (Objects.isNull(metrics) || metrics.isEmpty()) { return; } SqlParameterSource[] batch = metrics.stream() .map(mapper) .toArray(SqlParameterSource[]::new); jdbcTemplate.batchUpdate(sql, batch); } protected abstract String getUpsertSql(); protected abstract SqlParameterSource toParameterSource(T metric); }๊ทธ๋ฐ ๋ค์ ๊ฐ ๊ตฌํ์ฒด์์ SQL๊ณผ ๋งคํ ๋ก์ง๋ง ์ ๊ณตํ๋ฉด ๋ฉ๋๋ค.
core/infra/database/mysql/src/main/java/com/loopers/core/infra/database/mysql/product/WeeklyProductMetricBulkRepositoryImpl.java (1)
21-53: ๋ฒํฌ ์ ์ํธ ๋ก์ง์ด ์ฌ๋ฐ๋ฅด๊ฒ ๊ตฌํ๋์์ต๋๋ค.ON DUPLICATE KEY UPDATE๋ฅผ ์ฌ์ฉํ์ฌ ๋ฉํธ๋ฆญ์ ํจ์จ์ ์ผ๋ก ์ ์ํธํ๊ณ ์์ผ๋ฉฐ,
createdAt๋ณด์กด๊ณผ ๋ฉํธ๋ฆญ ํ๋ ๊ฐฑ์ ๋ก์ง์ด ์ ์ ํฉ๋๋ค.
MonthlyProductMetricBulkRepositoryImpl๊ณผ ๋์ผํ ๊ตฌ์กฐ์ ์ฝ๋ ์ค๋ณต์ด ๋ฐ์ํ๊ณ ์์ต๋๋ค. ์๊ฐ ๋ฉํธ๋ฆญ ๋ฆฌ๋ทฐ ์ฝ๋ฉํธ์์ ์ ์ํ ๊ฒ์ฒ๋ผ, ๊ณตํต ์ถ์ ํด๋์ค๋ฅผ ํตํด ์ค๋ณต์ ์ ๊ฑฐํ๋ ๊ฒ์ ๊ณ ๋ คํ์ธ์.apps/commerce-batch/src/main/java/com/loopers/application/batch/product/WeeklyProductMetricBatchReader.java (1)
22-30: ExecutionContext ํ๋ผ๋ฏธํฐ ๊ฒ์ฆ์ ๊ณ ๋ คํ์ธ์.
startDate,endDate,partitionOffset,partitionLimitํ๋ผ๋ฏธํฐ๋ฅผ ExecutionContext์์ ์ฝ์ ๋ null ์ฒดํฌ๋ ์์ธ ์ฒ๋ฆฌ๊ฐ ์์ต๋๋ค. ๋ฐฐ์น ์ค์ ์ค๋ฅ ์ ๋ช ํํ์ง ์์ ์์ธ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ํ์ ํ๋ผ๋ฏธํฐ ๊ฒ์ฆ์ ์ถ๊ฐํ๋ฉด ๋ฐฐ์น ์คํจ ์ ์์ธ ํ์ ์ด ๋ ์ฌ์์ง๋๋ค.๐ ๋ฐฉ์ด์ ๊ฒ์ฆ ์ถ๊ฐ ์ ์
@Override public void open(@NonNull ExecutionContext executionContext) { + Objects.requireNonNull(executionContext.getString("startDate"), "startDate is required"); + Objects.requireNonNull(executionContext.getString("endDate"), "endDate is required"); LocalDate startDate = LocalDate.parse(executionContext.getString("startDate")); LocalDate endDate = LocalDate.parse(executionContext.getString("endDate")); long partitionOffset = executionContext.getLong("partitionOffset"); long partitionLimit = executionContext.getLong("partitionLimit"); + + if (partitionLimit <= 0) { + throw new IllegalArgumentException("partitionLimit must be positive"); + } this.iterator = dailyProductMetricRepository.findAggregatedBy(startDate, endDate, partitionOffset, partitionLimit) .iterator(); }apps/commerce-batch/src/test/java/com/loopers/application/batch/product/WeeklyProductMetricSchedulerTest.java (1)
142-142: ๋ถํ์ํ ์ฃผ์ ์ฝ๋ ์ ๋ฆฌ ํ์์ฃผ์ ์ฒ๋ฆฌ๋ ์ฝ๋๊ฐ ๋จ์์์ต๋๋ค. ์ ๋ฆฌํด ์ฃผ์ธ์.
๐ ์์ ์ ์
-// // ์ํ 1: 7์ผ ๋์ ์ข์์ 10์ฉ (์ด 70) + // ์ํ 1: 7์ผ ๋์ ์ข์์ 10์ฉ (์ด 70)core/domain/src/main/java/com/loopers/core/domain/product/MonthlyProductMetric.java (1)
8-45:WeeklyProductMetric๊ณผ์ ์ผ๊ด์ฑ ๊ฐ์ ์ ์
WeeklyProductMetric์@AllArgsConstructor(access = AccessLevel.PRIVATE)๋ฅผ ์ฌ์ฉํ์ฌ ์์ฑ์๋ฅผ ์๋ ์์ฑํ๋ ๋ฐ๋ฉด, ์ด ํด๋์ค๋ ์๋์ผ๋ก private ์์ฑ์๋ฅผ ์ ์ํ์ต๋๋ค. ๊ธฐ๋ฅ์ ๋์ผํ์ง๋ง, ์ฝ๋๋ฒ ์ด์ค ์ผ๊ด์ฑ์ ์ํด Lombok ์ด๋ ธํ ์ด์ ์ฌ์ฉ์ ๊ณ ๋ คํด ๋ณด์ธ์.๐ ์์ ์ ์
import com.loopers.core.domain.common.vo.CreatedAt; import com.loopers.core.domain.common.vo.UpdatedAt; import com.loopers.core.domain.product.vo.*; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Getter; @Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) public class MonthlyProductMetric { private final MonthlyProductMetricId id; - private final ProductId productId; - private final ProductLikeCount likeCount; - private final ProductDetailViewCount viewCount; - private final ProductTotalSalesCount totalSalesCount; - private final YearMonth yearMonth; - private final CreatedAt createdAt; - private final UpdatedAt updatedAt; - private MonthlyProductMetric( - MonthlyProductMetricId id, - ProductId productId, - ProductLikeCount likeCount, - ProductDetailViewCount viewCount, - ProductTotalSalesCount totalSalesCount, - YearMonth yearMonth, - CreatedAt createdAt, - UpdatedAt updatedAt - ) { - this.id = id; - this.productId = productId; - this.likeCount = likeCount; - this.viewCount = viewCount; - this.totalSalesCount = totalSalesCount; - this.yearMonth = yearMonth; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } - public static MonthlyProductMetric create(apps/commerce-batch/src/main/java/com/loopers/application/batch/product/MonthlyProductMetricBatchReader.java (1)
31-38:@Nullable์ด๋ ธํ ์ด์ ๋๋ฝ
WeeklyProductMetricBatchReader์read()๋ฉ์๋์๋@Nullable์ด๋ ธํ ์ด์ ์ด ์์ง๋ง, ์ด ํด๋์ค์๋ ๋๋ฝ๋์ด ์์ต๋๋ค. ์ผ๊ด์ฑ์ ์ํด ์ถ๊ฐํ๋ ๊ฒ์ด ์ข์ต๋๋ค.๐ ์ ์๋ ์์
+import org.springframework.lang.Nullable; + @RequiredArgsConstructor public class MonthlyProductMetricBatchReader implements ItemStreamReader<ProductMetricAggregation> { // ... + @Nullable @Override public ProductMetricAggregation read() { if (Objects.isNull(iterator) || !iterator.hasNext()) { return null; } return iterator.next(); } }core/service/src/main/java/com/loopers/core/service/product/component/GetMonthlyProductRankingsStrategy.java (1)
26-29:supports()๋ฉ์๋์ NullPointerException ๊ฐ๋ฅ์ฑ
typeํ๋ผ๋ฏธํฐ๊ฐnull๋ก ์ ๋ฌ๋ ๊ฒฝ์ฐtype.equals("MONTHLY")์์ NPE๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.GetWeeklyProductRankingsStrategy๋ ๋์ผํ ํจํด์ ์ฌ์ฉํ๊ณ ์์ง๋ง, ๋ฐฉ์ด์ ์ฝ๋ฉ์ ๊ณ ๋ คํด๋ณผ ์ ์์ต๋๋ค.๐ ์ ์๋ ์์
@Override public boolean supports(String type) { - return type.equals("MONTHLY"); + return "MONTHLY".equals(type); }core/service/src/main/java/com/loopers/core/service/product/component/GetWeeklyProductRankingsStrategy.java (1)
26-29:type์ด null์ธ ๊ฒฝ์ฐ NPE ๋ฐ์ ๊ฐ๋ฅ
supports()๋ฉ์๋์์type.equals("WEEKLY")ํธ์ถ ์type์ด null์ด๋ฉด NullPointerException์ด ๋ฐ์ํฉ๋๋ค. ๋ค๋ฅธ ์ ๋ต๋ค๋ ๋์ผํ ํจํด์ ์ฌ์ฉํ๊ณ ์์ผ๋, ๋ฐฉ์ด์ ์ฝ๋ฉ์ ์ํด ์์ ์ ๊ถ์ฅํฉ๋๋ค.๐ ์ ์๋ ์์
@Override public boolean supports(String type) { - return type.equals("WEEKLY"); + return "WEEKLY".equals(type); }apps/commerce-batch/src/main/java/com/loopers/application/batch/product/MonthlyProductMetricBatchPartitioner.java (1)
42-51: ๋ถํ์ํ ๋น ํํฐ์ ์์ฑ ๊ฐ๋ฅ์ฑ
totalCount๊ฐgridSize๋ณด๋ค ์์ ๊ฒฝ์ฐ์๋ ๋ชจ๋gridSize๊ฐ์ ํํฐ์ ์ ์์ฑํฉ๋๋ค. ์ด๋ก ์ธํดpartitionOffset์ดtotalCount๋ฅผ ์ด๊ณผํ๋ ๋น ํํฐ์ ์ด ์์ฑ๋ ์ ์์ต๋๋ค.๐ ์ ์๋ ์์
+ int actualGridSize = (int) Math.min(gridSize, totalCount); + long targetSize = (totalCount / actualGridSize) + 1; Map<String, ExecutionContext> partitions = new HashMap<>(); - for (int i = 0; i < gridSize; i++) { + for (int i = 0; i < actualGridSize; i++) { ExecutionContext context = new ExecutionContext(); context.putLong("partitionOffset", i * targetSize); context.putLong("partitionLimit", targetSize); context.putString("startDate", startDateParam); context.putString("endDate", endDateParam); partitions.put("partition" + i, context); }core/domain/src/main/java/com/loopers/core/domain/common/vo/YearMonthWeek.java (1)
48-50:getWeekEndDateTime()์์ ๋ง์ง๋ง ๋๋ ธ์ด ๋๋ฝ
atTime(23, 59, 59)๋ ๋ง์ง๋ง 1์ด(999,999,999 ๋๋ ธ์ด)๋ฅผ ํฌํจํ์ง ์์ต๋๋ค. ๋ฒ์ ์ฟผ๋ฆฌ์ ์ฌ์ฉ ์ ๋ฏธ์ธํ ๋ฐ์ดํฐ ๋๋ฝ์ด ๋ฐ์ํ ์ ์์ต๋๋ค.๐ ์ ์๋ ์์
public LocalDateTime getWeekEndDateTime() { - return this.getWeekEndDate().atTime(23, 59, 59); + return this.getWeekEndDate().atTime(23, 59, 59, 999_999_999); }๋๋
LocalTime.MAX๋ฅผ ์ฌ์ฉ:return this.getWeekEndDate().atTime(LocalTime.MAX);core/infra/database/mysql/src/main/java/com/loopers/core/infra/database/mysql/product/impl/MonthlyProductMetricRepositoryImpl.java (1)
25-30: Null ๋ฆฌ์คํธ ์ ๋ ฅ ์ NPE ๊ฐ๋ฅ์ฑ
bulkUpsert์์monthlyProductMetrics๊ฐ null์ธ ๊ฒฝ์ฐstream()ํธ์ถ ์ NPE๊ฐ ๋ฐ์ํฉ๋๋ค.MonthlyProductMetricBulkRepositoryImpl์์ null ์ฒดํฌ๋ฅผ ์ํํ์ง๋ง, entity ๋ณํ ์ ์ ๋ฐ์ํฉ๋๋ค.๐ ์ ์๋ ์์
@Override public void bulkUpsert(List<MonthlyProductMetric> monthlyProductMetrics) { + if (monthlyProductMetrics == null || monthlyProductMetrics.isEmpty()) { + return; + } bulkRepository.bulkUpsert(monthlyProductMetrics.stream() .map(MonthlyProductMetricEntity::from) .toList()); }apps/commerce-batch/src/main/java/com/loopers/application/batch/product/config/WeeklyProductMetricBatchConfig.java (3)
38-49: Step ์ด๋ฆ์ด ์ค์ ๊ธฐ๋ฅ๊ณผ ๋ถ์ผ์นํฉ๋๋ค.
partitionDailyMetricStep๊ณผcollectDailyMetricStep์ ์ฃผ๊ฐ(Weekly) ๋ฉํธ๋ฆญ ์ง๊ณ ์์ ์ ์ํํ์ง๋ง, ์ด๋ฆ์ "Daily"๊ฐ ํฌํจ๋์ด ์์ด ํผ๋์ ์ค ์ ์์ต๋๋ค.partitionWeeklyMetricStep,aggregateWeeklyMetricStep๋ฑ์ผ๋ก ๋ณ๊ฒฝ์ ๊ถ์ฅํฉ๋๋ค.
80-86: TaskExecutor Bean ์ด๋ฆ ์ถฉ๋ ๊ฐ๋ฅ์ฑ
asyncTaskExecutor()๋ผ๋ ์ผ๋ฐ์ ์ธ ์ด๋ฆ์ ๋ค๋ฅธ ๋ฐฐ์น ์ค์ (์: MonthlyProductMetricBatchConfig์monthlyAsyncTaskExecutor)๊ณผ ์ถฉ๋ํ๊ฑฐ๋ ์๋์น ์์ ์ฃผ์ ์ด ๋ฐ์ํ ์ ์์ต๋๋ค.๐ ์์ ์ ์
@Bean -public TaskExecutor asyncTaskExecutor() { +public TaskExecutor weeklyAsyncTaskExecutor() { SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor("weekly-product-metric-batch-"); executor.setVirtualThreads(true); executor.setConcurrencyLimit(20); return executor; }๊ทธ๋ฆฌ๊ณ
collectDailyMetricStep์์๋ ํด๋น Bean์ ๋ช ์์ ์ผ๋ก ์ฐธ์กฐ:-.taskExecutor(asyncTaskExecutor()) +.taskExecutor(weeklyAsyncTaskExecutor())
75-77: ์ฌ์๋ ํ์(retryLimit) ๋ช ์ ๊ถ์ฅ
retry(DataAccessException.class)๋ง ์ค์ ๋์ด ์๊ณretryLimit๊ฐ ๋ช ์๋์ง ์์์ต๋๋ค. ๊ธฐ๋ณธ๊ฐ์ ์์กดํ๊ธฐ๋ณด๋ค ๋ช ์์ ์ผ๋ก ์ฌ์๋ ํ์๋ฅผ ์ค์ ํ๋ ๊ฒ์ด ์ด์ ์ ์์ธก ๊ฐ๋ฅํ ๋์์ ๋ณด์ฅํฉ๋๋ค.๐ ์์ ์ ์
.faultTolerant() .retry(DataAccessException.class) +.retryLimit(3) .build();core/infra/database/mysql/src/main/java/com/loopers/core/infra/database/mysql/product/DailyProductMetricJpaRepository.java (2)
30-46: JPQL์์ LIMIT/OFFSET ์ง์ ์ฌ์ฉ์ Hibernate ์ ์ฉ ๋ฌธ๋ฒ์ ๋๋ค.ํ์ค JPQL์
LIMIT/OFFSET์ ์ง์ํ์ง ์์ผ๋ฉฐ, ์ด๋ Hibernate(HQL) ํ์ฅ ๊ธฐ๋ฅ์ ๋๋ค. Hibernate ์ธ JPA ๊ตฌํ์ฒด๋ก ์ ํ ์ ํธํ์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.Spring Data JPA์
Pageableํ๋ผ๋ฏธํฐ๋ฅผ ํ์ฉํ๋ฉด ๊ตฌํ์ฒด์ ๋ ๋ฆฝ์ ์ธ ํ์ด์ง๋ค์ด์ ์ด ๊ฐ๋ฅํฉ๋๋ค.
22-28:CAST(createdAt AS date)์ฌ์ฉ ์ ์ธ๋ฑ์ค ํ์ฉ ํ์ธ ํ์
cast(pme.createdAt as date)ํจ์ ํธ์ถ์createdAt์ปฌ๋ผ์ ์ธ๋ฑ์ค๋ฅผ ํ์ฉํ์ง ๋ชปํ๊ฒ ํ ์ ์์ต๋๋ค(Index Scan โ Full Table Scan). ๋ฐ์ดํฐ ๊ท๋ชจ๊ฐ ํด ๊ฒฝ์ฐ ์ฟผ๋ฆฌ ์ฑ๋ฅ์ ์ํฅ์ ์ค ์ ์์ต๋๋ค.๋์์ผ๋ก ๋ ์ง ๋ฒ์ ์กฐ๊ฑด์ ์ฌ์ฉํ๊ฑฐ๋, ๋ณ๋์
date์ปฌ๋ผ์ ์ถ๊ฐํ์ฌ ์ธ๋ฑ์ฑํ๋ ๋ฐฉ๋ฒ์ ๊ณ ๋ คํด ๋ณด์ธ์.-- ๋์: ๋ฒ์ ์กฐ๊ฑด ์ฌ์ฉ WHERE pme.createdAt >= :startDate AND pme.createdAt < :endDate + 1 dayAlso applies to: 36-37
apps/commerce-batch/src/main/java/com/loopers/application/batch/product/config/MonthlyProductMetricBatchConfig.java (1)
75-77: ์ฌ์๋ ํ์ ์ ํ ๋ช ์๋ฅผ ๊ถ์ฅํฉ๋๋ค.
.retry(DataAccessException.class)๋ง ์ค์ ํ๋ฉด Spring Batch ๊ธฐ๋ณธ๊ฐ(1ํ)์ด ์ ์ฉ๋ฉ๋๋ค. ๋ช ์์ ์ผ๋ก.retryLimit()์ ์ง์ ํ๋ฉด ์๋๊ฐ ๋ ๋ช ํํด์ง๊ณ ์ ์ง๋ณด์๊ฐ ์ฉ์ดํฉ๋๋ค.๐ ์ ์๋ ์์ ์ฌํญ
.taskExecutor(monthlyAsyncTaskExecutor()) .faultTolerant() .retry(DataAccessException.class) + .retryLimit(3) .build();
| dependencies { | ||
| implementation(project(":core:infra:database:mysql:mysql-config")) | ||
| implementation(project(":supports:jackson")) | ||
| implementation(project(":supports:logging")) | ||
| implementation(project(":supports:monitoring")) | ||
|
|
||
| //service | ||
| implementation(project(":core:service")) | ||
|
|
||
| //domain | ||
| implementation(project(":core:domain")) | ||
|
|
||
| // web | ||
| implementation("org.springframework.boot:spring-boot-starter-web") | ||
|
|
||
| //batch | ||
| implementation("org.springframework.boot:spring-boot-starter-batch") | ||
|
|
||
| implementation("org.springframework:spring-tx") | ||
| implementation("org.springframework.boot:spring-boot-starter-actuator") | ||
|
|
||
| // test-fixtures | ||
| testImplementation(project(":core:infra:database:mysql:mysql-config")) | ||
| testImplementation(testFixtures(project(":core:domain"))) | ||
| testImplementation(testFixtures(project(":core:infra:database:mysql"))) | ||
| } |
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.
๐งฉ Analysis chain
๐ Script executed:
# Check for web component usage in commerce-batch module
fd -e java -e kt --path-separator / | grep -E "apps/commerce-batch" | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1574
๐ Script executed:
# Search for web annotations in commerce-batch source files
rg -i '(@(RestController|Controller|RequestMapping|GetMapping|PostMapping|Service|Component)|WebController|RestEndpoint)' apps/commerce-batch --type java --type kotlin -nRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 828
๐ Script executed:
# Check the actual directory structure of commerce-batch
find apps/commerce-batch -type f -name "*.kt" -o -name "*.java" -o -name "*.kts" | grep -v ".gradle" | head -30Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1574
๐ Script executed:
# Check CommerceBatchApplication main class
cat -n apps/commerce-batch/src/main/java/com/loopers/CommerceBatchApplication.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 599
๐ Script executed:
# Check one of the scheduler files to see if web exposure is used
cat -n apps/commerce-batch/src/main/java/com/loopers/application/batch/product/MonthlyProductMetricScheduler.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 2033
๐ Script executed:
# Search for any Actuator endpoint configuration or web exposure in the batch module
rg -i '(actuator|endpoint|web|servlet)' apps/commerce-batch --type java --type kotlin -A 2 -B 2Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 788
spring-boot-starter-web ์ ๊ฑฐ ๋ฐ ์ค๋ณต๋ mysql-config ์์กด์ฑ ์ ๋ฆฌ
-
spring-boot-starter-web ์ ๊ฑฐ (Line 14): ๋ฐฐ์น ์ ํ๋ฆฌ์ผ์ด์ ์์ ์น ์คํํฐ๊ฐ ์ฌ์ฉ๋์ง ์์ต๋๋ค. ๋ชจ๋ ์ ์ฒด์์ HTTP ์๋ํฌ์ธํธ๋ ์น ์ปจํธ๋กค๋ฌ๊ฐ ์์ผ๋ฉฐ, ๋ชจ๋ ๋ฐฐ์น ์์ ์
@Scheduled์ด๋ ธํ ์ด์ ์ ํตํด ์ค์ผ์ค๋ง์ผ๋ก ์คํ๋ฉ๋๋ค. ์ด ์์กด์ฑ์ ์ ๊ฑฐํ์ธ์. -
์ค๋ณต๋ mysql-config ์ ๊ฑฐ (Line 23):
mysql-config๋ ์ด๋ฏธimplementation์ค์ฝํ(Line 2)์ ์ ์ธ๋์ด ์์ผ๋ฏ๋ก,testImplementation์์์ ์ ์ธ์ ์ ๊ฑฐํ์ธ์. Gradle์์ ํ ์คํธ ์์กด์ฑ์ ์๋์ผ๋ก ๊ตฌํ ์์กด์ฑ์ ์์๋ฐ์ต๋๋ค.
๐ค Prompt for AI Agents
In apps/commerce-batch/build.gradle.kts around lines 1 to 26, remove the
unnecessary "org.springframework.boot:spring-boot-starter-web" dependency (line
~14) because this is a batch-only module with no web controllers, and delete the
duplicate testImplementation(project(":core:infra:database:mysql:mysql-config"))
(line ~23) since mysql-config is already declared as an implementation
dependency and test scope inherits implementation dependencies; update the file
to keep only the batch- and required support dependencies.
...tch/src/main/java/com/loopers/application/batch/product/MonthlyProductMetricBatchWriter.java
Show resolved
Hide resolved
| @Scheduled(cron = "0 0 2 1 * ?") | ||
| public void run() throws JobInstanceAlreadyCompleteException, JobExecutionAlreadyRunningException, JobParametersInvalidException, JobRestartException { |
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.
๋ฐฐ์น ์์ ์คํ ์คํจ ์ ๊ด์ฐฐ์ฑ๊ณผ ์ค๋ฅ ์ฒ๋ฆฌ๋ฅผ ๊ฐ์ ํ์ธ์.
์ค์ผ์ค๋ฌ ๋ฉ์๋๊ฐ 4๊ฐ์ ์ฒดํฌ ์์ธ๋ฅผ ๋์ง์ง๋ง ๋ก๊น ์ด๋ ์์ธ ์ฒ๋ฆฌ๊ฐ ์์ด, ๋ฐฐ์น ์์ ์คํจ๊ฐ ์กฐ์ฉํ ๋ฌด์๋ ์ ์์ต๋๋ค. ์ด์ ํ๊ฒฝ์์ ๋ฌธ์ ์ง๋จ์ ์ด๋ ต๊ฒ ๋ง๋ญ๋๋ค.
๐ ๋ก๊น ๊ณผ ์์ธ ์ฒ๋ฆฌ ์ถ๊ฐ ์ ์
@Scheduled(cron = "0 0 2 1 * ?")
- public void run() throws JobInstanceAlreadyCompleteException, JobExecutionAlreadyRunningException, JobParametersInvalidException, JobRestartException {
+ public void run() {
+ try {
+ log.info("Starting monthly product metric aggregation batch job");
YearMonth lastMonth = YearMonth.now().minusMonths(1);
LocalDate startDate = lastMonth.atDay(1);
LocalDate endDate = lastMonth.atEndOfMonth();
JobParameters jobParameters = new JobParametersBuilder()
.addString("startDate", startDate.toString())
.addString("endDate", endDate.toString())
.addLong("timestamp", System.currentTimeMillis())
.toJobParameters();
jobLauncher.run(monthlyProductMetricJob, jobParameters);
+ log.info("Monthly product metric aggregation batch job completed successfully");
+ } catch (JobExecutionAlreadyRunningException e) {
+ log.warn("Monthly product metric batch job is already running", e);
+ } catch (JobInstanceAlreadyCompleteException | JobRestartException | JobParametersInvalidException e) {
+ log.error("Failed to execute monthly product metric batch job", e);
+ }
}Lombok์ @Slf4j ์ด๋
ธํ
์ด์
์ ํด๋์ค์ ์ถ๊ฐํ์ธ์.
Committable suggestion skipped: line range outside the PR's diff.
๐ค Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/application/batch/product/MonthlyProductMetricScheduler.java
around lines 25-26, the scheduled run() method currently declares four checked
exceptions but has no logging or handling; add Lombok's @Slf4j to the class,
wrap the job execution in a try/catch that catches those specific exceptions (or
Exception) inside the method instead of letting them propagate silently, call
log.error(...) with a clear contextual message and the exception instance for
each failure, and optionally emit a metric or rethrow a runtime exception if you
need the scheduler to mark the job as failed.
| @Component | ||
| @RequiredArgsConstructor | ||
| public class WeeklyProductMetricBatchWriter implements ItemStreamWriter<ProductMetricAggregation> { | ||
|
|
||
| private final WeeklyProductMetricRepository repository; | ||
| private YearMonthWeek yearMonthWeek; | ||
|
|
||
| @Override | ||
| public void open(@NonNull ExecutionContext executionContext) { | ||
| LocalDate startDate = LocalDate.parse(executionContext.getString("startDate")); | ||
| this.yearMonthWeek = YearMonthWeek.from(startDate); | ||
| } |
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.
์ฑ๊ธํค Bean์์ ๊ฐ๋ณ ์ํ๋ก ์ธํ ๋์์ฑ ๋ฌธ์
@Component๋ ์ฑ๊ธํค ์ค์ฝํ์ด์ง๋ง, yearMonthWeek ํ๋๋ ๊ฐ๋ณ ์ํ์
๋๋ค. ํํฐ์
์ด ๋ณ๋ ฌ๋ก ์คํ๋ ๊ฒฝ์ฐ, ์ฌ๋ฌ ์ค๋ ๋๊ฐ ๋์ผํ ์ธ์คํด์ค๋ฅผ ๊ณต์ ํ๋ฉด์ open() ๋ฉ์๋์์ yearMonthWeek๋ฅผ ๋ฎ์ด์ธ ์ ์์ด ๋ฐ์ดํฐ ์ ํฉ์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค.
@StepScope๋ฅผ ์ถ๊ฐํ์ฌ ๊ฐ ํํฐ์
์ด ๋
๋ฆฝ์ ์ธ Writer ์ธ์คํด์ค๋ฅผ ๊ฐ๋๋ก ํด์ผ ํฉ๋๋ค.
๐ ์ ์๋ ์์
import org.springframework.batch.item.Chunk;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.item.ItemStreamWriter;
+import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.stereotype.Component;
@Component
+@StepScope
@RequiredArgsConstructor
public class WeeklyProductMetricBatchWriter implements ItemStreamWriter<ProductMetricAggregation> {๐ค Prompt for AI Agents
In
apps/commerce-batch/src/main/java/com/loopers/application/batch/product/WeeklyProductMetricBatchWriter.java
around lines 17 to 28, the class is a singleton component but holds mutable
state in the yearMonthWeek field which can be overwritten concurrently by
parallel partitions; make the writer step-scoped so each partition gets its own
instance: annotate the class with Spring Batch's @StepScope (and add the
necessary import) so dependency injection still works but a new writer instance
is created per step execution, keep yearMonthWeek as an instance field
(non-static) and leave open() logic unchanged; verify no other shared mutable
fields exist.
| - monitoring.yml | ||
| batch: | ||
| jdbc: | ||
| initialize-schema: always |
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.
ํ๋ก๋์ ํ๊ฒฝ์์ ๋ฐ์ดํฐ ์์ค ์ํ์ด ์์ต๋๋ค.
initialize-schema: always ์ค์ ์ ์ ํ๋ฆฌ์ผ์ด์
์ด ์์๋ ๋๋ง๋ค ์คํค๋ง๋ฅผ ์ฌ์์ฑํฉ๋๋ค. ์ด๋ ๊ฐ๋ฐ ํ๊ฒฝ์์๋ ์ ์ฉํ์ง๋ง, ํ๋ก๋์
ํ๊ฒฝ์์๋ ๋ฐ์ดํฐ ์์ค์ ์ด๋ํ ์ ์์ต๋๋ค. ํ๋กํ๋ณ๋ก ๋ค๋ฅด๊ฒ ์ค์ ํด์ผ ํฉ๋๋ค.
๐ ํ๋กํ๋ณ ์ค์ ์ ์
๊ธ๋ก๋ฒ ์ค์ ์ ์ ๊ฑฐํ๊ณ ๊ฐ ํ๋กํ ์น์ ์ ๋ค์๊ณผ ๊ฐ์ด ์ถ๊ฐํ์ธ์:
- batch:
- jdbc:
- initialize-schema: always
- job:
- enabled: false
+ batch:
+ job:
+ enabled: false๊ทธ๋ฆฌ๊ณ local/test ํ๋กํ ์น์ ์๋ง ์ถ๊ฐ:
spring:
config:
activate:
on-profile: local, test
+ batch:
+ jdbc:
+ initialize-schema: alwaysํ๋ก๋์
ํ๊ฒฝ(dev, qa, prd)์์๋ Flyway๋ Liquibase ๊ฐ์ ๋ง์ด๊ทธ๋ ์ด์
๋๊ตฌ๋ฅผ ์ฌ์ฉํ๊ฑฐ๋ never๋ก ์ค์ ํ์ธ์.
Committable suggestion skipped: line range outside the PR's diff.
๐ค Prompt for AI Agents
In apps/commerce-batch/src/main/resources/application.yml around line 33, the
global setting "initialize-schema: always" causes schema recreation on every
startup (risking production data loss); remove this global key and instead add
profile-specific settings: set initialize-schema: always (or create-drop) only
under local/test profiles, and set initialize-schema: never for production
profiles (dev/qa/prd) while relying on Flyway/Liquibase for migrations; update
the YAML to place these keys under appropriate spring.profiles sections so
production never recreates the schema.
| Page<MonthlyProductRankingProjection> findMonthlyProductRanking( | ||
| LocalDate date, | ||
| Double payWeight, | ||
| Double viewWeight, | ||
| Double likeWeight, | ||
| Pageable pageable | ||
| ); |
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.
๐งฉ Analysis chain
๐ Script executed:
#!/bin/bash
# Find the implementation file
find . -type f -name "*MonthlyProductMetricQuerydslRepositoryImpl*" -o -name "*MonthlyProductMetric*" | head -20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1964
๐ Script executed:
#!/bin/bash
# Search for the implementation class definition
rg "class.*MonthlyProductMetricQuerydslRepository" --type javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 329
๐ Script executed:
#!/bin/bash
# Search for findMonthlyProductRanking implementation
rg "findMonthlyProductRanking" --type java -A 30Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 9995
๐ Script executed:
#!/bin/bash
# Read the complete implementation file
cat -n core/infra/database/mysql/src/main/java/com/loopers/core/infra/database/mysql/product/MonthlyProductMetricQuerydslRepositoryImpl.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 3286
๐ Script executed:
#!/bin/bash
# Check the caller class for parameter validation
cat -n core/infra/database/mysql/src/main/java/com/loopers/core/infra/database/mysql/product/impl/MonthlyProductMetricRepositoryImpl.java | head -80Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 2708
๐ Script executed:
#!/bin/bash
# Search for any @Nullable or @NonNull annotations on the interface
rg "@Nullable|@NonNull|Objects.requireNonNull" core/infra/database/mysql/src/main/java/com/loopers/core/infra/database/mysql/product/MonthlyProductMetricQuerydslRepository.java -B 2 -A 2Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 70
๐ Script executed:
#!/bin/bash
# Check the interface definition for annotations
cat -n core/infra/database/mysql/src/main/java/com/loopers/core/infra/database/mysql/product/MonthlyProductMetricQuerydslRepository.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 770
๐ Script executed:
#!/bin/bash
# Search for any validation or use of these weight parameters in domain layer
rg "payWeight|viewWeight|likeWeight" --type java -B 2 -A 2Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 18074
๐ Script executed:
#!/bin/bash
# Check if there's validation at service or controller level
find . -type f -name "*.java" | xargs grep -l "findRankingsBy" | head -10Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 786
๐ Script executed:
#!/bin/bash
# Check the strategy component to see weight handling
cat -n core/service/src/main/java/com/loopers/core/service/product/component/GetMonthlyProductRankingsStrategy.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1493
date ํ๋ผ๋ฏธํฐ์ null ์ฒดํฌ์ ๊ฐ์ค์น ๊ฐ ๊ฒ์ฆ ํ์
๊ตฌํ์ฒด(MonthlyProductMetricQuerydslRepositoryImpl)์์ date ํ๋ผ๋ฏธํฐ์ ๋ํ null ์ฒดํฌ๊ฐ ์์ด date.getYear() ํธ์ถ ์ NullPointerException์ด ๋ฐ์ํ ์ ์์ต๋๋ค. ๋ํ payWeight, viewWeight, likeWeight ํ๋ผ๋ฏธํฐ๋ null์ผ ๊ฒฝ์ฐ .multiply() ์ฐ์ฐ์์ NPE๊ฐ ๋ฐ์ํ๋ฉฐ, ์์ ๋๋ ์ ํจํ์ง ์์ ๊ฐ์ค์น ๊ฐ์ ๋ํ ๊ฒ์ฆ์ด ์์ด ๋ถ์ ํํ ์์ ๊ณ์ฐ์ด ๋ฐ์ํ ์ ์์ต๋๋ค.
๊ถ์ฅ ์์ ์ฌํญ
๋ฉ์๋ ์์ ๋ถ๋ถ์์ ๋ค์ ๊ฒ์ฆ ๋ก์ง ์ถ๊ฐ:
Objects.requireNonNull(date, "date must not be null")Objects.requireNonNull(payWeight, "payWeight must not be null")- ๊ฐ์ค์น ๊ฐ์ด ์์ ๋๋ 0 ์ดํ์ธ ๊ฒฝ์ฐ์ ๋ํ ๊ฒ์ฆ ๋ก์ง
๐ค Prompt for AI Agents
In
core/infra/database/mysql/src/main/java/com/loopers/core/infra/database/mysql/product/MonthlyProductMetricQuerydslRepository.java
around lines 11-17, add input validation at the start of the implementation:
require non-null for date, payWeight, viewWeight, likeWeight (e.g.
Objects.requireNonNull(..., "X must not be null")), and validate that numeric
weights are > 0 (else throw IllegalArgumentException with a clear message);
these checks prevent date.getYear() and weight.multiply(...)
NullPointerExceptions and guard against zero/negative weights affecting ranking.
| List<MonthlyProductRankingProjection> content = queryFactory | ||
| .select(new QMonthlyProductRankingProjection( | ||
| metric.productId, | ||
| Expressions.numberTemplate(Long.class, | ||
| "ROW_NUMBER() OVER (ORDER BY {0} DESC)", | ||
| scoreCalculation), | ||
| scoreCalculation.as("score") | ||
| )) | ||
| .from(metric) | ||
| .where(metric.year.eq(year) | ||
| .and(metric.month.eq(month))) | ||
| .orderBy(scoreCalculation.desc()) | ||
| .offset(pageable.getOffset()) | ||
| .limit(pageable.getPageSize()) | ||
| .fetch(); |
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.
ROW_NUMBER()์ ํ์ด์ง๋ค์ด์ ์กฐํฉ ์ ๋ญํน ๊ฐ์ด ์๋ชป๋ฉ๋๋ค.
ROW_NUMBER() OVER (ORDER BY score DESC)๋ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ ์ ์ฒด๊ฐ ์๋ ํ์ด์ง๋ค์ด์
์ ์ฉ ํ์ ๊ฒฐ๊ณผ ์งํฉ์์ ๊ณ์ฐ๋ฉ๋๋ค. ์๋ฅผ ๋ค์ด, pageNo=1, pageSize=10์ผ ๋ ์ค์ ์์ 1120์์ธ ๋ฐ์ดํฐ๊ฐ ROW_NUMBER 110์ผ๋ก ๋ฐํ๋ฉ๋๋ค.
์์ ๋ฐฉ์:
- ์๋ธ์ฟผ๋ฆฌ์์ ๋จผ์ ROW_NUMBER๋ฅผ ๊ณ์ฐํ ํ ์ธ๋ถ ์ฟผ๋ฆฌ์์ ํ์ด์ง๋ค์ด์ ์ ์ฉ
- ๋๋ ROW_NUMBER ๋์
offset + index + 1๋ก ํด๋ผ์ด์ธํธ์์ ๋ญํน ๊ณ์ฐ
๐ ์์ ์ ์: ํด๋ผ์ด์ธํธ ์ธก ๋ญํน ๊ณ์ฐ
ROW_NUMBER ์ปฌ๋ผ์ ์ ๊ฑฐํ๊ณ , ์๋น์ค ๋ ์ด์ด์์ offset + index + 1๋ก ๋ญํน์ ๊ณ์ฐํ๋ ๋ฐฉ์์ ๊ถ์ฅํฉ๋๋ค:
// Service layer์์ ์ฒ๋ฆฌ
long offset = pageable.getOffset();
for (int i = 0; i < content.size(); i++) {
content.get(i).setRanking(offset + i + 1);
}๐ค Prompt for AI Agents
In
core/infra/database/mysql/src/main/java/com/loopers/core/infra/database/mysql/product/MonthlyProductMetricQuerydslRepositoryImpl.java
around lines 42 to 56, the current use of ROW_NUMBER() in the same query that
applies offset/limit produces incorrect rankings for paginated results; fix by
either 1) moving the ROW_NUMBER() calculation into a subquery (compute
ROW_NUMBER() OVER (ORDER BY score DESC) in an inner query and then apply
WHERE/ORDER/OFFSET/LIMIT in an outer query selecting the precomputed ranking),
or 2) remove the ROW_NUMBER projection from this repository and compute ranking
in the service layer using offset + index + 1 after fetch; choose one approach
and implement it consistently (if you pick client-side, strip ROW_NUMBER from
the select and set ranking in service using pageable.getOffset() + i + 1).
| Page<WeeklyProductRankingProjection> findWeeklyProductRanking( | ||
| LocalDate date, | ||
| Double payWeight, | ||
| Double viewWeight, | ||
| Double likeWeight, | ||
| Pageable pageable | ||
| ); |
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.
๐งฉ Analysis chain
๐ Script executed:
#!/bin/bash
# Find the implementation class
find . -name "*WeeklyProductMetricQuerydslRepository*" -type fRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 353
๐ Script executed:
#!/bin/bash
# Search for the implementation class
rg "class WeeklyProductMetricQuerydslRepositoryImpl" -A 30Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 5599
๐ Script executed:
#!/bin/bash
# Also search for the interface file to confirm its location
rg "interface WeeklyProductMetricQuerydslRepository" -A 20Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1661
๐ Script executed:
#!/bin/bash
# Get complete implementation file
wc -l ./core/infra/database/mysql/src/main/java/com/loopers/core/infra/database/mysql/product/WeeklyProductMetricQuerydslRepositoryImpl.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 225
๐ Script executed:
#!/bin/bash
# Read the complete implementation
cat -n ./core/infra/database/mysql/src/main/java/com/loopers/core/infra/database/mysql/product/WeeklyProductMetricQuerydslRepositoryImpl.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 3626
๐ Script executed:
#!/bin/bash
# Check for @NonNull or @NotNull annotations in both files
rg "(@NonNull|@NotNull|@Nullable)" ./core/infra/database/mysql/src/main/java/com/loopers/core/infra/database/mysql/product/WeeklyProductMetric*Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 70
๐ Script executed:
#!/bin/bash
# Search for callers of findWeeklyProductRanking method
rg "findWeeklyProductRanking" --type java -B 2 -A 5Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 3995
๐ Script executed:
#!/bin/bash
# Check if there are any validation annotations or constraints in the imports
head -20 ./core/infra/database/mysql/src/main/java/com/loopers/core/infra/database/mysql/product/WeeklyProductMetricQuerydslRepository.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 640
๊ฐ์ค์น ํ๋ผ๋ฏธํฐ์ null ์์ ์ฑ์ ์ถ๊ฐํ์ธ์.
๊ตฌํ์ฒด์ 42-44์ค์์ payWeight, viewWeight, likeWeight ํ๋ผ๋ฏธํฐ๊ฐ null ์ฒดํฌ ์์ด .multiply() ์ฐ์ฐ์ ์ง์ ์ฌ์ฉ๋ฉ๋๋ค. null ๊ฐ์ด ์ ๋ฌ๋๋ฉด NullPointerException์ด ๋ฐ์ํฉ๋๋ค. @NonNull ์ด๋
ธํ
์ด์
์ ํ๋ผ๋ฏธํฐ์ ์ถ๊ฐํ๊ฑฐ๋ primitive double ํ์
์ผ๋ก ๋ณ๊ฒฝํ์ธ์.
๐ค Prompt for AI Agents
In
core/infra/database/mysql/src/main/java/com/loopers/core/infra/database/mysql/product/WeeklyProductMetricQuerydslRepository.java
around lines 11 to 17 (implementation uses lines ~42-44), the weight parameters
payWeight, viewWeight, likeWeight are nullable and are used with .multiply()
without null checks which can cause NPEs; fix by making these parameters
null-safeโeither change their types from Double to primitive double or add
@NonNull to each parameter in the method signature, and ensure the
implementation assumes non-null (or add explicit null-to-default fallback before
multiply if you prefer to keep boxed types).
| List<WeeklyProductRankingProjection> content = queryFactory | ||
| .select(new QWeeklyProductRankingProjection( | ||
| metric.productId, | ||
| Expressions.numberTemplate(Long.class, | ||
| "ROW_NUMBER() OVER (ORDER BY {0} DESC)", | ||
| scoreCalculation), | ||
| scoreCalculation.as("score") | ||
| )) | ||
| .from(metric) | ||
| .where(metric.year.eq(year) | ||
| .and(metric.month.eq(month)) | ||
| .and(metric.weekOfYear.eq(weekOfYear))) | ||
| .orderBy(scoreCalculation.desc()) | ||
| .offset(pageable.getOffset()) | ||
| .limit(pageable.getPageSize()) | ||
| .fetch(); |
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.
ROW_NUMBER()์ ํ์ด์ง๋ค์ด์ ์กฐํฉ ๋ฌธ์ (MonthlyProductMetricQuerydslRepositoryImpl๊ณผ ๋์ผ)
์ Monthly ๊ตฌํ๊ณผ ๋์ผํ๊ฒ, ROW_NUMBER๊ฐ ํ์ด์ง๋ค์ด์ ์ ์ฉ ํ ๊ฒฐ๊ณผ์ ๋ํด ๊ณ์ฐ๋์ด ์๋ชป๋ ๋ญํน ๊ฐ์ด ๋ฐํ๋ฉ๋๋ค.
๐ค Prompt for AI Agents
In
core/infra/database/mysql/src/main/java/com/loopers/core/infra/database/mysql/product/WeeklyProductMetricQuerydslRepositoryImpl.java
around lines 46-61, the ROW_NUMBER() window is being applied after offset/limit
which yields wrong ranks; instead compute ROW_NUMBER() in an inner/subquery
without pagination and then apply pagination in an outer query. Change the
Querydsl logic to build a subquery that selects productId, ROW_NUMBER() OVER
(ORDER BY score DESC) as rank and score (no offset/limit), then create an outer
query that selects from that subquery, orders by score (or rank) and applies
pageable.getOffset()/getPageSize() so pagination does not affect rank
calculation.
| .where(metric.year.eq(year) | ||
| .and(metric.month.eq(month)) | ||
| .and(metric.weekOfYear.eq(weekOfYear))) |
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.
๐งฉ Analysis chain
๐ Script executed:
fd "YearMonthWeek.java" --exec cat {} \;Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1657
๐ Script executed:
fd "WeeklyProductMetricBatchWriter.java" -x head -100 {}Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 1517
๐ Script executed:
rg "class WeeklyProductMetric" --type java -A 10Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 18443
๐ Script executed:
cat > /tmp/test_week.java << 'EOF'
import java.time.LocalDate;
import java.time.temporal.WeekFields;
import java.util.Locale;
public class test_week {
public static void main(String[] args) {
// Test cross-month week scenario
// Week that spans Dec 31 2024 to Jan 6 2025
LocalDate dec31 = LocalDate.of(2024, 12, 31);
LocalDate jan1 = LocalDate.of(2025, 1, 1);
LocalDate jan6 = LocalDate.of(2025, 1, 6);
WeekFields wf = WeekFields.of(Locale.getDefault());
System.out.println("Dec 31, 2024: year=" + dec31.getYear() + ", month=" + dec31.getMonthValue() + ", weekOfYear=" + dec31.get(wf.weekOfYear()));
System.out.println("Jan 1, 2025: year=" + jan1.getYear() + ", month=" + jan1.getMonthValue() + ", weekOfYear=" + jan1.get(wf.weekOfYear()));
System.out.println("Jan 6, 2025: year=" + jan6.getYear() + ", month=" + jan6.getMonthValue() + ", weekOfYear=" + jan6.get(wf.weekOfYear()));
}
}
EOF
javac /tmp/test_week.java 2>/dev/null && java -cp /tmp test_weekRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 72
๐ Script executed:
# Check if there are any query methods that might use date ranges instead of the three-field filter
rg "findWeeklyProductRanking|findByYearMonthWeek" --type java -A 5Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 3129
๐ Script executed:
# Look at the complete query method to understand the context
cat core/infra/database/mysql/src/main/java/com/loopers/core/infra/database/mysql/product/WeeklyProductMetricQuerydslRepositoryImpl.java | head -80Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 3122
๐ Script executed:
# Verify the exact locale used in YearMonthWeek vs the query
rg "Locale\.(KOREA|getDefault)" --type java -B 2 -A 2Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 2720
๐ Script executed:
# Check if there are any tests that cover cross-month week scenarios
fd "test" -t f --name "*Weekly*" | head -5 | xargs grep -l "Dec\|Jan\|month" 2>/dev/null || echo "No specific cross-month tests found"Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 391
๐ Script executed:
# Verify the impact by checking if Locale.KOREA behavior is different from Locale.getDefault()
# and understand what the default locale typically is
rg "server.servlet.locale|application.properties|locale" core/infra/database/mysql/src/main/resources/ 2>/dev/null || echo "No locale config found in infra module"Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 126
๐ Script executed:
# Check the batch config to see if there's a default locale set
fd "application*.properties" -type f | xargs grep -i "locale" 2>/dev/null || echo "No locale property found"Repository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 300
๐ Script executed:
# Look for any TODO or comments about the locale inconsistency
rg "TODO|FIXME|locale|Locale\.(KOREA|getDefault)" core/domain/src/main/java/com/loopers/core/domain/common/vo/YearMonthWeek.java core/infra/database/mysql/src/main/java/com/loopers/core/infra/database/mysql/product/WeeklyProductMetricQuerydslRepositoryImpl.javaRepository: Loopers-dev-lab/loopers-spring-java-template
Length of output: 722
Locale.KOREA์ Locale.getDefault() ๋ถ์ผ์น๋ก ์ธํ ์ฟผ๋ฆฌ ๋ถ์ผ์น ๋ฌธ์
YearMonthWeek.java๋ ๋ฐ์ดํฐ ์ ์ฅ ์ Locale.getDefault()๋ฅผ ์ฌ์ฉํ์ฌ weekOfYear๋ฅผ ๊ณ์ฐํ์ง๋ง, WeeklyProductMetricQuerydslRepositoryImpl.java์ ์ฟผ๋ฆฌ๋ Locale.KOREA๋ฅผ ์ฌ์ฉํ์ฌ weekOfYear๋ฅผ ๊ณ์ฐํฉ๋๋ค.
์๋ฒ์ ๊ธฐ๋ณธ ๋ก์ผ์ผ์ด ํ๊ตญ์ด ์๋ ๊ฒฝ์ฐ, ์ ์ฅ๋ ๋ฐ์ดํฐ์ ์ฟผ๋ฆฌ์ weekOfYear ๊ฐ์ด ์ผ์นํ์ง ์์ ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๊ฐ ๋ฐํ๋์ง ์์ ์ ์์ต๋๋ค. ๋ฐ์ดํฐ ์ ์ฅ๊ณผ ์กฐํ ๋ก์ง์์ ๋์ผํ ๋ก์ผ์ผ(Locale.KOREA)์ ์ฌ์ฉํ์ฌ ์ผ๊ด์ฑ์ ๋ณด์ฅํ์ธ์.
ํ์ฌ ์ฝ๋
// ์ ์ฅ ์ - YearMonthWeek.java
WeekFields weekFields = WeekFields.of(Locale.getDefault());
// ์ฟผ๋ฆฌ ์ - WeeklyProductMetricQuerydslRepositoryImpl.java
WeekFields weekFields = WeekFields.of(Locale.KOREA);๐ค Prompt for AI Agents
In
core/infra/database/mysql/src/main/java/com/loopers/core/infra/database/mysql/product/WeeklyProductMetricQuerydslRepositoryImpl.java
around lines 55-57, the query uses WeekFields.of(Locale.KOREA) while
YearMonthWeek.java (storage) uses WeekFields.of(Locale.getDefault()), causing
weekOfYear mismatches; make the locale usage consistent by changing the storage
logic (YearMonthWeek.java) to use WeekFields.of(Locale.KOREA) instead of
Locale.getDefault() so both save and query use Locale.KOREA, and run/update
tests to verify week-based lookups still return expected results.
๐ Summary
๊ตฌํ ๊ธฐ๋ฅ ๋ชฉ๋ก
๋ฉํธ๋ฆญ ์ง๊ณ
๋ฉํธ๋ฆญ์ Spring Batch๋ฅผ ํตํด ๊ตฌํํ์ต๋๋ค.
๊ธฐ์กด์ ์๋ api, streamer์์ ๊ฐ์ด ๊ตฌํํ๋ฉด ์ค์๊ฐ ๊ณ ๊ฐ ๊ฒฝํ์ ๋ด๋นํ๋ ๋ค๋ฅธ ์๋น์ค์ ๋ฐฐํฌ ์, ํน์ ๋ฌธ์ ๋ฐ์ ์ ์ํฅ์ ์ค ์ ์์ ๊ฑฐ๋ผ๊ณ ํ๋จํ์ฌ Spring Batch๋ฅผ ๋ณ๋ ์ดํ๋ฆฌ์ผ์ด์ (commerce-batch)์ผ๋ก ๊ตฌ์ฑํ์ต๋๋ค.
์ง๊ณ Job์ ๋ Step์ผ๋ก ๊ตฌ๋ถ๋ฉ๋๋ค.
Step1. ์ง๊ณํ ์ ์ฒด ์ํ ID๋ฅผ ์ฌ๋ฌ Grid๋ก ๊ตฌ๋ถํฉ๋๋ค.
Step2. ์ง๊ณ ๋ฐ์ดํฐ๋ฅผ ์ฝ๊ณ , ์ฃผ๊ฐ ์ง๊ณ, ์๊ฐ ์ง๊ณ ๋ฐ์ดํฐ๋ฅผ ์์ฑํ์ฌ ์ ์ฅํฉ๋๋ค.
์์ ๊ฐ์ด ๋ Step์ผ๋ก ๊ตฌ๋ถ์ง์ ์ด์ ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
์ผ์ผ ์ง๊ณ ๋ฐ์ดํฐ๊ฐ ์์ญ๋ง๊ฑด์ด ๋์ด๊ฐ๋ ๊ฒฝ์ฐ, Scaling์ ํตํด Step์ ๋๋์ด ๋ ๋ฆฝ์ ์ผ๋ก ์ฒ๋ฆฌํ ํ์๊ฐ ์์ด๋ณด์์ต๋๋ค.
์ด Scailing ๋ฐฉ๋ฒ ์ค
partitioner๋ฅผ ์ฌ์ฉํ์ฌ ๊ตฌํํ์ต๋๋ค. ์ฐธ๊ณ ๋ฌธ์๋ Step์ ์๋์ ๊ฐ์ด ์ค์ ํ์ต๋๋ค.
Step1. ํํฐ์ ๋
๊ฐ ์ค์ ์ ์๋์ ๊ฐ์ต๋๋ค.
partitioner: ํ์ Step์ ๋ถ๋ฆฌํ ํํฐ์ ๋๋ฅผ ์ ์ํฉ๋๋ค.step: ํ์ Step์ ์ ์ํฉ๋๋ค.taskExecutor: ํ์ Step๋ค์ ์คํ ์ํฌ Executor๋ฅผ ์ ์ํฉ๋๋ค.gridSize: ํ์ ์คํ ์ ๋๋ gridSize๋ฅผ ์ ์ํฉ๋๋ค. (์ด ๊ฐ์ด 4์ธ ๊ฒฝ์ฐ, 4๊ฐ์ Step์ผ๋ก ๋์)Step2. ์ฃผ๊ฐ ์ง๊ณ
processor๊ฐ ์ง๊ณ์ ํฐ ์๋ฏธ๊ฐ ์๋ค๊ณ ํ๋จํ์ฌ reader์ writer๋ก๋ง ๊ตฌํํ์ต๋๋ค.SynchronizedItemStreamReader๋ฅผ ์ฌ์ฉํ์ฌ ๊ตฌํํ์ต๋๋ค.TaskExecutor๋ ์์ญ๋ง๊ฑด์ ์ํ์ ๋๊ธฐ๋ก Step์ ๋๊ธฐ์ ์ผ๋ก ์คํํ๋ฉด ์ฑ๋ฅ๋ฌธ์ ๊ฐ ๋ฐ์ํ ๊ฒ์ด๋ผ๊ณ ์๊ฐํ์ฌ,
Virtual Thread ๋ฉํฐ์ฐ๋ ๋ ๊ธฐ๋ฐ์ผ๋ก ๊ตฌํํ์ต๋๋ค.
Partitioner ๊ตฌํ์ ์๋์ ๊ฐ์ต๋๋ค.
ExecutionContext์ putํฉ๋๋ค.๋ง์ฝ ์ด 2000๊ฐ๋ผ๋ฉด 500๊ฐ์ฉ ๋๋์ด ๋์ํฉ๋๋ค.
ItemReader๋ ์๋์ ๊ฐ์ด ๊ตฌํํ์ต๋๋ค.
open: Reader๊ฐ ์์๋ ๋, ํํฐ์ ๋๋ก๋ถํฐ ExecutionContext์ ๋ฐ์์ ์ฒ๋ฆฌํ ๋ฐ์ดํฐ๋ฅผ ์กฐํํฉ๋๋ค.read: ์ ์ฒด ์กฐํํ ๋ฐ์ดํฐ๋ฅผ ์ฝ์ต๋๋ค.ItemWriter๋ ์๋์ ๊ฐ์ด ๊ตฌํํฉ๋๋ค.
open: ์์์ผ๋ก๋ถํฐ ๋ช๋ , ๋ช์, ๋ช์ฃผ์ฐจ์ธ์ง๋ฅผ ๊ณ์ฐํฉ๋๋ค.์ํ ๋ญํน ์กฐํ
์ํ ๋ญํน์ type์ ๋ฐ์ ์ผ์ผ, ์ฃผ๊ฐ, ์๊ฐ์ ์กฐํํ ์ ์๋๋ก ํ์ต๋๋ค.
์ผ๊ฐ ๋ญํน์ ๊ธฐ์กด๊ณผ ๋์ดํ๊ฒ ์ค์๊ฐ์ผ๋ก redis๋ฅผ ํตํด ๋ฐ์์ค๋ฉฐ ์ฃผ๊ฐ, ์๊ฐ์ metric์ ํตํด ๊ณ์ฐํฉ๋๋ค.
์ด ๋ถ๋ถ์ ์ ๋ตํจํด์ ํตํด type๋ณ๋ก ๋ค๋ฅธ ์ ๋ต์ ์ฌ์ฉํ๋๋ก ํ์ต๋๋ค.
์ ๋ต ์ธํฐํ์ด์ค
์ผ์ผ ๋ญํน ์กฐํ
์ฃผ๊ฐ ๋ญํน ์กฐํ
์๊ฐ ๋ญํน ์กฐํ
์ ๋ต ์ ํ์
๐ฌ Review Points
processor๋ฅผ ๋จ๊ฑด์ฉ ์ฃผ๊ฐ ๋ฉํธ๋ฆญ์ ๋ง๋๋ ๊ฒ๋ณด๋คwriter์์ ์ผ๊ด๋ก ๋ง๋๋ ๊ฒ์ด ๋ ํจ์จ์ ์ด๋ผ ํ๋จํ์ฌ ๊ตฌํํ์ง ์์์ต๋๋ค. ์ด ๋ถ๋ถ์ ๋ํด์ ์ด๋ป๊ฒ ์๊ฐํ์๋์?โ Checklist
๐งฑ Spring Batch
๐งฉ Ranking API
๐ References
Summary by CodeRabbit
๋ฆด๋ฆฌ์ค ๋ ธํธ
์ ๊ธฐ๋ฅ
๋ณ๊ฒฝ ์ฌํญ
โ๏ธ Tip: You can customize this high-level summary in your review settings.