Skip to content

Conversation

@Kimjipang
Copy link
Collaborator

@Kimjipang Kimjipang commented Dec 26, 2025

๐Ÿ“Œ Summary

  • Redis ZSET์— Event Type(์กฐํšŒ/์ข‹์•„์š”/์ฃผ๋ฌธ) ๋ณ„๋กœ ๊ฐ€์ค‘์น˜๋ฅผ ๋‹ฌ๋ฆฌ ํ•˜์—ฌ score ์ ์žฌ
  • size, page๋กœ ์ธ๊ธฐ ์ƒํ’ˆ ์กฐํšŒ API ๊ตฌํ˜„
  • ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ ์‹œ ๋žญํ‚น ์ •๋ณด๋„ ํ•จ๊ป˜ ๋ฐ˜ํ™˜

๐Ÿ’ฌ Review Points

Q1)

ํ˜„์žฌ ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ์— ์บ์‹œ๋ฅผ ๊ฑธ์–ด๋‘์–ด์„œ ์„ค์ •ํ•œ ์‹œ๊ฐ„ ๋‚ด์—๋Š” outbox ํ…Œ์ด๋ธ”์— row๊ฐ€ ์Œ“์ด์ง€ ์•Š๊ธฐ์— kafka broker์— ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰์ด ์•ˆ๋˜๋Š” ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.

abusing์„ ๋ง‰๊ธฐ ์œ„ํ•ด์„œ๋Š” ์กฐํšŒํ•  ๋•Œ๋งˆ๋‹ค ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰์ด ๋˜๋ฉด ์•ˆ๋˜๊ฒ ์ง€๋งŒ, ๊ทธ๋ ‡๋‹ค๊ณ  ์บ์‹ฑ์„ ํ•˜๋Š” ๊ธฐ๊ฐ„๋™์•ˆ ๋ง‰์•„๋‘๋Š” ๊ฒƒ ๋˜ํ•œ score๊ฐ€ ์ œ๋Œ€๋กœ ์Œ“์ด์ง€๋Š” ์•Š์„ ๊ฑฐ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๋ณดํ†ต ์œ„์™€ ๊ฐ™์€ ์ƒํ™ฉ์—์„œ ์–ด๋–ป๊ฒŒ outbox์— row๋ฅผ ์Œ“๋‚˜์š”?


โœ… Checklist

๐Ÿ“ˆ Ranking Consumer

  • ๋žญํ‚น ZSET ์˜ TTL, ํ‚ค ์ „๋žต์„ ์ ์ ˆํ•˜๊ฒŒ ๊ตฌ์„ฑํ•˜์˜€๋‹ค
  • ๋‚ ์งœ๋ณ„๋กœ ์ ์žฌํ•  ํ‚ค๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค์—ˆ๋‹ค
  • ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•œ ํ›„, ZSET ์— ์ ์ˆ˜๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ๋ฐ˜์˜๋œ๋‹ค

โšพ Ranking API

  • ๋žญํ‚น Page ์กฐํšŒ ์‹œ ์ •์ƒ์ ์œผ๋กœ ๋žญํ‚น ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค
  • ๋žญํ‚น Page ์กฐํšŒ ์‹œ ๋‹จ์ˆœํžˆ ์ƒํ’ˆ ID ๊ฐ€ ์•„๋‹Œ ์ƒํ’ˆ์ •๋ณด๊ฐ€ Aggregation ๋˜์–ด ์ œ๊ณต๋œ๋‹ค
  • ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ ์‹œ ํ•ด๋‹น ์ƒํ’ˆ์˜ ์ˆœ์œ„๊ฐ€ ํ•จ๊ป˜ ๋ฐ˜ํ™˜๋œ๋‹ค (์ˆœ์œ„์— ์—†๋‹ค๋ฉด null)

๐Ÿ“Ž References

JVHE and others added 30 commits October 28, 2025 17:01
ํšŒ์› ๊ฐ€์ž…์‹œ User ์ €์žฅ์ด ์ˆ˜ํ–‰๋œ๋‹ค. ( spy ๊ฒ€์ฆ )
์ด๋ฏธ ๊ฐ€์ž…๋œ ID ๋กœ ํšŒ์›๊ฐ€์ž… ์‹œ๋„ ์‹œ, ์‹คํŒจํ•œ๋‹ค.
ํšŒ์› ๊ฐ€์ž…์ด ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
ํšŒ์› ๊ฐ€์ž… ์‹œ์— ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ํšŒ์› ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.
ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค.
๋‚ด ์ •๋ณด ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ํ•ด๋‹นํ•˜๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
์กด์žฌํ•˜์ง€ ์•Š๋Š” ID ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.
ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค.

ํฌ์ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
`X-USER-ID` ํ—ค๋”๊ฐ€ ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
0 ์ดํ•˜์˜ ์ •์ˆ˜๋กœ ํฌ์ธํŠธ๋ฅผ ์ถฉ์ „ ์‹œ ์‹คํŒจํ•œ๋‹ค.
์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ € ID ๋กœ ์ถฉ์ „์„ ์‹œ๋„ํ•œ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ๋‹ค.
์กด์žฌํ•˜๋Š” ์œ ์ €๊ฐ€ 1000์›์„ ์ถฉ์ „ํ•  ๊ฒฝ์šฐ, ์ถฉ์ „๋œ ๋ณด์œ  ์ด๋Ÿ‰์„ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
์กด์žฌํ•˜์ง€ ์•Š๋Š” ์œ ์ €๋กœ ์š”์ฒญํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
adminhelper and others added 18 commits November 19, 2025 22:58
[volume-3] ๋„๋ฉ”์ธ ๋ชจ๋ธ๋ง ๋ฐ ๊ตฌํ˜„
โ€ฆ9-revert-86-3round

Revert "Revert "[volume-3] ๋„๋ฉ”์ธ ๋ชจ๋ธ๋ง ๋ฐ ๊ตฌํ˜„""
โ€ฆ-3round

Revert "[volume-3] ๋„๋ฉ”์ธ ๋ชจ๋ธ๋ง ๋ฐ ๊ตฌํ˜„"
Round3: Product, Brand, Like, Order
โ€ฆ-round3

Revert "Round3: Product, Brand, Like, Order"
* Redis ZSET์œผ๋กœ๋ถ€ํ„ฐ ์ƒํ’ˆ์˜ ๋žญํ‚น ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.

- ์ˆœ์œ„(rank)
- ์ ์ˆ˜(score)
* ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด ์กฐํšŒ ์‹œ ๋žญํ‚น ์ •๋ณด๋„ ํ•จ๊ป˜ ์กฐํšŒํ•˜๋„๋ก ์ˆ˜์ •ํ•˜๋ฉด์„œ ๊ธฐ์กด API ์ˆ˜์ •

- ๋ฐ˜ํ™˜ํƒ€์ž… ์ˆ˜์ •
- Facade ์ˆ˜์ •
- DTO ์ˆ˜์ •
@Kimjipang Kimjipang self-assigned this Dec 26, 2025
@Kimjipang Kimjipang added the enhancement New feature or request label Dec 26, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 26, 2025

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Redis ๊ธฐ๋ฐ˜ ์ผ์ผ ์ƒํ’ˆ ์ˆœ์œ„ ์‹œ์Šคํ…œ์„ ๋„์ž…ํ•˜๊ณ , Kafka ์ปจ์Šˆ๋จธ๋ฅผ ํ†ตํ•ด ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์ˆœ์œ„ ๊ณ„์‚ฐ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. ์ƒˆ๋กœ์šด ์ˆœ์œ„ API ์—”๋“œํฌ์ธํŠธ์™€ ์ „์—ญ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด ์ถ”๊ฐ€๋˜์–ด ์ƒํ’ˆ ์กฐํšŒ ์‹œ ์‹ค์‹œ๊ฐ„ ์ˆœ์œ„ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

Changes

Cohort / File(s) ์š”์•ฝ
์ƒํ’ˆ ์ˆœ์œ„ ํ†ตํ•ฉ (Commerce API)
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java, ProductRankingInfo.java, RankingInfo.java, RankingRedisReader.java
ProductFacade์˜ findProductById ๋ฉ”์„œ๋“œ๊ฐ€ Redis์—์„œ RankingInfo๋ฅผ ์กฐํšŒํ•˜์—ฌ ProductRankingInfo ๋ฐ˜ํ™˜์œผ๋กœ ๋ณ€๊ฒฝ. RankingRedisReader ์˜์กด์„ฑ ์ถ”๊ฐ€. ์ˆœ์œ„ ๋ฐ ์ ์ˆ˜ ๋ฐ์ดํ„ฐ ํฌํ•จ.
์ƒํ’ˆ API ์‘๋‹ต ํ™•์žฅ (Commerce API)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java, ProductV1Controller.java, ProductV1Dto.java
์ƒํ’ˆ ์กฐํšŒ ์‘๋‹ต ํƒ€์ž…์„ ProductResponse์—์„œ ProductRankingResponse๋กœ ๋ณ€๊ฒฝ. ์ˆœ์œ„ ๋ฐ ์ ์ˆ˜ ํ•„๋“œ ์ถ”๊ฐ€.
์ˆœ์œ„ ์กฐํšŒ API (Commerce Streamer)
apps/commerce-streamer/src/main/java/com/loopers/application/RankingFacade.java, apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java, RankingV1Controller.java, RankingV1Dto.java
์ผ์ผ ์ƒํ’ˆ ์ˆœ์œ„ ํŽ˜์ด์ง€๋„ค์ด์…˜ ์กฐํšŒ ๊ธฐ๋Šฅ ๊ตฌํ˜„. Redis ZSet์—์„œ ์ˆœ์œ„ ๋ฐ์ดํ„ฐ ์กฐํšŒ ๋ฐ ํŽ˜์ด์ง€ ๋‹จ์œ„ ๋ฐ˜ํ™˜.
API ์‘๋‹ต ๋ฐ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ (Commerce Streamer)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiResponse.java, ApiControllerAdvice.java
ํ‘œ์ค€ํ™”๋œ API ์‘๋‹ต ๋ž˜ํผ ๋ฐ ์ „์—ญ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ํ•ธ๋“ค๋Ÿฌ ์ถ”๊ฐ€. ์—ฌ๋Ÿฌ ์˜ˆ์™ธ ํƒ€์ž…์— ๋Œ€ํ•œ ์ƒ์„ธํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ.
์—๋Ÿฌ ํƒ€์ž… ์ •์˜ (Commerce Streamer)
apps/commerce-streamer/src/main/java/com/loopers/support/error/ErrorType.java, CoreException.java
ํ‘œ์ค€ํ™”๋œ ์—๋Ÿฌ ํƒ€์ž… ์—ด๊ฑฐํ˜• ๋ฐ ์ปค์Šคํ…€ ์˜ˆ์™ธ ํด๋ž˜์Šค ์ถ”๊ฐ€. HTTP ์ƒํƒœ์™€ ์—๋Ÿฌ ์ฝ”๋“œ ๋งคํ•‘.
Kafka ์ปจ์Šˆ๋จธ ์ˆœ์œ„ ํ†ตํ•ฉ (Commerce Streamer)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/KafkaOutboxConsumer.java
Redis ์ˆœ์œ„ ์—…๋ฐ์ดํŠธ ๋กœ์ง ์ถ”๊ฐ€. ์ด๋ฒคํŠธ ํƒ€์ž…๋ณ„ ๊ฐ€์ค‘์น˜ ์ ์šฉํ•˜์—ฌ ZSET์— ์ ์ˆ˜ ๋ˆ„์ . 2์ผ ๋งŒ๋ฃŒ ์„ค์ •.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor User
    participant ProductAPI as Product API<br/>(Commerce API)
    participant ProductFacade as ProductFacade
    participant Redis as Redis
    participant RankingReader as RankingRedisReader
    
    User->>ProductAPI: GET /api/v1/products/{id}
    ProductAPI->>ProductFacade: findProductById(id)
    ProductFacade->>RankingReader: getDailyRanking(date, id)
    RankingReader->>Redis: zScore, zRevRank, zCard<br/>(ranking:all:YYYYMMDD)
    Redis-->>RankingReader: score, revRank, total
    RankingReader->>RankingReader: rank = revRank + 1
    RankingReader-->>ProductFacade: RankingInfo{date, score, rank, total}
    ProductFacade->>ProductFacade: combine Product + RankingInfo
    ProductFacade-->>ProductAPI: ProductRankingInfo
    ProductAPI-->>User: ApiResponse(ProductRankingResponse)
Loading
sequenceDiagram
    autonumber
    participant KafkaProducer as Event Producer
    participant Kafka as Kafka Broker
    participant KafkaConsumer as KafkaOutboxConsumer
    participant DB as PostgreSQL
    participant Redis as Redis
    
    KafkaProducer->>Kafka: Publish ProductViewed/Liked/<br/>SALES event
    Kafka->>KafkaConsumer: Consume message
    KafkaConsumer->>DB: Query/Upsert ProductMetric<br/>(event_type, metric_value)
    DB-->>KafkaConsumer: ProductMetric
    KafkaConsumer->>KafkaConsumer: Calculate score based<br/>on event weights<br/>(VIEWED:0.1, LIKED:0.3, SALES:0.6)
    KafkaConsumer->>Redis: ZINCRBY ranking:all:YYYYMMDD<br/>productId score
    Redis-->>KafkaConsumer: Updated score
    KafkaConsumer->>Redis: EXPIRE ranking:all:YYYYMMDD<br/>2 days
Loading
sequenceDiagram
    autonumber
    actor User
    participant RankingAPI as Ranking API<br/>(Commerce Streamer)
    participant RankingController as RankingV1Controller
    participant RankingFacade as RankingFacade
    participant Redis as Redis
    
    User->>RankingAPI: GET /api/v1/rankings?page=0&size=10
    RankingAPI->>RankingController: getDailyProductRanking(size, page)
    RankingController->>RankingFacade: getDailyProductRanking(page, size)
    RankingFacade->>Redis: ZCARD ranking:all:YYYYMMDD<br/>(get total)
    Redis-->>RankingFacade: total count
    RankingFacade->>RankingFacade: Validate & normalize<br/>page/size parameters
    RankingFacade->>Redis: ZREVRANGE ranking:all:YYYYMMDD<br/>start end WITHSCORES
    Redis-->>RankingFacade: Ranked products with scores
    RankingFacade->>RankingFacade: Build ProductRankingResponse<br/>list with rank, productId, score
    RankingFacade-->>RankingController: ProductRankingPageResponse
    RankingController->>RankingController: Wrap in ApiResponse.success()
    RankingController-->>User: ApiResponse(ProductRankingPageResponse)
Loading

Estimated code review effort

๐ŸŽฏ 4 (Complex) | โฑ๏ธ ~60 minutes

Possibly related PRs

  • [volume - 8] Decoupling with Kafkaย #195: Kafka/Outbox ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋ฐ ProductViewed ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜์˜ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜ ๋„์ž…๊ณผ ๋ณธ PR์˜ Kafka ์ปจ์Šˆ๋จธ ์ˆœ์œ„ ๊ณ„์‚ฐ ๋กœ์ง์ด ๋™์ผ ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌํ˜„.
  • Round3: Product, Brand, Like, Orderย #132: ๋™์ผํ•œ ProductFacade.findProductById ๋ฉ”์„œ๋“œ ์ˆ˜์ •์œผ๋กœ Redis ๊ธฐ๋ฐ˜ ์ˆœ์œ„ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ ๋ฐ ๋ฐ˜ํ™˜ ํƒ€์ž… ProductRankingInfo ๋ณ€๊ฒฝ.
  • round8: ์นดํ”„์นด ๋ชจ๋“ˆ ์ ์šฉ, ๋„๋ฉ”์ธ ๊ด€๋ จ ์ฝ”๋“œ core๋กœ ๋ถ„๋ฆฌย #207: Kafka โ†’ Redis ์ƒํ’ˆ ๋ฉ”ํŠธ๋ฆญ/์ˆœ์œ„ ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌํ˜„ ๋ฐ ์ˆœ์œ„ ๊ด€๋ จ ์ปดํฌ๋„ŒํŠธ(Kafka ์ปจ์Šˆ๋จธ, Redis ์ˆœ์œ„ ํ‚ค, DTO/Facade) ์ถ”๊ฐ€๋กœ ๋ณธ PR๊ณผ ๋™์ผ ๊ธฐ๋Šฅ ๊ตฌํ˜„.

Poem

๐Ÿฐ ์ˆœ์œ„์˜ ๋‚˜๋ผ๋กœ ๋– ๋‚˜๋Š” ๋ชจํ—˜,
Redis ์ €์žฅ์†Œ์— ์ถค์„ ์ถ”๋ฉฐ,
Kafka ๋ฉ”์‹œ์ง€ ํ๋ฅด๋Š” ๊ฐ•์„ ๊ฑด๋„ˆ,
์ƒํ’ˆ๋“ค ๋ณ„ ์ค‘ ์ž๋ฆฌ๋ฅผ ์ฐพ๋„ค! โญ
ํŽ˜์ด์ง€๋งˆ๋‹ค ๋ฐ˜์ง์ด๋Š” ์ˆœ์œ„๋ฅผ! โœจ

Pre-merge checks and finishing touches

โŒ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage โš ๏ธ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
โœ… Passed checks (2 passed)
Check name Status Explanation
Title check โœ… Passed PR ์ œ๋ชฉ '[volume-9] Product Ranking with Redis'๋Š” Redis๋ฅผ ํ™œ์šฉํ•œ ์ƒํ’ˆ ์ˆœ์œ„ ๊ธฐ๋Šฅ ์ถ”๊ฐ€๋ผ๋Š” ์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ์š”์•ฝํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
Description check โœ… Passed PR description์—๋Š” ์š”์•ฝ๊ณผ ๊ฒ€ํ†  ํฌ์ธํŠธ, ์ฒดํฌ๋ฆฌ์ŠคํŠธ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์œผ๋‚˜, ๋ ˆ์ด์•„์›ƒ์ด ํ…œํ”Œ๋ฆฟ ๊ตฌ์กฐ๋ฅผ ์™„์ „ํžˆ ๋”ฐ๋ฅด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

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.

โค๏ธ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

๐Ÿงน Nitpick comments (6)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/KafkaOutboxConsumer.java (3)

75-75: ๋งค ๋ฐฐ์น˜๋งˆ๋‹ค expire() ํ˜ธ์ถœ ์ตœ์ ํ™” ๊ณ ๋ ค.

ํ˜„์žฌ ๋ชจ๋“  ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์‹œ๋งˆ๋‹ค expire()๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. Redis๊ฐ€ ์ด๋ฅผ ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜์ง€๋งŒ, ํ‚ค๊ฐ€ ์ด๋ฏธ ์กด์žฌํ•  ๋•Œ๋งŒ TTL์„ ๊ฐฑ์‹ ํ•˜๋Š” ์กฐ๊ฑด๋ถ€ ๋กœ์ง์„ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜, ์ดˆ๊ธฐ ์ƒ์„ฑ ์‹œ์—๋งŒ TTL์„ ์„ค์ •ํ•˜๋Š” ๋ฐฉ์‹์„ ๊ณ ๋ คํ•ด๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


42-42: ๋ฉ”์„œ๋“œ๋ช…์ด ํ™•์žฅ๋œ ๊ธฐ๋Šฅ์„ ๋ฐ˜์˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

productViewedListener๋Š” ์ด์ œ product-viewed, product-liked, product-sales ์„ธ ๊ฐ€์ง€ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. productEventListener ๋˜๋Š” productMetricListener์™€ ๊ฐ™์ด ๋ฒ”์œ„๋ฅผ ๋ฐ˜์˜ํ•˜๋Š” ์ด๋ฆ„์œผ๋กœ ๋ณ€๊ฒฝ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.


80-86: weight() ๋ฉ”์„œ๋“œ๋ฅผ private์œผ๋กœ ๋ณ€๊ฒฝ ๊ถŒ์žฅ.

๋‚ด๋ถ€์—์„œ๋งŒ ์‚ฌ์šฉ๋˜๋Š” ํ—ฌํผ ๋ฉ”์„œ๋“œ์ด๋ฏ€๋กœ private ์ ‘๊ทผ ์ œํ•œ์ž๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์บก์Аํ™”๋ฅผ ๊ฐ•ํ™”ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ
-    double weight(ProductEventType eventType) {
+    private double weight(ProductEventType eventType) {
apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java (1)

5-8: Swagger API ๋ฌธ์„œํ™” ์–ด๋…ธํ…Œ์ด์…˜ ๋ˆ„๋ฝ.

ProductV1ApiSpec๊ณผ ๋‹ฌ๋ฆฌ @Tag, @Operation ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†์Šต๋‹ˆ๋‹ค. API ๋ฌธ์„œํ™” ์ผ๊ด€์„ฑ์„ ์œ„ํ•ด ์ถ”๊ฐ€๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆ
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;

+@Tag(name = "Ranking V1 API", description = "์ƒํ’ˆ ์ˆœ์œ„ API ์ž…๋‹ˆ๋‹ค.")
 public interface RankingV1ApiSpec {

+    @Operation(summary = "์ผ๋ณ„ ์ƒํ’ˆ ์ˆœ์œ„ ์กฐํšŒ")
     ApiResponse<RankingV1Dto.ProductRankingPageResponse> getDailyProductRanking(int size, int page);

 }
apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java (1)

19-26: ์ž…๋ ฅ ํŒŒ๋ผ๋ฏธํ„ฐ์— ๊ธฐ๋ณธ๊ฐ’ ๋ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์ถ”๊ฐ€ ๊ถŒ์žฅ

size์™€ page ํŒŒ๋ผ๋ฏธํ„ฐ์— ๊ธฐ๋ณธ๊ฐ’์ด ์—†์–ด ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ƒ๋žต ์‹œ MissingServletRequestParameterException์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ size์— ์ƒํ•œ ์ œํ•œ์ด ์—†์–ด ๋งค์šฐ ํฐ ๊ฐ’ ์š”์ฒญ ์‹œ ์„ฑ๋Šฅ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ธฐ๋ณธ๊ฐ’ ๋ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์ ์šฉ ์˜ˆ์‹œ
 @GetMapping
 @Override
 public ApiResponse<RankingV1Dto.ProductRankingPageResponse> getDailyProductRanking(
-        @RequestParam int size,
-        @RequestParam int page
+        @RequestParam(defaultValue = "20") int size,
+        @RequestParam(defaultValue = "1") int page
 ) {
+    if (size > 100) size = 100; // ์ƒํ•œ ์ œํ•œ
     RankingV1Dto.ProductRankingPageResponse response = rankingFacade.getDailyProductRanking(page, size);

     return ApiResponse.success(response);
 }
apps/commerce-streamer/src/main/java/com/loopers/application/RankingFacade.java (1)

42-44: totalPages ์ค‘๋ณต ๊ณ„์‚ฐ

totalPages๊ฐ€ ๋ผ์ธ 43๊ณผ 66์—์„œ ๋™์ผํ•˜๊ฒŒ ๊ณ„์‚ฐ๋ฉ๋‹ˆ๋‹ค. ๋ณ€์ˆ˜๋ฅผ ์•ž์—์„œ ํ•œ ๋ฒˆ๋งŒ ๊ณ„์‚ฐํ•˜๋ฉด ์ฝ”๋“œ ์ค‘๋ณต์„ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ๋ฆฌํŒฉํ„ฐ๋ง ์ œ์•ˆ
 long totalElements = (total == null) ? 0 : total;
+int totalPages = (int) Math.ceil((double) totalElements / size);

 if (totalElements == 0) {
-    return new RankingV1Dto.ProductRankingPageResponse(date, page, size, 0, 0, List.of());
+    return new RankingV1Dto.ProductRankingPageResponse(date, page, size, 0, totalPages, List.of());
 }

 long start = (long) (page - 1) * size;
 long end = start + size - 1;

 if (start >= totalElements) {
-    int totalPages = (int) Math.ceil((double) totalElements / size);
     return new RankingV1Dto.ProductRankingPageResponse(date, page, size, totalElements, totalPages, List.of());
 }
 // ... 
-int totalPages = (int) Math.ceil((double) totalElements / size);
 return new RankingV1Dto.ProductRankingPageResponse(date, page, size, totalElements, totalPages, items);

Also applies to: 66-66

๐Ÿ“œ Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between 5e43c2a and dcab5ea.

๐Ÿ“’ Files selected for processing (16)
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductRankingInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/RankingInfo.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/RankingRedisReader.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java
  • apps/commerce-streamer/src/main/java/com/loopers/application/RankingFacade.java
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiResponse.java
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1ApiSpec.java
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Controller.java
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/KafkaOutboxConsumer.java
  • apps/commerce-streamer/src/main/java/com/loopers/support/error/CoreException.java
  • apps/commerce-streamer/src/main/java/com/loopers/support/error/ErrorType.java
๐Ÿงฐ Additional context used
๐Ÿง  Learnings (6)
๐Ÿ“š Learning: 2025-11-27T09:09:24.961Z
Learnt from: sky980221
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 121
File: apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java:22-24
Timestamp: 2025-11-27T09:09:24.961Z
Learning: Product ์—”ํ‹ฐํ‹ฐ (apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java)๋Š” ์œ ์ฆˆ์ผ€์ด์Šค๋ณ„๋กœ ์˜๋„์ ์œผ๋กœ ๋‹ค๋ฅธ ๋ฝ ์ „๋žต์„ ์‚ฌ์šฉํ•œ๋‹ค: ์ข‹์•„์š” ๊ธฐ๋Šฅ์—๋Š” ๋น„๊ด€์  ๋ฝ(findByIdForUpdate)์„, ์žฌ๊ณ  ์ฐจ๊ฐ์—๋Š” ๋‚™๊ด€์  ๋ฝ(Version + ์žฌ์‹œ๋„)์„ ์‚ฌ์šฉํ•œ๋‹ค.

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
๐Ÿ“š Learning: 2025-12-19T09:30:12.459Z
Learnt from: HongChangMo
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 198
File: apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentEventListener.java:0-0
Timestamp: 2025-12-19T09:30:12.459Z
Learning: In the loopers-spring-java-template repository's commerce-api module, when entities are managed within a transactional context (e.g., Transactional methods), prefer relying on JPA dirty checking for automatic persistence rather than explicit save() calls. Both Payment and Order entities in PaymentEventListener use this pattern, with state changes automatically flushed on transaction commit.

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
๐Ÿ“š Learning: 2025-12-19T20:59:57.713Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 68
File: docs/week8/round8-detailed-design.md:151-178
Timestamp: 2025-12-19T20:59:57.713Z
Learning: In the Loopers-dev-lab/loopers-spring-kotlin-template repository's Kafka event pipeline, only 5 domain events are intentionally published to Kafka via CloudEventEnvelopeFactory: OrderPaidEventV1, LikeCreatedEventV1, LikeCanceledEventV1, ProductViewedEventV1, and StockDepletedEventV1. Other domain events (OrderCreatedEventV1, OrderCanceledEventV1, PaymentCreatedEventV1, PaymentPaidEventV1, PaymentFailedEventV1) are internal-only and intentionally not mapped in resolveMetadata(), which correctly returns null for them to exclude them from Outbox publication.

Applied to files:

  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/KafkaOutboxConsumer.java
๐Ÿ“š Learning: 2025-12-18T13:24:51.650Z
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 190
File: apps/commerce-streamer/src/main/java/com/loopers/applications/streamer/consumer/product/IncreaseProductViewKafkaConsumer.java:25-35
Timestamp: 2025-12-18T13:24:51.650Z
Learning: Adopt centralized error handling for Kafka consumers by using the EventInboxAspect to intercept methods annotated with InboxEvent. Ensure that service methods annotated with InboxEvent save failed EventInbox entries and log errors, avoiding duplicating error handling logic in individual consumers. Apply this pattern broadly to similar consumer/service layers within the commerce-streamer module.

Applied to files:

  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/KafkaOutboxConsumer.java
๐Ÿ“š Learning: 2025-12-19T23:39:20.851Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 68
File: apps/commerce-streamer/src/main/kotlin/com/loopers/interfaces/consumer/product/ProductEventConsumer.kt:0-0
Timestamp: 2025-12-19T23:39:20.851Z
Learning: In Loopers-dev-lab/loopers-spring-kotlin-template, toongri prefers pragmatic idempotency handling in Kafka consumers: when idempotency key persistence fails after successful business logic execution, log a warning and continue (accepting low risk of duplicates on retry) rather than rolling back business data. This keeps business logic decoupled from idempotency store (which might be Redis/external system, not RDB), avoids over-engineering for rare edge cases, and prioritizes core business value over auxiliary metadata. Transaction-based atomicity between business data and idempotency keys is considered excessive when duplicate event probability is very low.

Applied to files:

  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/KafkaOutboxConsumer.java
๐Ÿ“š Learning: 2025-12-19T21:30:16.024Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 68
File: apps/commerce-api/src/main/kotlin/com/loopers/infrastructure/outbox/OutboxEventListener.kt:0-0
Timestamp: 2025-12-19T21:30:16.024Z
Learning: In the Loopers-dev-lab/loopers-spring-kotlin-template Kafka event pipeline, Like events (LikeCreatedEventV1, LikeCanceledEventV1) intentionally use aggregateType="Like" with aggregateId=productId. The aggregateId serves as a partitioning/grouping key (not a unique Like entity identifier), ensuring all like events for the same product go to the same partition for ordering guarantees and aligning with ProductStatisticService's product-based aggregation logic. Using individual like_id would scatter events across partitions and break the statistics aggregation pattern.

Applied to files:

  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/KafkaOutboxConsumer.java
๐Ÿงฌ Code graph analysis (6)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java (1)
  • ProductV1Dto (11-74)
apps/commerce-streamer/src/main/java/com/loopers/application/RankingFacade.java (1)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java (1)
  • RankingV1Dto (5-20)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiResponse.java (1)
apps/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiResponse.kt (3)
  • meta (3-32)
  • result (7-19)
  • SUCCESS (12-12)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/KafkaOutboxConsumer.java (1)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/ProductMetricRepositoryImpl.java (1)
  • Component (9-23)
apps/commerce-streamer/src/main/java/com/loopers/support/error/CoreException.java (1)
apps/pg-simulator/src/main/kotlin/com/loopers/support/error/CoreException.kt (1)
  • errorType (3-6)
apps/commerce-streamer/src/main/java/com/loopers/support/error/ErrorType.java (2)
apps/commerce-streamer/src/main/java/com/loopers/support/error/CoreException.java (1)
  • Getter (5-19)
apps/pg-simulator/src/main/kotlin/com/loopers/support/error/ErrorType.kt (1)
  • status (5-11)
๐Ÿ”‡ Additional comments (14)
apps/commerce-streamer/src/main/java/com/loopers/support/error/ErrorType.java (1)

7-19: LGTM!

pg-simulator ๋ชจ๋“ˆ์˜ Kotlin ๋ฒ„์ „ ErrorType๊ณผ ์ผ๊ด€๋œ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค. Lombok์„ ํ™œ์šฉํ•œ ๊น”๋”ํ•œ enum ๊ตฌํ˜„์ด๋ฉฐ, ์—๋Ÿฌ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ(์ƒํƒœ, ์ฝ”๋“œ, ๋ฉ”์‹œ์ง€)๊ฐ€ ์ž˜ ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Controller.java (1)

44-51: API ์‘๋‹ต ํƒ€์ž… ๋ณ€๊ฒฝ์œผ๋กœ ์ธํ•œ ํ•˜์œ„ ํ˜ธํ™˜์„ฑ ๊ฒ€ํ†  ํ•„์š”.

findProductById ์—”๋“œํฌ์ธํŠธ๊ฐ€ ProductResponse์—์„œ ProductRankingResponse๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ์กด ์‘๋‹ต์— ์žˆ๋˜ likeCount ํ•„๋“œ๊ฐ€ ์ œ๊ฑฐ๋˜๊ณ  rank, score ํ•„๋“œ๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. API ์†Œ๋น„์ž์—๊ฒŒ breaking change๊ฐ€ ๋  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ํ™•์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1ApiSpec.java (1)

17-18: LGTM!

์ปจํŠธ๋กค๋Ÿฌ ๊ตฌํ˜„๊ณผ ์ผ๊ด€๋˜๊ฒŒ ์ธํ„ฐํŽ˜์ด์Šค ์ŠคํŽ™์ด ์—…๋ฐ์ดํŠธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/application/product/RankingInfo.java (1)

3-8: LGTM!

์ˆœ์œ„ ์ •๋ณด๋ฅผ ๋‹ด๋Š” immutable record๋กœ ์ ์ ˆํ•˜๊ฒŒ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. date๋ฅผ String ๋Œ€์‹  LocalDate๋กœ ์‚ฌ์šฉํ•˜๋ฉด ํƒ€์ž… ์•ˆ์ „์„ฑ์ด ํ–ฅ์ƒ๋˜์ง€๋งŒ, Redis์™€์˜ ์—ฐ๋™ ์‹œ ๋ฌธ์ž์—ด ๋ณ€ํ™˜์ด ํ•„์š”ํ•˜๋ฏ€๋กœ ํ˜„์žฌ ๊ตฌ์กฐ๋„ ํ•ฉ๋ฆฌ์ ์ž…๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/interfaces/api/product/ProductV1Dto.java (1)

12-24: likeCount ํ•„๋“œ ๋ˆ„๋ฝ ํ™•์ธ ํ•„์š”.

ProductRankingInfo์—๋Š” likeCount ํ•„๋“œ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์ง€๋งŒ, ProductRankingResponse์—๋Š” ๋งคํ•‘๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์˜๋„์ ์ธ ์„ค๊ณ„๋ผ๋ฉด ๋ฌธ์ œ์—†์ง€๋งŒ, ๊ธฐ์กด ProductResponse์™€์˜ ์ผ๊ด€์„ฑ ์ธก๋ฉด์—์„œ ํ™•์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/application/product/ProductRankingInfo.java (1)

7-19: LGTM!

Product์™€ RankingInfo๋ฅผ ๊ฒฐํ•ฉํ•˜๋Š” ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ๊ฐ€ ๊น”๋”ํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. RankingInfo๊ฐ€ null์ผ ๊ฒฝ์šฐ์˜ ์ฒ˜๋ฆฌ๊ฐ€ ํ˜ธ์ถœ ์ธก์—์„œ ๋ณด์žฅ๋˜๋Š”์ง€ ํ™•์ธ์ด ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ranking/RankingV1Dto.java (1)

5-19: LGTM!

DTO ๋ ˆ์ฝ”๋“œ ๊ตฌ์กฐ๊ฐ€ ์ ์ ˆํ•˜๋ฉฐ, ํŽ˜์ด์ง• ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์™€ ๋žญํ‚น ์‘๋‹ต ๊ตฌ์กฐ๊ฐ€ ์ž˜ ์„ค๊ณ„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/application/product/RankingRedisReader.java (1)

24-30: Redis ํŒŒ์ดํ”„๋ผ์ธ ์‚ฌ์šฉ ๋ฐฉ์‹ ์ ์ ˆํ•จ

3๊ฐœ์˜ Redis ๋ช…๋ น์„ ๋‹จ์ผ ํŒŒ์ดํ”„๋ผ์ธ์œผ๋กœ ์‹คํ–‰ํ•˜์—ฌ ๋„คํŠธ์›Œํฌ ๋ผ์šด๋“œํŠธ๋ฆฝ์„ ์ตœ์†Œํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค. ํšจ์œจ์ ์ธ ๊ตฌํ˜„์ž…๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/support/error/CoreException.java (1)

5-18: LGTM!

CoreException ๊ตฌํ˜„์ด pg-simulator์˜ Kotlin ๋ฒ„์ „๊ณผ ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ƒ์„ฑ์ž ์œ„์ž„ ํŒจํ„ด๊ณผ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java (2)

56-57: @Transactional๊ณผ @Cacheable ์กฐํ•ฉ ์‹œ ์“ฐ๊ธฐ ์ž‘์—… ์ฃผ์˜

์ด ๋ฉ”์„œ๋“œ๋Š” ์บ์‹œ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ OutboxEvent๋ฅผ ์ €์žฅํ•˜๋Š” ์“ฐ๊ธฐ ์ž‘์—…๋„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์บ์‹œ ํžˆํŠธ ์‹œ OutboxEvent๊ฐ€ ์ €์žฅ๋˜์ง€ ์•Š์•„ ์กฐํšŒ ์ด๋ฒคํŠธ๊ฐ€ ๋ˆ„๋ฝ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์บ์‹œ ํžˆํŠธ ์‹œ์—๋„ ์กฐํšŒ ์ด๋ฒคํŠธ ๋ฐœํ–‰์ด ํ•„์š”ํ•œ์ง€ ํ™•์ธํ•ด ์ฃผ์„ธ์š”. ํ•„์š”ํ•˜๋‹ค๋ฉด ์บ์‹œ ๋กœ์ง๊ณผ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ๋กœ์ง์„ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.


74-74: ํƒ€์ž„์กด ๋ถˆ์ผ์น˜ ๋ฌธ์ œ

RankingFacade๋Š” ZoneId.of("Asia/Seoul")์„ ์‚ฌ์šฉํ•˜์ง€๋งŒ, ์—ฌ๊ธฐ์„œ๋Š” ์‹œ์Šคํ…œ ๊ธฐ๋ณธ ํƒ€์ž„์กด์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์„œ๋ฒ„ ํƒ€์ž„์กด ์„ค์ •์— ๋”ฐ๋ผ ๋‚ ์งœ ํ‚ค๊ฐ€ ๋ถˆ์ผ์น˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ผ๊ด€๋œ ํƒ€์ž„์กด ์‚ฌ์šฉ
+private static final ZoneId KST = ZoneId.of("Asia/Seoul");
+
 try {
-    String date = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
+    String date = LocalDate.now(KST).format(DateTimeFormatter.BASIC_ISO_DATE);
     ranking = rankingRedisReader.getDailyRanking(date, product.getId());
โ›” Skipped due to learnings
Learnt from: jikimee64
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 71
File: apps/commerce-streamer/src/main/kotlin/com/loopers/application/ranking/RankingFacade.kt:167-173
Timestamp: 2025-12-22T16:33:50.678Z
Learning: In the Loopers-dev-lab/loopers-spring-kotlin-template repository, using system default timezone (ZonedDateTime.now() without explicit ZoneId) in date key calculations is an intentional design choice. The deployment ensures all instances share the same timezone configuration.
apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiResponse.java (1)

3-31: LGTM!

ApiResponse ๊ตฌํ˜„์ด pg-simulator์˜ Kotlin ๋ฒ„์ „๊ณผ ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. Java record๋ฅผ ํ™œ์šฉํ•œ ๊น”๋”ํ•œ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java (2)

26-30: CoreException ํ•ธ๋“ค๋Ÿฌ ๊ตฌํ˜„ ์ ์ ˆํ•จ

CoreException์„ ์ ์ ˆํžˆ ์ฒ˜๋ฆฌํ•˜๊ณ  ErrorType์˜ ์ƒํƒœ ์ฝ”๋“œ์™€ ๋ฉ”์‹œ์ง€๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ผ๊ด€๋œ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.


49-92: JSON ํŒŒ์‹ฑ ์˜ค๋ฅ˜ ์ƒ์„ธ ์ฒ˜๋ฆฌ ์šฐ์ˆ˜

InvalidFormatException, MismatchedInputException, JsonMappingException์„ ๊ตฌ๋ถ„ํ•˜์—ฌ ํ•„๋“œ ๊ฒฝ๋กœ์™€ ์˜ˆ์ƒ ํƒ€์ž… ๋“ฑ ์ƒ์„ธํ•œ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ํ–ฅ์ƒ์— ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค.

Comment on lines +73 to +76
try {
String date = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
ranking = rankingRedisReader.getDailyRanking(date, product.getId());
} catch (Exception ignored) {}
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

์˜ˆ์™ธ๋ฅผ ๋ฌด์‹œํ•˜๋Š” catch (Exception ignored) ํŒจํ„ด ๊ฐœ์„  ํ•„์š”

๋ชจ๋“  ์˜ˆ์™ธ๋ฅผ ์กฐ์šฉํžˆ ์‚ผํ‚ค๋ฉด Redis ์—ฐ๊ฒฐ ์‹คํŒจ, ์ง๋ ฌํ™” ์˜ค๋ฅ˜ ๋“ฑ ์ค‘์š”ํ•œ ๋ฌธ์ œ๋ฅผ ๋””๋ฒ„๊น…ํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค. ์ตœ์†Œํ•œ ๋กœ๊น…์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”.

๐Ÿ”Ž ๋กœ๊น… ์ถ”๊ฐ€ ์˜ˆ์‹œ
 try {
     String date = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
     ranking = rankingRedisReader.getDailyRanking(date, product.getId());
-} catch (Exception ignored) {}
+} catch (Exception e) {
+    log.warn("Failed to fetch ranking for product {}: {}", product.getId(), e.getMessage());
+}
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java
around lines 73 to 76, replace the silent catch block "catch (Exception ignored)
{}" with an explicit catch that logs the error and relevant context (product id
and date) and only swallows expected recoverable exceptions; e.g., catch
Exception as e -> use the class logger to log at warn/error with a message like
"Failed to read daily ranking for productId=<id> date=<date>" and include the
exception, or narrow to specific exceptions (e.g., Redis or serialization
exceptions) if appropriate, so failures are visible while preserving the
existing fallback behavior.

Long revRank0 = (Long) results.get(1);
Long total = (Long) results.get(2);

Integer rank = (revRank0 == null) ? null : Math.toIntExact(revRank0 + 1);
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

Math.toIntExact ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ ๊ฐ€๋Šฅ์„ฑ

revRank0๊ฐ€ Integer.MAX_VALUE๋ฅผ ์ดˆ๊ณผํ•˜๋ฉด ArithmeticException์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์ƒํ’ˆ ์ˆ˜๊ฐ€ ๋งŽ์€ ํ™˜๊ฒฝ์—์„œ๋Š” ์•ˆ์ „ํ•œ ๋ณ€ํ™˜์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์•ˆ์ „ํ•œ ๋ณ€ํ™˜ ์ œ์•ˆ
-Integer rank = (revRank0 == null) ? null : Math.toIntExact(revRank0 + 1);
+Integer rank = (revRank0 == null) ? null : (int) Math.min(revRank0 + 1, Integer.MAX_VALUE);
๐Ÿ“ 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
Integer rank = (revRank0 == null) ? null : Math.toIntExact(revRank0 + 1);
Integer rank = (revRank0 == null) ? null : (int) Math.min(revRank0 + 1, Integer.MAX_VALUE);
๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/product/RankingRedisReader.java
around line 36, the use of Math.toIntExact(revRank0 + 1) can throw
ArithmeticException if revRank0 + 1 exceeds Integer.MAX_VALUE; replace this with
a safe conversion that first checks for null, computes revRank0 + 1 as a long,
and clamps the result to Integer.MAX_VALUE (or Integer.MIN_VALUE if negative
underflow is possible), then cast to int; also optionally log or metric the
clamping event for observability.

Comment on lines +58 to +62
items.add(new RankingV1Dto.ProductRankingResponse(
rank++,
Long.parseLong(member),
score
));
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

Long.parseLong ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ˆ„๋ฝ

Redis์— ์ž˜๋ชป๋œ ํ˜•์‹์˜ member ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•  ๊ฒฝ์šฐ NumberFormatException์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐฉ์–ด์  ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ์ถ”๊ฐ€ ์˜ˆ์‹œ
 for (var t : tuples) {
     String member = t.getValue();
     Double score = t.getScore();
     if (member == null || score == null) continue;

+    Long productId;
+    try {
+        productId = Long.parseLong(member);
+    } catch (NumberFormatException e) {
+        continue; // ์ž˜๋ชป๋œ ๋ฐ์ดํ„ฐ๋Š” ์Šคํ‚ต
+    }
+
     items.add(new RankingV1Dto.ProductRankingResponse(
             rank++,
-            Long.parseLong(member),
+            productId,
             score
     ));
 }
๐Ÿ“ 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
items.add(new RankingV1Dto.ProductRankingResponse(
rank++,
Long.parseLong(member),
score
));
for (var t : tuples) {
String member = t.getValue();
Double score = t.getScore();
if (member == null || score == null) continue;
Long productId;
try {
productId = Long.parseLong(member);
} catch (NumberFormatException e) {
continue; // ์ž˜๋ชป๋œ ๋ฐ์ดํ„ฐ๋Š” ์Šคํ‚ต
}
items.add(new RankingV1Dto.ProductRankingResponse(
rank++,
productId,
score
));
}
๐Ÿค– Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/application/RankingFacade.java
around lines 58 to 62, parsing Redis member with Long.parseLong may throw
NumberFormatException for malformed values; wrap the parse in a try-catch that
catches NumberFormatException, log a warning (including the offending member
value and context), and skip that entry (do not add a ProductRankingResponse) so
processing continues for other members; ensure the counter/rank logic remains
consistent when skipping invalid members.

Comment on lines 94 to 103
@ExceptionHandler
public ResponseEntity<ApiResponse<?>> handleBadRequest(ServerWebInputException e) {
String missingParams = extractMissingParameter(e.getReason() != null ? e.getReason() : "");
if (!missingParams.isEmpty()) {
String message = String.format("ํ•„์ˆ˜ ์š”์ฒญ ๊ฐ’ '%s'๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", missingParams);
return failureResponse(ErrorType.BAD_REQUEST, message);
} else {
return failureResponse(ErrorType.BAD_REQUEST, null);
}
}
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:

#!/bin/bash

echo "=== Build Configuration Files ==="
fd -e gradle -e xml -e properties | head -20

echo ""
echo "=== Spring Dependencies in build.gradle ==="
fd build.gradle | xargs cat 2>/dev/null

echo ""
echo "=== Checking pom.xml ==="
fd pom.xml | xargs cat 2>/dev/null | head -100

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

Length of output: 9932


๐Ÿ Script executed:

#!/bin/bash

echo "=== Checking ApiControllerAdvice.java imports and class definition ==="
cat apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java | head -20

echo ""
echo "=== Checking if ServerWebInputException is used ==="
rg "ServerWebInputException" apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java

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

Length of output: 1387


ServerWebInputException์€ WebFlux ์ „์šฉ ์˜ˆ์™ธ๋กœ ์ด ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ํ˜ธ์ถœ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค

์ด ํ”„๋กœ์ ํŠธ๋Š” spring-boot-starter-web์„ ์‚ฌ์šฉํ•˜๋Š” Spring MVC(Servlet ๊ธฐ๋ฐ˜) ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ž…๋‹ˆ๋‹ค. ServerWebInputException์€ Spring WebFlux์—์„œ๋งŒ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ์ด๋ฏ€๋กœ ์ด ํ•ธ๋“ค๋Ÿฌ(94-103์ค„)๋Š” ์ ˆ๋Œ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š๋Š” ๋ฐ๋“œ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค.

Servlet MVC์—์„œ ์š”์ฒญ ๋งค๊ฐœ๋ณ€์ˆ˜ ์˜ค๋ฅ˜๋ฅผ ์ฒ˜๋ฆฌํ•˜๋ ค๋ฉด MissingServletRequestParameterException(์ด๋ฏธ ๊ตฌํ˜„๋จ) ๋˜๋Š” HttpMessageNotReadableException(์ด๋ฏธ ๊ตฌํ˜„๋จ)์„ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค. ์ด ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ œ๊ฑฐํ•˜๊ฑฐ๋‚˜, WebFlux ํ™˜๊ฒฝ์ด ์ถ”๊ฐ€๋˜๋Š” ๊ฒฝ์šฐ์—๋งŒ ๋ณ„๋„ ๋ชจ๋“ˆ์— ๋ฐฐ์น˜ํ•˜์„ธ์š”.

๐Ÿค– Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java
around lines 94-103, the handler for ServerWebInputException is dead code
because this project uses Spring MVC (servlet) not WebFlux; remove this
exception handler (or move it to a separate WebFlux-specific module) and rely on
the existing MissingServletRequestParameterException and
HttpMessageNotReadableException handlers to handle servlet request binding
errors instead.

Comment on lines 41 to 45
@Transactional
public void productViewedListener(
List<ConsumerRecord<String, String>> messages,
Acknowledgment acknowledgment
) throws JsonProcessingException {
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

๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์ค‘ ๋‹จ์ผ ๋ฉ”์‹œ์ง€ ์‹คํŒจ ์‹œ ์ „์ฒด ๋ฐฐ์น˜๊ฐ€ ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค.

JsonProcessingException์ด for ๋ฃจํ”„ ๋ฐ”๊นฅ์—์„œ throw๋˜๋ฏ€๋กœ, ํ•˜๋‚˜์˜ ๋ฉ”์‹œ์ง€ ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ์ „์ฒด ๋ฐฐ์น˜๊ฐ€ ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ฐœ๋ณ„ ๋ฉ”์‹œ์ง€ ์‹คํŒจ๋ฅผ ๊ฒฉ๋ฆฌํ•˜์—ฌ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ฐœ์„  ์ œ์•ˆ
     @Transactional
     public void productViewedListener(
             List<ConsumerRecord<String, String>> messages,
             Acknowledgment acknowledgment
-    ) throws JsonProcessingException {
+    ) {

         Map<Long, Double> scoreDelta = new HashMap<>();

         for (var record : messages) {
-            OutboxEvent value = objectMapper.readValue(record.value(), OutboxEvent.class);
+            try {
+                OutboxEvent value = objectMapper.readValue(record.value(), OutboxEvent.class);
+                // ... process message
+            } catch (JsonProcessingException e) {
+                log.error("Failed to parse message: {}", record.value(), e);
+                // ๊ฐœ๋ณ„ ๋ฉ”์‹œ์ง€ ์‹คํŒจ ์‹œ ๋กœ๊น… ํ›„ ๊ณ„์† ์ฒ˜๋ฆฌ
+            }
         }

Committable suggestion skipped: line range outside the PR's diff.

๐Ÿค– Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/KafkaOutboxConsumer.java
around lines 41-45, the method currently declares and can throw
JsonProcessingException outside the message loop so a single bad message aborts
the whole transactional batch; change the handling so JSON parsing and
processing for each ConsumerRecord is wrapped in its own try/catch that catches
JsonProcessingException (and other per-message errors), logs the failure (and/or
emits metrics or forwards the raw message to a DLQ), and continues processing
the remaining messages; ensure transactional boundaries are adjusted so
successful messages are not rolled back by a single-message failure (e.g.,
perform per-message processing in a separate transaction or remove the
method-level @Transactional and manage transactions per message) and do not
rethrow JsonProcessingException from the outer method.

Kimjipang and others added 7 commits December 28, 2025 02:00
**AS-IS**
commerce-streamer์—์„œ ๋žญํ‚น ์ •๋ณด API ์ •์˜

**TO-BE**
commerce-streamer์—์„œ๋Š” ์ง‘๊ณ„๋งŒ ํ•˜๋„๋ก ํ•˜๊ธฐ ์œ„ํ•ด commerce-api ๋ชจ๋“ˆ๋กœ ๋žญํ‚น ์ •๋ณด API ์ˆ˜์ •
refactor: ๋žญํ‚น ์ •๋ณด API path ์ˆ˜์ •
- ์ฃผ๊ฐ„/์›”๊ฐ„ ๋žญํ‚น ์ •๋ณด ์ง‘๊ณ„๋ฅผ ์šฉ์ดํ•˜๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด metric_date ์ปฌ๋Ÿผ ์ถ”๊ฐ€
refactor: product_metrics ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ˆ˜์ •
@Kimjipang Kimjipang merged commit 5a4975e into Loopers-dev-lab:Kimjipang Jan 1, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants