Skip to content

Conversation

@rkdalsdn94
Copy link
Collaborator

@rkdalsdn94 rkdalsdn94 commented Dec 18, 2025

๐Ÿ“Œ Summary

์š”์•ฝ

Kafka ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌํ˜„

๊ตฌํ˜„ ์‚ฌํ•ญ

1. Producer

  • Transactional Outbox ํŒจํ„ด ๊ตฌํ˜„
    • ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ๋™์ผํ•œ ํŠธ๋žœ์žญ์…˜ ๋‚ด์—์„œ event_outbox ํ…Œ์ด๋ธ” ํ™œ์šฉ
    • ์Šค์ผ€์ค„๋Ÿฌ(OutboxEventPublisher)๊ฐ€ ์ฃผ๊ธฐ์ ์œผ๋กœ PENDING ์ƒํƒœ ์ด๋ฒคํŠธ๋ฅผ Kafka๋กœ ์ „๋‹ฌ
    • Kafka ์žฅ์•  ์ƒํ™ฉ์—๋„ ์ด๋ฒคํŠธ ์œ ์‹ค ๋ฐฉ์ง€
  • Partition Key ๊ธฐ๋ฐ˜ ์ˆœ์„œ ๋ณด์žฅ
    • aggregateId๋ฅผ Partition Key๋กœ ์‚ฌ์šฉํ•˜์—ฌ ๋™์ผ ์—”ํ‹ฐํ‹ฐ์˜ ์ด๋ฒคํŠธ ์ˆœ์„œ ๋ณด์žฅ
    • ์˜ˆ: ๊ฐ™์€ ์ƒํ’ˆ์˜ ์ข‹์•„์š” ์ƒ์„ฑ/์ทจ์†Œ ์ด๋ฒคํŠธ๊ฐ€ ์ˆœ์„œ๋Œ€๋กœ ์ฒ˜๋ฆฌ๋จ

2. Consumer

  • Inbox ํŒจํ„ด์œผ๋กœ ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ
  • Batch Processing with Manual Commit
    • max.poll.records=100์œผ๋กœ ๋ฐฐ์น˜ ๋‹จ์œ„ ์ฒ˜๋ฆฌ
    • ๋ฐฐ์น˜ ๋‚ด ๋ชจ๋“  ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ํ›„ ์ˆ˜๋™ Offset Commit
    • ์ฒ˜๋ฆฌ ์ค‘ ์‹คํŒจํ•œ ๋ฉ”์‹œ์ง€๋Š” DLQ๋กœ ์ „์†ก
  • Dead Letter Queue (DLQ)
    • ์ฒ˜๋ฆฌํ•˜์ง€ ๋ชปํ•œ ๋ฉ”์‹œ์ง€๋ฅผ dead_letter_queue ํ…Œ์ด๋ธ”์— ์ €์žฅ

3. ์บ์‹œ ๋ฌดํšจํ™” ์ „๋žต

  • ์žฌ๊ณ  ์†Œ์ง„ ์‹œ ์ƒํ’ˆ ์บ์‹œ ๋ฌดํšจํ™” (OrderFacade์—์„œ product.getStock() == 0์ผ ๋•Œ ๋ฌดํšจํ™”)

4. Embbed Kafka๋ฅผ ํ†ตํ•œ ์นดํ”„์นด ํ•™์Šต ํ…Œ์ŠคํŠธ

  • modules/kafka/src/test/java/com/loopers/kafka/learning/ ํŒจํ‚ค์ง€์— ํ•™์Šต ํ…Œ์ŠคํŠธ ์ž‘์„ฑ
      1. Auto Commit vs Manual Commit ๋น„๊ต
      1. earliest vs latest ๋™์ž‘ ํ•™์Šต
      1. max.poll.interval.ms ํƒ€์ž„์•„์›ƒ
      1. ๋ฐฐ์น˜ ํฌ๊ธฐ์— ๋”ฐ๋ฅธ ์„ฑ๋Šฅ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„ ํ…Œ์ŠคํŠธ
    • ์‹ค์ œ ์นดํ”„์นด๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๋ฐฉ์‹์ด ์•„๋‹Œ, Spring ๋‚ด๋ถ€ Embbed Kafka๋กœ ํ…Œ์ŠคํŠธ ์ง„ํ–‰
      ๊ฐ„๋‹จํ•œ ์„ค์ • ํ…Œ์ŠคํŠธ๋ผ๊ณ  ํŒ๋‹จํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ด ๋ฐฉ์‹์„ ์„ ํƒํ•จ

๐Ÿ’ฌ Review Points

Q. ์™œ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋ง ํ…Œ์ด๋ธ”๊ณผ ๋กœ๊ทธ ํ…Œ์ด๋ธ”์„ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฑธ๊นŒ? ์— ๋Œ€ํ•œ ์ €์˜ ์ƒ๊ฐ์ž…๋‹ˆ๋‹ค.

์ €๋Š” ํ•ธ๋“ค๋ง ํ…Œ์ด๋ธ”๊ณผ ๋กœ๊ทธ ํ…Œ์ด๋ธ”์„ ๊ฐ๊ฐ inbox ํ…Œ์ด๋ธ”๊ณผ outbox ํ…Œ์ด๋ธ”๋กœ ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ์€ ์ œ๊ฐ€ ์ƒ๊ฐํ•˜๋Š” ๋ถ„๋ฆฌํ•˜๋Š” ์ด์œ  ์ž…๋‹ˆ๋‹ค.

1. ๋‘ ํ…Œ์ด๋ธ”์€ ์‚ฌ์šฉํ•˜๋Š” ๋ชฉ์ ์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค.

inbox๋Š” ์ด ์ด๋ฒคํŠธ๋ฅผ ์ด๋ฏธ ์ฒ˜๋ฆฌํ–ˆ๋Š”์ง€์— ๋Œ€ํ•œ ํŒ๋‹จ์„ ํ•ฉ๋‹ˆ๋‹ค.๋”ฐ๋ผ์„œ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์ง์ „์—
์ค‘๋ณต๋œ ์ด๋ฒคํŠธ์ธ๊ฐ€๋ฅผ ์‚ดํŽด๋ณด๊ณ  ์ค‘๋ณต ๋ฐฉ์ง€ ๋ชฉ์ ์ด ๋๋‚˜๋ฉด ์‚ญ์ œํ•˜๋Š” ๋ฐ์ดํ„ฐ ์ž…๋‹ˆ๋‹ค. (ex. 10์ผ์ด ์ง€๋‚˜๋ฉด ์‚ญ์ œ)

outbox๋Š” ์ด ์ด๋ฒคํŠธ๋ฅผ MQ(์—ฌ๊ธฐ์„  Kafka)์— ๋ฐœํ–‰ํ–ˆ๋Š”์ง€์— ๋Œ€ํ•œ ๊ธฐ๋ก์ž…๋‹ˆ๋‹ค.
๋”ฐ๋ผ์„œ ์ด๋ฒคํŠธ ๋ฐœํ–‰์— ๋Œ€ํ•ด ๋ฐœํ–‰ํ–ˆ๋Š”์ง€, ์–ธ์ œ, ์–ด๋–ค ๋‚ด์šฉ์œผ๋กœ ๋ฐœํ–‰ํ–ˆ๋Š”์ง€์— ๋Œ€ํ•œ ๋ชฉ์ ์„ ๊ฐ€์ง‘๋‹ˆ๋‹ค.

2. ๋ฐ์ดํ„ฐ ๋ณด์กด ๊ธฐ๊ฐ„์˜ ์ฐจ์ด

inbox์˜ ๋ชฉ์ ์€ ์ค‘๋ณต๋งŒ ๋ฐฉ์ง€ํ•˜๋ฉด ๋˜๋ฏ€๋กœ ์ตœ๊ทผ ์ด๋ฒคํŠธ๋งŒ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
ํ•˜์ง€๋งŒ, outbox๋Š” ์ด๋ฒคํŠธ์— ๋Œ€ํ•ด ๊ฐ์‚ฌ ๋กœ๊ทธ ์—ญํ• ๋„ ๊ฒธํ•˜๋ฏ€๋กœ ์žฅ๊ธฐ ๋ณด๊ด€์ด ํ•„์š”ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.


Q. ์ด๋ฒˆ ๊ตฌํ˜„์—์„œ Exactly-Once๋ฅผ ๋ณด์žฅํ•˜๊ธฐ ์œ„ํ•ด ์ ์šฉํ•œ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

์ผ๋‹จ Exactly-Once๋ฅผ ๋ณด์žฅํ•˜๋ ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•  ์ˆ˜ ์žˆ์„์ง€์— ๋Œ€ํ•œ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค. (Kafka๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ƒํ™ฉ)

1. Kafka Transactions (Kafka Native Exactly-Once)

  • Kafka ์„ค์ •์„ ์ด์šฉํ•ด ์ด๋ฒคํŠธ ์ค‘๋ณต ๋ฐฉ์ง€
  • enable.idempotence: true, transactional.id ๋“ฑ ์„ค์ •

2. ๋น„์ฆˆ๋‹ˆ์Šค ๋ ˆ๋ฒจ์—์„œ Idempotency ๊ตฌํ˜„

  • DB์—์„œ ๋น„์ฆˆ๋‹ˆ์Šค ๊ณ ์œ ํ‚ค๋ฅผ ๋งŒ๋“ค์–ด ์ด๋ฒคํŠธ ์ค‘๋ณต ๋ฐฉ์ง€
  • ์˜ˆ: UNIQUE INDEX (user_id, product_id)

3. Upsert ๋ฐฉ์‹

  • INSERT ... ON DUPLICATE KEY UPDATE ๋ฐฉ์‹
  • ์žˆ์œผ๋ฉด Update, ์—†์œผ๋ฉด Insert

4. Inbox ํŒจํ„ด ์‚ฌ์šฉ

  • event_inbox ํ…Œ์ด๋ธ”์„ ๋งŒ๋“ค์–ด event_id ๊ธฐ๋ฐ˜ ์ค‘๋ณต ์ฒดํฌ
  • Inbox์— ์žˆ๋Š” ์ด๋ฒคํŠธ๋Š” ๋ฌด์‹œ, ์—†๋‹ค๋ฉด ์ฒ˜๋ฆฌ ํ›„ ์ถ”๊ฐ€

ํ˜„์žฌ ์„ ํƒํ•œ ๋ฐฉ์‹

์ €๋Š” Inbox ํŒจํ„ด์€ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด์œ ๋กœ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. Kafka ๋ฐฉ์‹์˜ ๋ฌธ์ œ์ 
    ์™ธ๋ถ€ DB์™€ Kafka๋ฅผ ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ์–ด๋–ป๊ฒŒ ๋ฌถ์„ ์ˆ˜ ์žˆ์„์ง€์— ๋Œ€ํ•ด ํ•ด๊ฒฐ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค..

  2. ๋น„์ฆˆ๋‹ˆ์Šค ๋ ˆ๋ฒจ์—์„œ Idempotencey์˜ ๋ฌธ์ œ์ 
    ๋น„์ฆˆ๋‹ˆ์Šค ํ‚ค๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ์ ์šฉํ•  ์ˆ˜ ์—†๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

  3. Upsert์˜ ๋ฌธ์ œ์ 
    ์ค‘๋ณต ์ด๋ฒคํŠธ ์‹œ ๊ฐ’์ด 2๋ฐฐ๋กœ ์ฆ๊ฐ€(ex. ์กฐํšŒ์ˆ˜)ํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค. (๋ฉฑ๋“ฑ์„ฑ์ด ๋ณด์žฅ๋˜์ง€ ์•Š์Œ)

  4. ๋”ฐ๋ผ์„œ Inbox ํ…Œ์ด๋ธ”๋กœ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
    ์‚ฌ์‹ค ์ฒ˜์Œ์—๋Š” Inbox๊นŒ์ง€ ํ•„์š”ํ• ๊นŒ ๋ผ๋Š” ๋งˆ์Œ์œผ๋กœ ๊ตฌํ˜„์„ ์‹œ์ž‘ ํ–ˆ์Šต๋‹ˆ๋‹ค.
    ํ•˜์ง€๋งŒ, ๋ฃจํผ์Šค๋ฅผ ์ง„ํ–‰ํ•˜๋Š” ๊ณผ์ • ๋‚ด์—์„œ๋Š” ์ด๋ฒˆ ์ฃผ์—๋Š” ์–ด๋–ค ๊ฒƒ์„ ๋ฐฐ์šฐ๊ณ , ์ ์šฉํ•  ์ˆ˜ ์žˆ์„๊นŒ์— ๋Œ€ํ•ด ๊ณ ๋ฏผํ•ด๋ณด๋‹ค๊ฐ€
    ์ตœ๋Œ€ํ•œ ๊ณ ๋ฏผ์„ ๋งŽ์ด ๋‹ด์„ ์ˆ˜ ์žˆ๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.
    Inbox ํ…Œ์ด๋ธ”์€ ์ฃผ๊ธฐ์ ์œผ๋กœ ์‚ญ์ œ๋ฅผ ํ•ด์•ผ ๋˜๋Š”๋ฐ ํŠธ๋ž˜ํ”ฝ์ด ์—„์ฒญ ๋ชฐ๋ ค Inbox ํ…Œ์ด๋ธ”์ด ์‚ญ์ œ๋  ๋•Œ downtime์ด ๋ฐœ์ƒํ•˜๋ฉด ์–ด๋–กํ•˜์ง€? ๋“ฑ ์ถ”๊ฐ€์ ์ธ ๊ณ ๋ฏผ์„ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์„ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.. ์ž˜ ํ•œ ๊ฑด์ง€๋Š” ๋ชจ๋ฅด๊ฒ ์Šต๋‹ˆ๋‹ค..!

โœ… Checklist

๐ŸŽพ Producer

  • ๋„๋ฉ”์ธ(์• ํ”Œ๋ฆฌ์ผ€์ด์…˜) ์ด๋ฒคํŠธ ์„ค๊ณ„
  • Producer ์•ฑ์—์„œ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ฐœํ–‰ (catalog-events, order-events, ๋“ฑ)
  • PartitionKey ๊ธฐ๋ฐ˜์˜ ์ด๋ฒคํŠธ ์ˆœ์„œ ๋ณด์žฅ
  • ๋ฉ”์„ธ์ง€ ๋ฐœํ–‰์ด ์‹คํŒจํ–ˆ์„ ๊ฒฝ์šฐ์— ๋Œ€ํ•ด ๊ณ ๋ฏผํ•ด๋ณด๊ธฐ

โšพ Consumer

  • Consumer ๊ฐ€ Metrics ์ง‘๊ณ„ ์ฒ˜๋ฆฌ
  • event_handled ํ…Œ์ด๋ธ”์„ ํ†ตํ•œ ๋ฉฑ๋“ฑ ์ฒ˜๋ฆฌ ๊ตฌํ˜„
  • ์žฌ๊ณ  ์†Œ์ง„ ์‹œ ์ƒํ’ˆ ์บ์‹œ ๊ฐฑ์‹ 
  • ์ค‘๋ณต ๋ฉ”์„ธ์ง€ ์žฌ์ „์†ก ํ…Œ์ŠคํŠธ โ†’ ์ตœ์ข… ๊ฒฐ๊ณผ๊ฐ€ ํ•œ ๋ฒˆ๋งŒ ๋ฐ˜์˜๋˜๋Š”์ง€ ํ™•์ธ

Summary by CodeRabbit

๋ฆด๋ฆฌ์Šค ๋…ธํŠธ

์ƒˆ ๊ธฐ๋Šฅ

  • ์นดํ”„์นด ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์‹œ์Šคํ…œ์œผ๋กœ ์‹ ๋ขฐ์„ฑ ์žˆ๋Š” ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์ง€์›
  • ์‹คํŒจํ•œ ์ด๋ฒคํŠธ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ Dead Letter Queue ์ถ”๊ฐ€
  • ์ค‘๋ณต ์ด๋ฒคํŠธ ์ž๋™ ๊ฐ์ง€๋ฅผ ์œ„ํ•œ Event Inbox ๊ตฌํ˜„
  • ์ƒํ’ˆ๋ณ„ ๋ฉ”ํŠธ๋ฆญ(์ข‹์•„์š”, ์กฐํšŒ์ˆ˜, ํŒ๋งค๋Ÿ‰) ์ž๋™ ์ถ”์ 

๋ฆฌํŒฉํ† ๋ง

  • ์ฝ”๋“œ ๊ตฌ์กฐ ๊ฐœ์„  ๋ฐ ์บ์‹œ ๋ฌดํšจํ™” ์ตœ์ ํ™”

โœ๏ธ Tip: You can customize this high-level summary in your review settings.

- event_outbox: Producer Outbox ํŒจํ„ด (At Least Once ๋ณด์žฅ)
- event_inbox: Consumer Inbox ํŒจํ„ด (๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ)
- dead_letter_queue: ์‹คํŒจ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ
- product_metrics: ์‹ค์‹œ๊ฐ„ ์ง‘๊ณ„ ํ…Œ์ด๋ธ” (์ข‹์•„์š”/์กฐํšŒ์ˆ˜/์ฃผ๋ฌธ์ˆ˜/ํŒ๋งค๊ธˆ์•ก)
- KRaft ๋ชจ๋“œ ํ™œ์„ฑํ™” (Zookeeper ์ œ๊ฑฐ)
- Producer: At Least Once ์„ค์ • (acks=all, idempotence=true)
- Consumer: ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๋ฐ ์ˆ˜๋™ ์˜คํ”„์…‹ ์ปค๋ฐ‹
- ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ: auto.offset.reset=earliest ์„ค์ •
- Outbox Publisher ์Šค์ผ€์ค„๋Ÿฌ ํ™œ์„ฑํ™”
- OutboxEventService: ์ด๋ฒคํŠธ๋ฅผ Outbox ํ…Œ์ด๋ธ”์— ์ €์žฅ
- OutboxEventPublisher: ์Šค์ผ€์ค„๋Ÿฌ๋กœ ๋ฏธ๋ฐœํ–‰ ์ด๋ฒคํŠธ ์กฐํšŒ ๋ฐ Kafka ๋ฐœํ–‰
- At Least Once ๋ณด์žฅ: ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ํ›„ ์ด๋ฒคํŠธ ๋ฐœํ–‰
- ์‹คํŒจ ์‹œ ์žฌ์‹œ๋„: ์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ ์ฃผ๊ธฐ์ ์œผ๋กœ ๋ฏธ๋ฐœํ–‰ ์ด๋ฒคํŠธ ์žฌ๋ฐœํ–‰
Consumer ๊ตฌํ˜„:
- CatalogEventConsumer: ์ข‹์•„์š”/์กฐํšŒ์ˆ˜ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
- OrderEventConsumer: ์ฃผ๋ฌธ/๊ฒฐ์ œ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
- Inbox ํŒจํ„ด์œผ๋กœ ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ (์ค‘๋ณต ๋ฉ”์‹œ์ง€ ๋ฐฉ์ง€)
- ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๋กœ ์„ฑ๋Šฅ ์ตœ์ ํ™”
- DLQ(Dead Letter Queue)๋กœ ์‹คํŒจ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ

ProductMetrics ์ง‘๊ณ„:
- ์ข‹์•„์š” ์ˆ˜, ์กฐํšŒ์ˆ˜, ์ฃผ๋ฌธ ์ˆ˜, ํŒ๋งค ๊ธˆ์•ก ์‹ค์‹œ๊ฐ„ ์ง‘๊ณ„
- ๋ณ„๋„ ํ…Œ์ด๋ธ”๋กœ ์กฐํšŒ ์„ฑ๋Šฅ ์ตœ์ ํ™”
- OrderFacade: ์žฌ๊ณ  ์ฐจ๊ฐ ํ›„ ์žฌ๊ณ ๊ฐ€ 0์ด ๋˜๋ฉด ์บ์‹œ ์‚ญ์ œ
- ProductCacheService: Spring Cache ์‚ญ์ œ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
- ๋‹ค์Œ ์กฐํšŒ ์‹œ DB์—์„œ ์ตœ์‹  ๋ฐ์ดํ„ฐ(์žฌ๊ณ  0)๋ฅผ ๊ฐ€์ ธ์™€ ์บ์‹œ ๊ฐฑ์‹ 
ํ…Œ์ŠคํŠธ ์ž‘์„ฑ:
- CatalogEventConsumerTest: ์ข‹์•„์š”/์กฐํšŒ์ˆ˜ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๊ฒ€์ฆ (6๊ฐœ)
- OrderEventConsumerTest: ์ฃผ๋ฌธ/๊ฒฐ์ œ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๊ฒ€์ฆ (7๊ฐœ)
- EventInboxServiceTest: ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ ๊ฒ€์ฆ
- ProductMetricsServiceTest: ์ง‘๊ณ„ ๋กœ์ง ๊ฒ€์ฆ
- ConsumerSmokeTest: Bean ์ƒ์„ฑ ๋ฐ Context ๋กœ๋”ฉ ๊ฒ€์ฆ

ํ…Œ์ŠคํŠธ ์ „๋žต:
- Consumer ๋กœ์ง ๋‹จ์œ„ ํ…Œ์ŠคํŠธ (Mock ์‚ฌ์šฉ)
- ์‹ค์ œ Kafka ์—†์ด ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๋งŒ ๊ฒ€์ฆ
- ๋ฌธ์„œ๋กœ์„œ์˜ ํ…Œ์ŠคํŠธ: ๋ช…ํ™•ํ•œ ์„ค๋ช…๊ณผ ์‹œ๋‚˜๋ฆฌ์˜ค
- IntelliJ Optimize Imports ์ ์šฉ
- ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” import ์ œ๊ฑฐ
- import ์ˆœ์„œ ์ •๋ฆฌ (static import ์šฐ์„ )
- Retry์™€ Circuit Breaker ์ƒํ˜ธ์ž‘์šฉ์„ ๊ณ ๋ คํ•œ ๊ฒ€์ฆ ์ˆ˜์ •
- times(6)์—์„œ atLeast(6)์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์žฌ์‹œ๋„ ๋กœ์ง ๋ฐ˜์˜
- Mock ์„ค์ • ๋ˆ„๋ฝ์œผ๋กœ ์ธํ•œ NullPointerException ํ•ด๊ฒฐ
Embedded Kafka๋ฅผ ํ™œ์šฉํ•œ 4๊ฐ€์ง€ ํ•™์Šต ํ…Œ์ŠคํŠธ ๊ตฌํ˜„:
- Experiment1: Auto Commit vs Manual Commit ๋™์ž‘ ๋น„๊ต
- Experiment2: auto.offset.reset (earliest/latest) ํ•™์Šต
- Experiment3: max.poll.interval.ms ํƒ€์ž„์•„์›ƒ ์ฒดํ—˜
- Experiment4: max.poll.records ๋ฐฐ์น˜ ํฌ๊ธฐ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„

ํŠน์ง•:
- @embeddedkafka ์‚ฌ์šฉ์œผ๋กœ Docker ๋ถˆํ•„์š”
- IntelliJ์—์„œ ๋ฐ”๋กœ ์‹คํ–‰ ๊ฐ€๋Šฅ
- ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ๋…๋ฆฝ์ ์ธ ํ† ํ”ฝ ์‚ฌ์šฉ
@rkdalsdn94 rkdalsdn94 self-assigned this Dec 18, 2025
@rkdalsdn94 rkdalsdn94 added the enhancement New feature or request label Dec 18, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 18, 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.

์›Œํฌ์Šค๋ฃจ

์ด ๋ณ€๊ฒฝ์€ Kafka ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ๋ฐ ์†Œ๋น„ ์ธํ”„๋ผ๋ฅผ ๋„์ž…ํ•ฉ๋‹ˆ๋‹ค. ์ƒ๊ฑฐ๋ž˜ API์—๋Š” ์•„์›ƒ๋ฐ•์Šค ์ด๋ฒคํŠธ๋ฅผ Kafka ์ฃผ์ œ๋กœ ๋ฐœํ–‰ํ•˜๋Š” EventKafkaProducer๊ฐ€ ์ถ”๊ฐ€๋˜๊ณ , ์ƒ๊ฑฐ๋ž˜ ์ŠคํŠธ๋ฆฌ๋จธ ๋ชจ๋“ˆ์—๋Š” ์นดํƒˆ๋กœ๊ทธ ๋ฐ ์ฃผ๋ฌธ ์ด๋ฒคํŠธ๋ฅผ ์†Œ๋น„ํ•˜๊ณ  ์ฒ˜๋ฆฌํ•˜๋Š” ์ปจ์Šˆ๋จธ๊ฐ€ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค. ๋ฉฑ๋“ฑ์„ฑ ํ™•์ธ, ์ƒํ’ˆ ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘, ์‹คํŒจํ•œ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ์ง€์› ์„œ๋น„์Šค๊ฐ€ ๋„์ž…๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๋ณ€๊ฒฝ์‚ฌํ•ญ

์‘์ง‘๋ ฅ ์žˆ๋Š” ๋ณ€๊ฒฝ / ํŒŒ์ผ ๋ณ€๊ฒฝ ์š”์•ฝ
Kafka ๋ชจ๋“ˆ ํ†ตํ•ฉ
apps/commerce-api/build.gradle.kts, apps/commerce-api/src/main/resources/application.yml
Kafka ๋ชจ๋“ˆ ์˜์กด์„ฑ ์ถ”๊ฐ€ ๋ฐ ์นดํƒˆ๋กœ๊ทธ/์ฃผ๋ฌธ ์ด๋ฒคํŠธ ์ฃผ์ œ ์„ค์ •
์•„์›ƒ๋ฐ•์Šค ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์ธํ”„๋ผ
apps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.java, apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.java, apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventPublisher.java
EventKafkaProducer ๋„์ž… ๋ฐ OutboxEventService๋ฅผ Kafka ๋ฐœํ–‰์œผ๋กœ ๋ณ€๊ฒฝ, ์กฐ๊ฑด๋ถ€ ์•„์›ƒ๋ฐ•์Šค ํผ๋ธ”๋ฆฌ์…” ์„ค์ •
์ œํ’ˆ ์บ์‹œ ๋ฌดํšจํ™”
apps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheService.java, apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
์บ์‹œ ์ œ๊ฑฐ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€ ๋ฐ ์ฃผ๋ฌธ ์ฒ˜๋ฆฌ ์‹œ ์บ์‹œ ๋ฌดํšจํ™” ํ†ตํ•ฉ
์นดํƒˆ๋กœ๊ทธ ์ด๋ฒคํŠธ ์ปจ์Šˆ๋จธ
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.java, apps/commerce-streamer/src/main/java/com/loopers/application/inbox/EventInboxService.java, apps/commerce-streamer/src/main/java/com/loopers/application/dlq/DeadLetterQueueService.java
์นดํƒˆ๋กœ๊ทธ ์ด๋ฒคํŠธ ์ˆ˜์‹  ๋ฐ ์ฒ˜๋ฆฌ, ๋ฉฑ๋“ฑ์„ฑ ํ™•์ธ, ์‹คํŒจ ์ฒ˜๋ฆฌ
์ฃผ๋ฌธ ์ด๋ฒคํŠธ ์ปจ์Šˆ๋จธ
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java, apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.java
์ฃผ๋ฌธ ์ด๋ฒคํŠธ ์ˆ˜์‹  ๋ฐ ์ƒํ’ˆ ๋ฉ”ํŠธ๋ฆญ ์—…๋ฐ์ดํŠธ
๋„๋ฉ”์ธ ๋ชจ๋ธ ๋ฐ ์ €์žฅ์†Œ
apps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInbox.java, apps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInboxRepository.java, apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DeadLetterQueue.java, apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DeadLetterQueueRepository.java, apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.java, apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java
๋ฐ›์€ํŽธ์ง€ํ•จ, DLQ, ๋ฉ”ํŠธ๋ฆญ ๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ ๋ฐ ์ €์žฅ์†Œ
์ €์žฅ์†Œ ๊ตฌํ˜„
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxRepositoryImpl.java, apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxJpaRepository.java, apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueRepositoryImpl.java, apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueJpaRepository.java, apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java, apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java
๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๊ณ„์ธต ๊ตฌํ˜„
๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ
docker/init-db.sql
์ด๋ฒคํŠธ ๋ฐ›์€ํŽธ์ง€ํ•จ, ์ œํ’ˆ ๋ฉ”ํŠธ๋ฆญ, DLQ ํ…Œ์ด๋ธ” ์ถ”๊ฐ€
Kafka ๊ตฌ์„ฑ
modules/kafka/src/main/resources/kafka.yml, modules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.java
Kafka ํ”„๋กœ๋“€์„œ ์„ค์ • ๋ฐ ๋ช…์‹œ์  ์ž„ํฌํŠธ ์ •๋ฆฌ
Kafka ํ•™์Šต ํ…Œ์ŠคํŠธ
modules/kafka/src/test/java/com/loopers/kafka/learning/*
์ž๋™ ์ปค๋ฐ‹, ์˜คํ”„์…‹ ๋ฆฌ์…‹, ํด ๊ฐ„๊ฒฉ, ๋ฐฐ์น˜ ํฌ๊ธฐ ๊ด€๋ จ Kafka ๋™์ž‘ ๊ฒ€์ฆ
ํ†ตํ•ฉ ๋ฐ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ
apps/commerce-streamer/src/test/java/com/loopers/application/inbox/EventInboxServiceTest.java, apps/commerce-streamer/src/test/java/com/loopers/application/metrics/ProductMetricsServiceTest.java, apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/CatalogEventConsumerTest.java, apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/OrderEventConsumerTest.java, apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/ConsumerSmokeTest.java
์ปจ์Šˆ๋จธ, ์„œ๋น„์Šค, ๋ฉ”ํŠธ๋ฆญ ์ฒ˜๋ฆฌ ๋กœ์ง ๊ฒ€์ฆ
์ž„ํฌํŠธ ์ •๋ฆฌ ๋ฐ ๊ธฐํƒ€ ๊ฐœ์„ 
apps/commerce-api/src/main/java/com/loopers/..., apps/commerce-streamer/src/main/java/com/loopers/..., modules/jpa/..., modules/redis/..., modules/pg-simulator/..., performance-test/...
์ค‘๋ณต ์ž„ํฌํŠธ ์ œ๊ฑฐ, ์ž„ํฌํŠธ ์ˆœ์„œ ์žฌ์ •๋ ฌ, ๋ฏธ์‚ฌ์šฉ ์ž„ํฌํŠธ ์ •๋ฆฌ

์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ

sequenceDiagram
    participant API as Commerce API
    participant Kafka as Kafka Broker
    participant Consumer as Event Consumer
    participant Inbox as Event Inbox
    participant Metrics as Metrics Service
    participant DLQ as Dead Letter Queue

    rect rgb(200, 220, 255)
        Note over API,Kafka: 1. Outbox ๋ฐœํ–‰ ํ๋ฆ„
        API->>Kafka: publish(EventOutbox)
        Kafka-->>API: SendResult with offset
        API->>API: Mark Outbox as Published
    end

    rect rgb(200, 255, 220)
        Note over Consumer,DLQ: 2. ์ด๋ฒคํŠธ ์†Œ๋น„ ํ๋ฆ„
        Kafka->>Consumer: Poll batch of records
        Consumer->>Inbox: Check isDuplicate(eventId)
        alt ๋ฉฑ๋“ฑ์„ฑ ํ™•์ธ ํ†ต๊ณผ
            Inbox-->>Consumer: false (new event)
            Consumer->>Inbox: save(eventId, ...)
            Inbox-->>Consumer: saved
            alt ์ด๋ฒคํŠธ ํƒ€์ž…๋ณ„ ์ฒ˜๋ฆฌ
                Consumer->>Metrics: incrementLikeCount/View/Order
                Metrics-->>Consumer: metrics updated
            end
        else ์ค‘๋ณต ๊ฐ์ง€
            Inbox-->>Consumer: true (duplicate)
            Consumer->>Consumer: skip processing
        end
    end

    rect rgb(255, 220, 220)
        Note over Consumer,DLQ: 3. ์‹คํŒจ ์ฒ˜๋ฆฌ
        Consumer->>Consumer: Exception occurs
        Consumer->>DLQ: save(topic, eventId, payload, error)
        DLQ-->>Consumer: saved to DLQ
    end

    Consumer->>Kafka: commitAsync() for batch offset
    Kafka-->>Consumer: offset committed
Loading

์˜ˆ์ƒ ์ฝ”๋“œ ๋ฆฌ๋ทฐ ๋…ธ๋ ฅ

๐ŸŽฏ 4 (๋ณต์žกํ•จ) | โฑ๏ธ ~75๋ถ„

์ถ”๊ฐ€ ๊ฒ€ํ†  ์š”์ฒญ ์‚ฌํ•ญ:

  • EventKafkaProducer์™€ OutboxEventService: Kafka ๋ฐœํ–‰ ํ๋ฆ„, ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„ ๋‚ด join() ํ˜ธ์ถœ์˜ ๋™๊ธฐํ™” ๋ณด์ฆ, ์ƒํƒœ ์ „์ด ๋กœ์ง ๊ฒ€์ฆ
  • ์ปจ์Šˆ๋จธ ๊ตฌํ˜„ (CatalogEventConsumer, OrderEventConsumer): ๋ฉฑ๋“ฑ์„ฑ ํ™•์ธ ๋กœ์ง, ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์ค‘ ๋ถ€๋ถ„ ์‹คํŒจ ์ฒ˜๋ฆฌ, DLQ ์ €์žฅ ํ๋ฆ„
  • ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘ (ProductMetricsService): ๋™์‹œ์„ฑ ์‹œ๋‚˜๋ฆฌ์˜ค, ๋‚™๊ด€์  ์ž ๊ธˆ (version) ์ฒ˜๋ฆฌ, ์นด์šดํ„ฐ ์ฆ๊ฐ์˜ ์›์ž์„ฑ
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ: ์ธ๋ฑ์Šค ์„ค๊ณ„ (event_inbox์˜ event_id ๊ณ ์œ ์„ฑ, product_metrics ์กฐํšŒ ์„ฑ๋Šฅ), ๊ธฐ์กด ๋ฐ์ดํ„ฐ์™€์˜ ํ˜ธํ™˜์„ฑ
  • ๊ตฌ์„ฑ ๋ฐ ์กฐ๊ฑด๋ถ€ ๋นˆ: outbox.publisher.enabled ํ”Œ๋ž˜๊ทธ์˜ ๊ธฐ๋ณธ๊ฐ’, ๋กœ์ปฌ/ํ…Œ์ŠคํŠธ ํ”„๋กœํ•„์—์„œ์˜ ํ™œ์„ฑํ™” ์ƒํƒœ ํ™•์ธ
  • ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€: CatalogEventConsumerTest์™€ OrderEventConsumerTest์˜ ๋ชจ์˜ ๊ฐ์ฒด ์„ค์ •, ์—ฃ์ง€ ์ผ€์ด์Šค (๋ˆ„๋ฝ๋œ eventId, ์ž˜๋ชป๋œ ํŽ˜์ด๋กœ๋“œ) ์ฒ˜๋ฆฌ ๊ฒ€์ฆ

๊ด€๋ จ PR ๋ชฉ๋ก

์‹œ

๐Ÿฐ Kafka์˜ ํ๋ฆ„์— ํƒˆ ํƒ€์ด๋ฐ,
์•„์›ƒ๋ฐ•์Šค์—์„œ ์ด๋ฒคํŠธ ๋ฐœํ–‰,
๋ฉฑ๋“ฑ์„ฑ์œผ๋กœ ์ค‘๋ณต ๋นผ๋‚ด๊ณ ,
๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘์— DLQ ์ค€๋น„,
๋น„๋™๊ธฐ ์„ธ์ƒ ์•ˆ์ „ํ•˜๊ฒŒ! โœจ

Pre-merge checks and finishing touches

โŒ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage โš ๏ธ Warning Docstring coverage is 38.64% 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 ์ œ๋ชฉ์€ ํ’€ ๋ฆฌํ€˜์ŠคํŠธ์˜ ์ฃผ์š” ๋ณ€๊ฒฝ์‚ฌํ•ญ์ธ Kafka ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌํ˜„์„ ๋ช…ํ™•ํ•˜๊ฒŒ ์š”์•ฝํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
Description check โœ… Passed PR ์„ค๋ช…์ด ํ•„์ˆ˜ ์„น์…˜์„ ๋ชจ๋‘ ํฌํ•จํ•˜๊ณ  ์žˆ์œผ๋ฉฐ ๊ตฌํ˜„ ์‚ฌํ•ญ, ์„ค๊ณ„ ๊ณ ๋ฏผ, ์ฒดํฌ๋ฆฌ์ŠคํŠธ๊ฐ€ ์ƒ์„ธํžˆ ์ž‘์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

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: 15

Caution

Some comments are outside the diff and canโ€™t be posted inline due to platform limitations.

โš ๏ธ Outside diff range comments (2)
modules/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt (1)

54-80: ์ƒํƒœ ๋ณ€๊ฒฝ ์‹œ updatedAt ํ•„๋“œ๋ฅผ ์—…๋ฐ์ดํŠธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

updatedAt ํ•„๋“œ๊ฐ€ ์ดˆ๊ธฐํ™” ํ›„ ์ƒํƒœ ๋ณ€๊ฒฝ ๋ฉ”์„œ๋“œ(approve(), invalidCard(), limitExceeded())์—์„œ ์—…๋ฐ์ดํŠธ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด updated_at ์ปฌ๋Ÿผ์ด ํ•ญ์ƒ ์ƒ์„ฑ ์‹œ์ ์˜ ํƒ€์ž„์Šคํƒฌํ”„๋ฅผ ์œ ์ง€ํ•˜๊ฒŒ ๋˜์–ด ์‹ค์ œ ์ˆ˜์ • ์‹œ๊ฐ„์„ ์ถ”์ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

๊ฐ ์ƒํƒœ ๋ณ€๊ฒฝ ๋ฉ”์„œ๋“œ์—์„œ updatedAt์„ ์—…๋ฐ์ดํŠธํ•˜์„ธ์š”. private set์„ internal set์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜, @PreUpdate ๋ผ์ดํ”„์‚ฌ์ดํด ์ฝœ๋ฐฑ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ์‹์ด ์žˆ์Šต๋‹ˆ๋‹ค:

fun approve() {
    if (status != TransactionStatus.PENDING) {
        throw CoreException(ErrorType.INTERNAL_ERROR, "๊ฒฐ์ œ์Šน์ธ์€ ๋Œ€๊ธฐ์ƒํƒœ์—์„œ๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.")
    }
    status = TransactionStatus.SUCCESS
    reason = "์ •์ƒ ์Šน์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."
+   updatedAt = LocalDateTime.now()
}

fun invalidCard() {
    if (status != TransactionStatus.PENDING) {
        throw CoreException(ErrorType.INTERNAL_ERROR, "๊ฒฐ์ œ์ฒ˜๋ฆฌ๋Š” ๋Œ€๊ธฐ์ƒํƒœ์—์„œ๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.")
    }
    status = TransactionStatus.FAILED
    reason = "์ž˜๋ชป๋œ ์นด๋“œ์ž…๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์นด๋“œ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”."
+   updatedAt = LocalDateTime.now()
}

fun limitExceeded() {
    if (status != TransactionStatus.PENDING) {
        throw CoreException(ErrorType.INTERNAL_ERROR, "ํ•œ๋„์ดˆ๊ณผ ์ฒ˜๋ฆฌ๋Š” ๋Œ€๊ธฐ์ƒํƒœ์—์„œ๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.")
    }
    status = TransactionStatus.FAILED
    reason = "ํ•œ๋„์ดˆ๊ณผ์ž…๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์นด๋“œ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”."
+   updatedAt = LocalDateTime.now()
}
apps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceCircuitBreakerTest.java (1)

125-158: ์žฌ์‹œ๋„ ๋™์ž‘๊ณผ ์ƒํƒœ ์ „ํ™˜ ํƒ€์ด๋ฐ์„ ๊ณ ๋ คํ•œ ํ…Œ์ŠคํŠธ ์•ˆ์ •์„ฑ ๊ฐœ์„ ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ๊ตฌํ˜„์—์„œ ๋‹ค์Œ ์‚ฌํ•ญ๋“ค์ด ํ…Œ์ŠคํŠธ ์•ˆ์ •์„ฑ์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  1. 145๋ฒˆ ๋ผ์ธ์˜ ์—„๊ฒฉํ•œ ์ƒํƒœ ๋‹จ์–ธ: ์žฌ์‹œ๋„์™€ ์„œํ‚ท ๋ธŒ๋ ˆ์ด์ปค์˜ ๋น„๋™๊ธฐ ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋กœ ์ธํ•ด ๋ฃจํ”„ ์งํ›„ ์ฆ‰์‹œ OPEN ์ƒํƒœ๊ฐ€ ์•„๋‹ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. 81-120๋ฒˆ ๋ผ์ธ์˜ ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ๋Š” isIn(CircuitBreaker.State.OPEN, CircuitBreaker.State.HALF_OPEN)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋” ๊ด€๋Œ€ํ•œ ๊ฒ€์ฆ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

  2. ์žฌ์‹œ๋„๋กœ ์ธํ•œ ์‹ค์ œ ํ˜ธ์ถœ ํšŸ์ˆ˜ ๋ถˆ์ผ์น˜: 82-84๋ฒˆ ๋ผ์ธ์˜ ์ฃผ์„์— ๋”ฐ๋ฅด๋ฉด ์žฌ์‹œ๋„๊ฐ€ 2๋ฒˆ ๋ฐœ์ƒํ•˜๋ฏ€๋กœ, 6๋ฒˆ ๋ฃจํ”„๋Š” ์‹ค์ œ๋กœ ์•ฝ 18๋ฒˆ(6 ์ดˆ๊ธฐ + 12 ์žฌ์‹œ๋„)์˜ pgClient ํ˜ธ์ถœ์„ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. ์Šฌ๋ผ์ด๋”ฉ ์œˆ๋„์šฐ๊ฐ€ 10์ด๋ฏ€๋กœ ๋งˆ์ง€๋ง‰ 10๊ฐœ ํ˜ธ์ถœ๋งŒ ์‹คํŒจ์œจ ๊ณ„์‚ฐ์— ํฌํ•จ๋˜๋Š”๋ฐ, 129-130๋ฒˆ ๋ผ์ธ์˜ ์ฃผ์„("์Šฌ๋ผ์ด๋”ฉ ์œˆ๋„์šฐ 10๊ฐœ ์ค‘ 6๊ฐœ ์ด์ƒ ์‹คํŒจ")์€ ์ด๋ฅผ ๋ช…ํ™•ํžˆ ๋ฐ˜์˜ํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ฐœ์„  ์ œ์•ˆ

์˜ต์…˜ 1: ๋” ๊ด€๋Œ€ํ•œ ์ƒํƒœ ๊ฒ€์ฆ ์‚ฌ์šฉ

-        // Circuit์ด OPEN ์ƒํƒœ์ธ์ง€ ํ™•์ธ
-        assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.OPEN);
+        // Circuit์ด OPEN ์ƒํƒœ์ธ์ง€ ํ™•์ธ (์žฌ์‹œ๋„ ํƒ€์ด๋ฐ์œผ๋กœ ์ธํ•ด HALF_OPEN์ผ ์ˆ˜๋„ ์žˆ์Œ)
+        assertThat(circuitBreaker.getState()).isIn(CircuitBreaker.State.OPEN, CircuitBreaker.State.HALF_OPEN);

์˜ต์…˜ 2: ๋ฉ”ํŠธ๋ฆญ ๊ธฐ๋ฐ˜ ๊ฒ€์ฆ ์ถ”๊ฐ€

         // Circuit์ด OPEN ์ƒํƒœ์ธ์ง€ ํ™•์ธ
         assertThat(circuitBreaker.getState()).isEqualTo(CircuitBreaker.State.OPEN);
+        
+        // ์‹คํŒจ ํšŸ์ˆ˜์™€ ์‹คํŒจ์œจ๋กœ ์ถ”๊ฐ€ ๊ฒ€์ฆ
+        var metrics = circuitBreaker.getMetrics();
+        assertThat(metrics.getNumberOfFailedCalls()).isGreaterThanOrEqualTo(5);
+        assertThat(metrics.getFailureRate()).isGreaterThanOrEqualTo(50.0f);

์˜ต์…˜ 3: ์ฃผ์„ ๋ช…ํ™•ํ™”

-        // ์Šฌ๋ผ์ด๋”ฉ ์œˆ๋„์šฐ 10๊ฐœ ์ค‘ 6๊ฐœ ์ด์ƒ ์‹คํŒจ ์‹œ์ผœ์„œ Circuit OPEN (์‹คํŒจ์œจ 50% ์ดˆ๊ณผ)
+        // 6๋ฒˆ ์š”์ฒญ ์‹œ๋„ (๊ฐ ์š”์ฒญ์€ ์žฌ์‹œ๋„ ํฌํ•จ ์‹œ ์ตœ๋Œ€ 3๋ฒˆ ํ˜ธ์ถœ)
+        // ์Šฌ๋ผ์ด๋”ฉ ์œˆ๋„์šฐ(10)์— ์ถฉ๋ถ„ํ•œ ์‹คํŒจ๋ฅผ ๋ฐœ์ƒ์‹œ์ผœ Circuit OPEN (์‹คํŒจ์œจ 50% ์ดˆ๊ณผ)
๐Ÿงน Nitpick comments (18)
modules/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionKeyGenerator.kt (1)

6-6: wildcard import ๋Œ€์‹  ๋ช…์‹œ์  import ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

java.util.* wildcard import๋Š” ์ฝ”๋“œ์—์„œ ์‹ค์ œ๋กœ UUID ํด๋ž˜์Šค๋งŒ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์–ด ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. Wildcard import๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฌธ์ œ๋ฅผ ์•ผ๊ธฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ์–ด๋–ค ํด๋ž˜์Šค๊ฐ€ ์‚ฌ์šฉ๋˜๋Š”์ง€ ๋ช…ํ™•ํ•˜์ง€ ์•Š์•„ ๊ฐ€๋…์„ฑ ์ €ํ•˜
  • ์—ฌ๋Ÿฌ ํŒจํ‚ค์ง€์— ๋™์ผํ•œ ์ด๋ฆ„์˜ ํด๋ž˜์Šค๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ naming conflict ๊ฐ€๋Šฅ์„ฑ
  • ์˜์กด์„ฑ ์ถ”์  ์–ด๋ ค์›€

๋˜ํ•œ, ์ด ๋ณ€๊ฒฝ์€ PR์˜ ์ฃผ์š” ๋ชฉ์ (Kafka ํ†ตํ•ฉ)๊ณผ ๋ฌด๊ด€ํ•ด ๋ณด์ž…๋‹ˆ๋‹ค.

๐Ÿ”Ž ๋ช…์‹œ์  import๋กœ ๋˜๋Œ๋ฆฌ๋Š” diff:
-import java.util.*
+import java.util.UUID
modules/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt (1)

8-8: ์™€์ผ๋“œ์นด๋“œ ์ž„ํฌํŠธ ๋Œ€์‹  ๋ช…์‹œ์  ์ž„ํฌํŠธ ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

์ด ํŒŒ์ผ์—์„œ๋Š” TimeZone๋งŒ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์œผ๋ฏ€๋กœ, import java.util.* ๋Œ€์‹  import java.util.TimeZone์œผ๋กœ ๋ช…์‹œ์  ์ž„ํฌํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. ์™€์ผ๋“œ์นด๋“œ ์ž„ํฌํŠธ๋Š” ์ฝ”๋“œ์˜ ๋ช…ํ™•์„ฑ์„ ๋–จ์–ด๋œจ๋ฆฌ๊ณ  ๋„ค์ด๋ฐ ์ถฉ๋Œ ๊ฐ€๋Šฅ์„ฑ์„ ๋†’์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ œ์•ˆํ•˜๋Š” ์ˆ˜์ •์‚ฌํ•ญ
-import java.util.*
+import java.util.TimeZone
modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment2_OffsetResetTest.java (1)

209-237: ํ—ฌํผ ๋ฉ”์„œ๋“œ ์ค‘๋ณต์„ ๊ณ ๋ คํ•ด๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

produceMessages์™€ createConsumerProps ๋ฉ”์„œ๋“œ๊ฐ€ Experiment1~4 ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค์—์„œ ๋™์ผํ•˜๊ฒŒ ์ค‘๋ณต๋ฉ๋‹ˆ๋‹ค. ํ•™์Šต์šฉ ํ…Œ์ŠคํŠธ์ด๋ฏ€๋กœ ๊ฐ ํŒŒ์ผ์ด ๋…๋ฆฝ์ ์œผ๋กœ ์ดํ•ด ๊ฐ€๋Šฅํ•œ ์ ์€ ์žฅ์ ์ด์ง€๋งŒ, ๊ณตํ†ต ๋ฒ ์ด์Šค ํด๋ž˜์Šค๋‚˜ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค๋กœ ์ถ”์ถœํ•˜๋ฉด ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์šฉ์ดํ•ด์ง‘๋‹ˆ๋‹ค.

docker/init-db.sql (2)

10-24: Inbox ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์ฆ๊ฐ€ ๋ฐ retention ์ •์ฑ… ๊ฒ€ํ†  ๊ถŒ์žฅ

event_inbox ํ…Œ์ด๋ธ”์€ ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ์„ ์œ„ํ•ด ์ฒ˜๋ฆฌ๋œ ๋ชจ๋“  ์ด๋ฒคํŠธ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ์‹œ๊ฐ„์ด ์ง€๋‚จ์— ๋”ฐ๋ผ ํ…Œ์ด๋ธ”์ด ์ง€์†์ ์œผ๋กœ ์ฆ๊ฐ€ํ•˜๋ฏ€๋กœ, ์˜ค๋ž˜๋œ ๋ ˆ์ฝ”๋“œ๋ฅผ ์ •๋ฆฌํ•˜๋Š” retention ์ •์ฑ…์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๊ณ ๋ ค ์‚ฌํ•ญ:

  • processed_at ๋˜๋Š” created_at ๊ธฐ์ค€์œผ๋กœ ์ผ์ • ๊ธฐ๊ฐ„(์˜ˆ: 7์ผ, 30์ผ) ์ดํ›„ ๋ ˆ์ฝ”๋“œ ์‚ญ์ œ
  • deleted_at ์ปฌ๋Ÿผ์ด ์žˆ์ง€๋งŒ soft delete ์‚ฌ์šฉ ์—ฌ๋ถ€๊ฐ€ ๋ช…ํ™•ํ•˜์ง€ ์•Š์Œ
  • ๋ฐฐ์น˜ ์ž‘์—…์œผ๋กœ ์ฃผ๊ธฐ์  ์ •๋ฆฌ ๋˜๋Š” ํŒŒํ‹ฐ์…”๋‹ ์ „๋žต ๊ณ ๋ ค

PR ์š”์•ฝ์— Inbox์™€ Outbox์˜ retention ๊ธฐ๊ฐ„์ด ๋‹ค๋ฅด๋‹ค๋Š” ์„ค๊ณ„ ๋…ผ์˜๊ฐ€ ์žˆ์—ˆ๋‹ค๊ณ  ๋ช…์‹œ๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ, ํ•ด๋‹น ์ •์ฑ…์ด ๊ตฌํ˜„๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

#!/bin/bash
# Description: Inbox ์ •๋ฆฌ ์Šค์ผ€์ค„๋Ÿฌ ๋˜๋Š” ๋ฐฐ์น˜ ์ž‘์—… ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ

echo "=== Inbox cleanup/retention ๊ด€๋ จ ์ฝ”๋“œ ๊ฒ€์ƒ‰ ==="
rg -n "deleteOldInbox|cleanupInbox|retentionInbox" --type=java -C 3

echo -e "\n=== ์Šค์ผ€์ค„๋Ÿฌ ์ž‘์—… ๊ฒ€์ƒ‰ ==="
rg -n "@Scheduled.*Inbox" --type=java -C 3

44-60: DLQ ํ…Œ์ด๋ธ” ์„ค๊ณ„ ๊ฒ€ํ†  ๋ฐ ๋ฐ์ดํ„ฐ ์ฆ๊ฐ€ ๋Œ€๋น„ ๊ถŒ์žฅ

dead_letter_queue ํ…Œ์ด๋ธ” ์„ค๊ณ„์—์„œ ๊ณ ๋ คํ•  ์‚ฌํ•ญ:

  1. ์ปฌ๋Ÿผ ํƒ€์ž… ํฌ๊ธฐ ์ œํ•œ:

    • payload TEXT: ์ตœ๋Œ€ 64KB๊นŒ์ง€๋งŒ ์ €์žฅ ๊ฐ€๋Šฅ
    • error_message TEXT: ๊ธด ์Šคํƒ ํŠธ๋ ˆ์ด์Šค๊ฐ€ ์ž˜๋ฆด ์ˆ˜ ์žˆ์Œ
    • ๋Œ€์šฉ๋Ÿ‰ ๋ฉ”์‹œ์ง€๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒฝ์šฐ MEDIUMTEXT (16MB) ๋˜๋Š” LONGTEXT (4GB) ๊ณ ๋ ค ํ•„์š”
  2. ๋ฐ์ดํ„ฐ ์ฆ๊ฐ€ ๊ด€๋ฆฌ:

    • DLQ ๋ ˆ์ฝ”๋“œ๋„ ์ง€์†์ ์œผ๋กœ ์ฆ๊ฐ€ํ•˜๋ฏ€๋กœ retention ์ •์ฑ… ํ•„์š”
    • retry_count ์ปฌ๋Ÿผ์ด ์žˆ์ง€๋งŒ, ์žฌ์‹œ๋„ ํ›„ ์ž๋™ ์ •๋ฆฌ ๋กœ์ง์ด ์žˆ๋Š”์ง€ ๋ถˆ๋ช…ํ™•
๐Ÿ”Ž ๋Œ€์šฉ๋Ÿ‰ ๋ฉ”์‹œ์ง€๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒฝ์šฐ ์ ์šฉ ๊ฐ€๋Šฅํ•œ diff:
 CREATE TABLE IF NOT EXISTS dead_letter_queue (
     id BIGINT AUTO_INCREMENT PRIMARY KEY,
     original_topic VARCHAR(100) NOT NULL COMMENT '์›๋ณธ Topic',
     partition_key VARCHAR(100) COMMENT 'Partition Key',
     event_id VARCHAR(50) COMMENT '์ด๋ฒคํŠธ ID',
-    payload TEXT NOT NULL COMMENT '์›๋ณธ ๋ฉ”์‹œ์ง€',
-    error_message TEXT COMMENT '์—๋Ÿฌ ๋ฉ”์‹œ์ง€',
+    payload MEDIUMTEXT NOT NULL COMMENT '์›๋ณธ ๋ฉ”์‹œ์ง€',
+    error_message MEDIUMTEXT COMMENT '์—๋Ÿฌ ๋ฉ”์‹œ์ง€',
     retry_count INT NOT NULL DEFAULT 0 COMMENT '์žฌ์‹œ๋„ ํšŸ์ˆ˜',
apps/commerce-streamer/src/main/java/com/loopers/application/inbox/EventInboxService.java (1)

27-35: ์ค‘๋ณต ์ฒดํฌ ๋กœ์ง์ด ์ ์ ˆํ•˜๋ฉฐ, ์„ ํƒ์ ์œผ๋กœ ์ฝ๊ธฐ ์ „์šฉ ํŠธ๋žœ์žญ์…˜ ์ถ”๊ฐ€ ๊ณ ๋ ค ๊ฐ€๋Šฅ

isDuplicate() ๋ฉ”์„œ๋“œ์˜ ๊ตฌํ˜„์ด ๋ช…ํ™•ํ•˜๊ณ  ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ๋‹จ์ˆœ ์กด์žฌ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๋Š” ์ฝ๊ธฐ ์ „์šฉ ์ž‘์—…์ด๋ฏ€๋กœ ํ˜„์žฌ ๊ตฌํ˜„๋„ ๋ฌธ์ œ์—†์ง€๋งŒ, ์„ ํƒ์ ์œผ๋กœ @Transactional(readOnly = true)๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ช…์‹œ์ ์œผ๋กœ ์ฝ๊ธฐ ์ „์šฉ์ž„์„ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ฝ๊ธฐ ์ „์šฉ ํŠธ๋žœ์žญ์…˜ ์ถ”๊ฐ€ (์„ ํƒ ์‚ฌํ•ญ):
+    @Transactional(readOnly = true)
     public boolean isDuplicate(String eventId) {
         boolean exists = eventInboxRepository.existsByEventId(eventId);
apps/commerce-streamer/src/test/java/com/loopers/application/inbox/EventInboxServiceTest.java (1)

14-28: ํ…Œ์ŠคํŠธ ์„ค์ •์ด ์ ์ ˆํ•˜๋ฉฐ, BeforeEach ์ •๋ฆฌ ๋กœ์ง ๋‹จ์ˆœํ™” ๊ณ ๋ ค ๊ฐ€๋Šฅ

ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ๊ตฌ์„ฑ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. @Transactional ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ๊ฐ ํ…Œ์ŠคํŠธ ํ›„ ์ž๋™ ๋กค๋ฐฑ๋˜๋ฏ€๋กœ, BeforeEach์˜ deleteAll() ํ˜ธ์ถœ์€ ์ฒซ ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์ „ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ ๋ชฉ์ ์œผ๋กœ๋งŒ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ๊ตฌํ˜„๋„ ๋ฌธ์ œ์—†์ง€๋งŒ, ๋ช…์‹œ์ ์œผ๋กœ ์ฒซ ์‹คํ–‰๋งŒ์„ ์œ„ํ•œ ๊ฒƒ์ž„์„ ๋‚˜ํƒ€๋‚ด๋ ค๋ฉด @BeforeAll(static) ์‚ฌ์šฉ์„ ๊ณ ๋ คํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋งŒ @Transactional๊ณผ์˜ ์กฐํ•ฉ ์‹œ ์ฃผ์˜๊ฐ€ ํ•„์š”ํ•˜๋ฏ€๋กœ, ํ˜„์žฌ ๋ฐฉ์‹์ด ๋” ์•ˆ์ „ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java (1)

97-102: ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ํ›„ ์บ์‹œ ๋ฌดํšจํ™” ํ•„์ˆ˜ ๊ถŒ์žฅ

์žฌ๊ณ  ์†Œ์ง„ ์‹œ ์บ์‹œ ๋ฌดํšจํ™” ๋กœ์ง์—์„œ ์ค‘์š”ํ•œ ์ด์Šˆ:

  1. ํŠธ๋žœ์žญ์…˜ ํƒ€์ด๋ฐ ์ด์Šˆ (์ค‘์š”):

    • ํ˜„์žฌ ์บ์‹œ ๋ฌดํšจํ™”๊ฐ€ ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ์ „์— ์‹คํ–‰๋จ
    • ์ดํ›„ ์ฟ ํฐ ํ• ์ธ, ํฌ์ธํŠธ ์ฐจ๊ฐ, ์ฃผ๋ฌธ ์ €์žฅ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ๋˜์ง€๋งŒ ์บ์‹œ๋Š” ์ด๋ฏธ ๋ฌดํšจํ™”๋จ
    • ๊ฒฐ๊ณผ: DB์˜ ์žฌ๊ณ ๋Š” ์›์ƒ๋ณต๊ตฌ๋˜๋‚˜ ์บ์‹œ๋งŒ ๋ฌดํšจํ™” ์ƒํƒœ๋กœ ๋‚จ์•„ ๋ถˆ์ผ์น˜ ๋ฐœ์ƒ
    • @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) ์‚ฌ์šฉํ•˜์—ฌ ํŠธ๋žœ์žญ์…˜ ์„ฑ๊ณต ํ›„์—๋งŒ ์บ์‹œ ๋ฌดํšจํ™”
  2. ์กฐ๊ฑด ๊ฒ€ํ† :

    • ํ˜„์žฌ getStock() == 0 ์กฐ๊ฑด์€ ์ •ํ™•ํ•จ (deductStock()์ด ์Œ์ˆ˜ ๋ฐฉ์ง€)
    • ๋ฐฉ์–ด์  ์ฝ”๋”ฉ์„ ์›ํ•œ๋‹ค๋ฉด <= 0์œผ๋กœ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ
apps/commerce-streamer/src/test/java/com/loopers/application/metrics/ProductMetricsServiceTest.java (1)

41-42: ํ…Œ์ŠคํŠธ์˜ ๋””๋ฒ„๊น… ํŽธ์˜์„ฑ์„ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

orElseThrow()์— ๋ช…ํ™•ํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ํ…Œ์ŠคํŠธ ์‹คํŒจ ์‹œ ์›์ธ ํŒŒ์•…์ด ๋” ์‰ฌ์›Œ์ง‘๋‹ˆ๋‹ค.

๐Ÿ”Ž ๊ฐœ์„  ์ œ์•ˆ
-        ProductMetrics metrics = productMetricsRepository.findByProductId(productId)
-            .orElseThrow();
+        ProductMetrics metrics = productMetricsRepository.findByProductId(productId)
+            .orElseThrow(() -> new AssertionError("ProductMetrics not found for productId: " + productId));

๊ฐ ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ์— ๋™์ผํ•˜๊ฒŒ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Also applies to: 57-58, 72-73, 88-89

apps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.java (2)

53-57: ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ํŒจํ„ด์„ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

exceptionally ๋ธ”๋ก ๋‚ด์—์„œ RuntimeException์„ ๋˜์ง€๋Š” ํ˜„์žฌ ํŒจํ„ด์€ ๋™์ž‘ํ•˜์ง€๋งŒ ํ˜ผ๋ž€์Šค๋Ÿฌ์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. exceptionally ํ•ธ๋“ค๋Ÿฌ๋Š” ๋ณต๊ตฌ๋œ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๊ฑฐ๋‚˜, ์˜ˆ์™ธ๋ฅผ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ „ํŒŒํ•˜๋„๋ก ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ๋” ๋ช…ํ™•ํ•œ ํŒจํ„ด์œผ๋กœ ๊ฐœ์„ 

์˜ต์…˜ 1 (๊ถŒ์žฅ): exceptionally ๋ธ”๋ก ์ œ๊ฑฐ

์—๋Ÿฌ ๋กœ๊น…๋งŒ ์œ ์ง€ํ•˜๊ณ  ์˜ˆ์™ธ๋Š” ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ „ํŒŒ๋˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค:

-        return kafkaTemplate.send(topic, partitionKey, outbox.getPayload())
+        CompletableFuture<SendResult<Object, Object>> future = 
+            kafkaTemplate.send(topic, partitionKey, outbox.getPayload())
             .thenApply(result -> {
                 log.info("Kafka ๋ฐœํ–‰ ์„ฑ๊ณต - topic: {}, partition: {}, offset: {}, eventId: {}",
                     topic,
                     result.getRecordMetadata().partition(),
                     result.getRecordMetadata().offset(),
                     outbox.getId());
                 return result;
-            })
-            .exceptionally(ex -> {
-                log.error("Kafka ๋ฐœํ–‰ ์‹คํŒจ - topic: {}, key: {}, eventId: {}, error: {}",
-                    topic, partitionKey, outbox.getId(), ex.getMessage(), ex);
-                throw new RuntimeException("Kafka ๋ฐœํ–‰ ์‹คํŒจ", ex);
             });
+        
+        // ์—๋Ÿฌ ๋กœ๊น…์€ OutboxEventService์—์„œ ์ฒ˜๋ฆฌ
+        return future;

์˜ต์…˜ 2: whenComplete ์‚ฌ์šฉ

์„ฑ๊ณต/์‹คํŒจ ๋ชจ๋‘ ๋กœ๊น…ํ•˜๋˜ ๊ฒฐ๊ณผ๋Š” ๊ทธ๋Œ€๋กœ ์ „ํŒŒ:

         return kafkaTemplate.send(topic, partitionKey, outbox.getPayload())
-            .thenApply(result -> {
-                log.info("Kafka ๋ฐœํ–‰ ์„ฑ๊ณต - topic: {}, partition: {}, offset: {}, eventId: {}",
-                    topic,
-                    result.getRecordMetadata().partition(),
-                    result.getRecordMetadata().offset(),
-                    outbox.getId());
-                return result;
-            })
-            .exceptionally(ex -> {
+            .whenComplete((result, ex) -> {
+                if (ex != null) {
-                log.error("Kafka ๋ฐœํ–‰ ์‹คํŒจ - topic: {}, key: {}, eventId: {}, error: {}",
-                    topic, partitionKey, outbox.getId(), ex.getMessage(), ex);
-                throw new RuntimeException("Kafka ๋ฐœํ–‰ ์‹คํŒจ", ex);
+                    log.error("Kafka ๋ฐœํ–‰ ์‹คํŒจ - topic: {}, key: {}, eventId: {}, error: {}",
+                        topic, partitionKey, outbox.getId(), ex.getMessage(), ex);
+                } else {
+                    log.info("Kafka ๋ฐœํ–‰ ์„ฑ๊ณต - topic: {}, partition: {}, offset: {}, eventId: {}",
+                        topic,
+                        result.getRecordMetadata().partition(),
+                        result.getRecordMetadata().offset(),
+                        outbox.getId());
+                }
             });

ํ˜„์žฌ ์ฝ”๋“œ๋„ ๋™์ž‘ํ•˜์ง€๋งŒ, ์œ„ ํŒจํ„ด๋“ค์ด CompletableFuture์˜ ์˜๋„๋œ ์‚ฌ์šฉ๋ฒ•์— ๋” ๋ถ€ํ•ฉํ•ฉ๋‹ˆ๋‹ค.


23-23: ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

KafkaTemplate<Object, Object>๋Š” ๋งค์šฐ ์ผ๋ฐ˜์ ์ด์ง€๋งŒ, ์‹ค์ œ๋กœ๋Š” String ํƒ€์ž…์˜ ํŒŒํ‹ฐ์…˜ ํ‚ค์™€ ํŽ˜์ด๋กœ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋” ๊ตฌ์ฒด์ ์ธ ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜๋ฉด ์ปดํŒŒ์ผ ํƒ€์ž„์— ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ํ™•๋ณดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”Ž ํƒ€์ž… ์•ˆ์ „์„ฑ ๊ฐœ์„  ์ œ์•ˆ
-    private final KafkaTemplate<Object, Object> kafkaTemplate;
+    private final KafkaTemplate<String, String> kafkaTemplate;

     /**
      * Outbox ์ด๋ฒคํŠธ๋ฅผ Kafka๋กœ ๋ฐœํ–‰
      *
      * @param outbox Outbox ์ด๋ฒคํŠธ
      * @return CompletableFuture<SendResult>
      */
-    public CompletableFuture<SendResult<Object, Object>> publish(EventOutbox outbox) {
+    public CompletableFuture<SendResult<String, String>> publish(EventOutbox outbox) {

์ด ๋ณ€๊ฒฝ์€ KafkaTemplate ์„ค์ •์—์„œ๋„ ํƒ€์ž…์„ ๋ช…์‹œํ•ด์•ผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Also applies to: 37-44

apps/commerce-streamer/src/main/java/com/loopers/application/dlq/DeadLetterQueueService.java (1)

32-33: ๋ฉ”์„œ๋“œ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

6๊ฐœ์˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ์ธ์ง€์  ๋ถ€๋‹ด์ด ํฌ๊ณ  ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆœ์„œ๋ฅผ ์‹ค์ˆ˜ํ•˜๊ธฐ ์‰ฝ์Šต๋‹ˆ๋‹ค. ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ์ฒด๋ฅผ ๋„์ž…ํ•˜๋ฉด ๊ฐ€๋…์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ์ฒด ๋„์ž… ์ œ์•ˆ

1๋‹จ๊ณ„: ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ์ฒด ์ƒ์„ฑ

@Getter
@Builder
public class DLQSaveRequest {
    private final String originalTopic;
    private final String partitionKey;
    private final String eventId;
    private final String payload;
    private final String errorMessage;
    private final int retryCount;
}

2๋‹จ๊ณ„: ์„œ๋น„์Šค ๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ๊ฐœ์„ 

     @Transactional
-    public void save(String originalTopic, String partitionKey, String eventId,
-                     String payload, String errorMessage, int retryCount) {
+    public void save(DLQSaveRequest request) {

         DeadLetterQueue dlq = DeadLetterQueue.builder()
-            .originalTopic(originalTopic)
-            .partitionKey(partitionKey)
-            .eventId(eventId)
-            .payload(payload)
-            .errorMessage(errorMessage)
-            .retryCount(retryCount)
+            .originalTopic(request.getOriginalTopic())
+            .partitionKey(request.getPartitionKey())
+            .eventId(request.getEventId())
+            .payload(request.getPayload())
+            .errorMessage(request.getErrorMessage())
+            .retryCount(request.getRetryCount())
             .build();

         deadLetterQueueRepository.save(dlq);

         log.error("โš ๏ธ DLQ์— ๋ฉ”์‹œ์ง€ ์ €์žฅ - topic: {}, eventId: {}, retryCount: {}, error: {}",
-            originalTopic, eventId, retryCount, errorMessage);
+            request.getOriginalTopic(), request.getEventId(), 
+            request.getRetryCount(), request.getErrorMessage());
     }

3๋‹จ๊ณ„: Consumer ํ˜ธ์ถœ๋ถ€ ์ˆ˜์ • ์˜ˆ์‹œ

-    deadLetterQueueService.save(
-        record.topic(),
-        record.key() != null ? record.key().toString() : null,
-        eventId,
-        payload,
-        error.getMessage(),
-        retryCount
-    );
+    deadLetterQueueService.save(
+        DLQSaveRequest.builder()
+            .originalTopic(record.topic())
+            .partitionKey(record.key() != null ? record.key().toString() : null)
+            .eventId(eventId)
+            .payload(payload)
+            .errorMessage(error.getMessage())
+            .retryCount(retryCount)
+            .build()
+    );

์ด ๋ณ€๊ฒฝ์€ ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆœ์„œ ์‹ค์ˆ˜๋ฅผ ๋ฐฉ์ง€ํ•˜๊ณ , ๋นŒ๋” ํŒจํ„ด์œผ๋กœ ๋ช…ํ™•ํ•œ ์˜๋„ ์ „๋‹ฌ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.java (1)

95-99: ์ข‹์•„์š” ์ˆ˜ ๊ฐ์†Œ ์‹œ ์Œ์ˆ˜ ๋ฐฉ์ง€ ๋กœ์ง์„ ํ™•์ธํ•˜์„ธ์š”.

ํ˜„์žฌ likeCount > 0 ์ฒดํฌ๋กœ ์Œ์ˆ˜๋ฅผ ๋ฐฉ์ง€ํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, 0์ผ ๋•Œ๋Š” ๋ฌด์‹œ๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์•ˆ์ „ํ•œ ๋ฐฉ์–ด ๋กœ์ง์ด์ง€๋งŒ, ๋น„์ฆˆ๋‹ˆ์Šค ์š”๊ตฌ์‚ฌํ•ญ์— ๋”ฐ๋ผ ๋กœ๊น…์ด๋‚˜ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ข‹์•„์š”๊ฐ€ ์—†๋Š”๋ฐ ์‚ญ์ œ ์ด๋ฒคํŠธ๊ฐ€ ์˜ค๋Š” ๊ฒƒ์€ ๋น„์ •์ƒ ์ƒํ™ฉ์ผ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ๋ชจ๋‹ˆํ„ฐ๋ง ๊ด€์ ์—์„œ ๊ฒฝ๊ณ  ๋กœ๊ทธ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜์„ธ์š”.

๐Ÿ”Ž ์„ ํƒ์  ๊ฐœ์„ : ๊ฒฝ๊ณ  ๋กœ๊ทธ ์ถ”๊ฐ€
 public void decrementLikeCount() {
     if (this.likeCount > 0) {
         this.likeCount--;
+    } else {
+        log.warn("์ข‹์•„์š” ์ˆ˜๊ฐ€ ์ด๋ฏธ 0์ธ ์ƒํƒœ์—์„œ ๊ฐ์†Œ ์‹œ๋„ - productId: {}", this.productId);
     }
 }
apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/CatalogEventConsumerTest.java (1)

46-52: ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€ ์‹œ DeadLetterQueueService Mock ๊ฐ์ฒด ๊ถŒ์žฅ

ํ˜„์žฌ ํ…Œ์ŠคํŠธ๋Š” ์ •์ƒ ํ”Œ๋กœ์šฐ๋งŒ ๊ฒ€์ฆํ•˜๊ณ  ์žˆ์œผ๋ฏ€๋กœ null ์ฃผ์ž…์ด ๋ฌธ์ œ๊ฐ€ ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๊ตฌํ˜„๋ถ€์—์„œ processEvent() ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ sendToDLQ()๋ฅผ ํ†ตํ•ด deadLetterQueueService.save()๋ฅผ ํ˜ธ์ถœํ•˜๋ฏ€๋กœ, ํ–ฅํ›„ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ๋Š” DeadLetterQueueService๋ฅผ Mock ๊ฐ์ฒด๋กœ ๋Œ€์ฒดํ•˜์—ฌ NPE๋ฅผ ๋ฐฉ์ง€ํ•˜๋Š” ๊ฒƒ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DeadLetterQueue.java (2)

46-47: ํ•„๋“œ ์ดˆ๊ธฐํ™” ์ค‘๋ณต์„ ์ œ๊ฑฐํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜์„ธ์š”.

retryCount ํ•„๋“œ์— = 0์œผ๋กœ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ๊ฒƒ์€ ๋นŒ๋”(Line 61)์—์„œ ์ด๋ฏธ null ์ฒดํฌ๋ฅผ ํ†ตํ•ด ๊ธฐ๋ณธ๊ฐ’์„ ์„ค์ •ํ•˜๊ณ  ์žˆ์–ด ์ค‘๋ณต์ž…๋‹ˆ๋‹ค. JPA๋Š” protected ์ƒ์„ฑ์ž๋ฅผ ํ†ตํ•ด ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ƒ์„ฑํ•˜๋ฏ€๋กœ ํ•„๋“œ ์ดˆ๊ธฐํ™”๊ฐ€ ์ ์šฉ๋˜์ง€๋งŒ, ๋นŒ๋” ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ๋นŒ๋” ๋‚ด๋ถ€์˜ ๊ธฐ๋ณธ๊ฐ’ ์„ค์ •๋งŒ์œผ๋กœ ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ํ•„๋“œ ์ดˆ๊ธฐํ™”๋ฅผ ์ œ๊ฑฐํ•˜๋Š” diff ์ ์šฉ
-    @Column(name = "retry_count", nullable = false)
-    private Integer retryCount = 0;
+    @Column(name = "retry_count", nullable = false)
+    private Integer retryCount;

1-64: MAX_RETRY ์ƒ์ˆ˜๊ฐ€ ์ •์˜๋˜์—ˆ์ง€๋งŒ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

CatalogEventConsumer์™€ OrderEventConsumer์—์„œ MAX_RETRY = 3 ์ƒ์ˆ˜๊ฐ€ ์ •์˜๋˜์–ด ์žˆ์ง€๋งŒ, DLQ์— ์ €์žฅํ•  ๋•Œ ํ•ญ์ƒ retryCount = 0์œผ๋กœ ํ•˜๋“œ์ฝ”๋”ฉ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์žฌ์‹œ๋„ ๋กœ์ง์„ ๊ตฌํ˜„ํ•  ๊ณ„ํš์ด๋ผ๋ฉด ์ด ์ƒ์ˆ˜๋ฅผ ํ™œ์šฉํ•˜๋„๋ก ๊ฐœ์„ ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java (1)

1-194: CatalogEventConsumer์™€ ์ฝ”๋“œ ์ค‘๋ณต์ด ์‹ฌ๊ฐํ•ฉ๋‹ˆ๋‹ค.

OrderEventConsumer์™€ CatalogEventConsumer๊ฐ€ ๊ฑฐ์˜ ๋™์ผํ•œ ํŒจํ„ด์„ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค:

  • ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๋ฐ ์ˆ˜๋™ ์ปค๋ฐ‹ ๋กœ์ง (Lines 39-72)
  • DLQ ์ „์†ก ๋กœ์ง (Lines 163-180)
  • EventId ์ถ”์ถœ ๋กœ์ง (Lines 185-193)

๊ถŒ์žฅ ํ•ด๊ฒฐ์ฑ…:
์ถ”์ƒ ๋ฒ ์ด์Šค ํด๋ž˜์Šค ๋˜๋Š” ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค๋ฅผ ๋„์ž…ํ•˜์—ฌ ์ค‘๋ณต์„ ์ œ๊ฑฐํ•˜๊ณ  ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ค์„ธ์š”.

๐Ÿ”Ž ์ถ”์ƒ ๋ฒ ์ด์Šค ํด๋ž˜์Šค ์˜ˆ์‹œ
@Slf4j
@RequiredArgsConstructor
public abstract class AbstractBatchEventConsumer {
    
    protected final EventInboxService eventInboxService;
    protected final DeadLetterQueueService deadLetterQueueService;
    protected final ObjectMapper objectMapper;
    
    protected void processBatch(
        List<ConsumerRecord<Object, Object>> records,
        Acknowledgment acknowledgment
    ) {
        log.info("๐Ÿ“ฆ ์ด๋ฒคํŠธ ์ˆ˜์‹  - count: {}", records.size());
        
        int successCount = 0;
        int skipCount = 0;
        int failCount = 0;
        
        for (ConsumerRecord<Object, Object> record : records) {
            try {
                boolean processed = processEvent(record);
                if (processed) {
                    successCount++;
                } else {
                    skipCount++;
                }
            } catch (Exception e) {
                failCount++;
                log.error("โŒ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์‹คํŒจ - partition: {}, offset: {}, key: {}, error: {}",
                    record.partition(), record.offset(), record.key(), e.getMessage(), e);
                sendToDLQ(record, e, 0);
            }
        }
        
        acknowledgment.acknowledge();
        
        log.info("โœ… ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์™„๋ฃŒ - success: {}, skip: {}, fail: {}, total: {}",
            successCount, skipCount, failCount, records.size());
    }
    
    protected abstract boolean processEvent(ConsumerRecord<Object, Object> record) throws Exception;
    
    // sendToDLQ, extractEventId ๋“ฑ ๊ณตํ†ต ๋ฉ”์„œ๋“œ...
}

๊ทธ๋Ÿฐ ๋‹ค์Œ ๊ฐ Consumer๊ฐ€ ์ด๋ฅผ ์ƒ์†:

@Component
public class OrderEventConsumer extends AbstractBatchEventConsumer {
    
    private final ProductMetricsService productMetricsService;
    
    @KafkaListener(...)
    public void consumeOrderEvents(
        List<ConsumerRecord<Object, Object>> records,
        Acknowledgment acknowledgment
    ) {
        processBatch(records, acknowledgment);
    }
    
    @Override
    @Transactional
    protected boolean processEvent(ConsumerRecord<Object, Object> record) throws Exception {
        // Order ํŠนํ™” ๋กœ์ง๋งŒ ๊ตฌํ˜„
    }
}
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.java (1)

33-33: MAX_RETRY ์ƒ์ˆ˜๊ฐ€ ์ •์˜๋˜์—ˆ์ง€๋งŒ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Line 33์— MAX_RETRY = 3์ด ์ •์˜๋˜์–ด ์žˆ์ง€๋งŒ, ์‹ค์ œ๋กœ๋Š” Line 64์—์„œ retryCount = 0์œผ๋กœ ํ•˜๋“œ์ฝ”๋”ฉ๋˜์–ด ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. OrderEventConsumer์—๋„ ๋™์ผํ•œ ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

์žฌ์‹œ๋„ ๋กœ์ง์„ ๊ตฌํ˜„ํ•  ๊ณ„ํš์ด ์—†๋‹ค๋ฉด ์ด ์ƒ์ˆ˜๋ฅผ ์ œ๊ฑฐํ•˜๊ฑฐ๋‚˜, ์žฌ์‹œ๋„๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค๋ฉด ์ด ๊ฐ’์„ ํ™œ์šฉํ•˜๋„๋ก ๊ฐœ์„ ํ•˜์„ธ์š”.

๐Ÿ“œ Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

๐Ÿ“ฅ Commits

Reviewing files that changed from the base of the PR and between e625896 and adece8b.

๐Ÿ“’ Files selected for processing (69)
  • apps/commerce-api/build.gradle.kts (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/dataplatform/DataPlatformEventHandler.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java (3 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventPublisher.java (2 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.java (3 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheService.java (2 hunks)
  • apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java (0 hunks)
  • apps/commerce-api/src/main/java/com/loopers/config/CacheConfig.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/dataplatform/DataPlatformClientConfig.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PgPaymentRequest.java (0 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/ApiControllerAdvice.java (1 hunks)
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentApi.java (0 hunks)
  • apps/commerce-api/src/main/resources/application.yml (2 hunks)
  • apps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceCircuitBreakerTest.java (3 hunks)
  • apps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceFallbackTest.java (0 hunks)
  • apps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceRetryTest.java (0 hunks)
  • apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleModelTest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/domain/example/ExampleServiceIntegrationTest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java (1 hunks)
  • apps/commerce-api/src/test/java/com/loopers/support/error/CoreExceptionTest.java (1 hunks)
  • apps/commerce-streamer/build.gradle.kts (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/application/dlq/DeadLetterQueueService.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/application/inbox/EventInboxService.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DeadLetterQueue.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DeadLetterQueueRepository.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInbox.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInboxRepository.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueJpaRepository.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueRepositoryImpl.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxJpaRepository.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxRepositoryImpl.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/DemoKafkaConsumer.java (1 hunks)
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java (1 hunks)
  • apps/commerce-streamer/src/main/resources/application.yml (2 hunks)
  • apps/commerce-streamer/src/test/java/com/loopers/application/inbox/EventInboxServiceTest.java (1 hunks)
  • apps/commerce-streamer/src/test/java/com/loopers/application/metrics/ProductMetricsServiceTest.java (1 hunks)
  • apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/CatalogEventConsumerTest.java (1 hunks)
  • apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/ConsumerSmokeTest.java (1 hunks)
  • apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/OrderEventConsumerTest.java (1 hunks)
  • docker/init-db.sql (1 hunks)
  • modules/jpa/src/main/java/com/loopers/domain/BaseEntity.java (1 hunks)
  • modules/jpa/src/testFixtures/java/com/loopers/utils/DatabaseCleanUp.java (1 hunks)
  • modules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.java (1 hunks)
  • modules/kafka/src/main/resources/kafka.yml (2 hunks)
  • modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment1_AutoCommitTest.java (1 hunks)
  • modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment2_OffsetResetTest.java (1 hunks)
  • modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment3_PollIntervalTest.java (1 hunks)
  • modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment4_BatchSizeTest.java (1 hunks)
  • modules/kafka/src/test/java/com/loopers/kafka/learning/README.md (1 hunks)
  • modules/pg-simulator/src/main/kotlin/com/loopers/PaymentGatewayApplication.kt (1 hunks)
  • modules/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.kt (1 hunks)
  • modules/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt (1 hunks)
  • modules/pg-simulator/src/main/kotlin/com/loopers/domain/payment/TransactionKeyGenerator.kt (1 hunks)
  • modules/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt (0 hunks)
  • modules/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentApi.kt (1 hunks)
  • modules/redis/src/main/java/com/loopers/config/redis/RedisConfig.java (1 hunks)
  • modules/redis/src/main/java/com/loopers/config/redis/RedisProperties.java (1 hunks)
  • performance-test/k6/product-load-test-fixed.js (1 hunks)
  • performance-test/k6/product-load-test.js (1 hunks)
๐Ÿ’ค Files with no reviewable changes (6)
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/payment/PgPaymentRequest.java
  • apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java
  • apps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceRetryTest.java
  • apps/commerce-api/src/main/java/com/loopers/interfaces/api/payment/PaymentApi.java
  • modules/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/ApiControllerAdvice.kt
  • apps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceFallbackTest.java
๐Ÿงฐ Additional context used
๐Ÿง  Learnings (10)
๐Ÿ““ Common learnings
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 190
File: apps/commerce-streamer/src/main/java/com/loopers/applications/streamer/consumer/product/IncreaseProductViewKafkaConsumer.java:25-35
Timestamp: 2025-12-18T13:24:54.339Z
Learning: In this codebase, Kafka consumers delegate error handling and event tracking to the service layer via EventInboxAspect. Service methods annotated with InboxEvent are intercepted by the aspect, which handles failures by saving failed EventInbox entries and logging errors. This centralized approach avoids duplicating error handling logic across multiple consumers.
๐Ÿ“š 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/DemoKafkaConsumer.java
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.java
  • apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java
๐Ÿ“š Learning: 2025-11-09T10:41:39.297Z
Learnt from: ghojeong
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 25
File: apps/commerce-api/src/main/kotlin/com/loopers/domain/product/ProductRepository.kt:1-12
Timestamp: 2025-11-09T10:41:39.297Z
Learning: In this codebase, domain repository interfaces are allowed to use Spring Data's org.springframework.data.domain.Page and org.springframework.data.domain.Pageable types. This is an accepted architectural decision and should not be flagged as a DIP violation.

Applied to files:

  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxJpaRepository.java
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueJpaRepository.java
๐Ÿ“š Learning: 2025-12-16T09:44:15.945Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 58
File: apps/commerce-api/src/main/kotlin/com/loopers/domain/payment/PaymentService.kt:157-183
Timestamp: 2025-12-16T09:44:15.945Z
Learning: In PaymentService.requestPgPayment (apps/commerce-api/src/main/kotlin/com/loopers/domain/payment/PaymentService.kt), PG payment requests are idempotent using paymentId as the idempotency key, so Retryable covering the entire method including the pgClient.requestPayment call is safe and will not cause duplicate charges even if retries occur due to ObjectOptimisticLockingFailureException.

Applied to files:

  • modules/pg-simulator/src/main/kotlin/com/loopers/domain/payment/Payment.kt
  • apps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceCircuitBreakerTest.java
  • modules/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.kt
  • modules/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentApi.kt
๐Ÿ“š Learning: 2025-12-18T13:24:54.339Z
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 190
File: apps/commerce-streamer/src/main/java/com/loopers/applications/streamer/consumer/product/IncreaseProductViewKafkaConsumer.java:25-35
Timestamp: 2025-12-18T13:24:54.339Z
Learning: In this codebase, Kafka consumers delegate error handling and event tracking to the service layer via EventInboxAspect. Service methods annotated with InboxEvent are intercepted by the aspect, which handles failures by saving failed EventInbox entries and logging errors. This centralized approach avoids duplicating error handling logic across multiple consumers.

Applied to files:

  • apps/commerce-streamer/src/main/java/com/loopers/application/inbox/EventInboxService.java
  • apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/OrderEventConsumerTest.java
  • apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.java
  • apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxRepositoryImpl.java
  • apps/commerce-streamer/src/test/java/com/loopers/application/inbox/EventInboxServiceTest.java
  • apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/CatalogEventConsumerTest.java
  • apps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInbox.java
๐Ÿ“š Learning: 2025-12-07T15:47:18.277Z
Learnt from: toongri
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 52
File: apps/commerce-api/src/main/kotlin/com/loopers/infrastructure/payment/PgFeignConfig.kt:0-0
Timestamp: 2025-12-07T15:47:18.277Z
Learning: In Spring Cloud OpenFeign, configuration classes specified in FeignClient(configuration = ...) do NOT need the Configuration annotation. Spring Cloud OpenFeign creates a separate ApplicationContext for each named client and directly instantiates Bean methods from the configuration class. Adding Configuration would cause the class to be registered globally via ComponentScan, making the configuration apply to ALL Feign clients instead of just the specific client, which is usually undesirable.

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java
  • apps/commerce-api/src/main/java/com/loopers/infrastructure/dataplatform/DataPlatformClientConfig.java
๐Ÿ“š Learning: 2025-12-18T01:01:55.894Z
Learnt from: jikimee64
Repo: Loopers-dev-lab/loopers-spring-kotlin-template PR: 65
File: modules/kafka/src/main/resources/kafka.yml:25-32
Timestamp: 2025-12-18T01:01:55.894Z
Learning: In Spring Boot Kafka configuration YAML, properties inside spring.kafka.producer.properties and spring.kafka.consumer.properties maps must use exact Kafka client property names with dot notation, and must be quoted with bracket notation like "[enable.idempotence]": true and "[enable.auto.commit]": false to prevent YAML from parsing dots as nested keys. Spring Boot's relaxed binding only applies to top-level Spring Kafka properties, not to the properties map.

Applied to files:

  • modules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.java
  • modules/kafka/src/main/resources/kafka.yml
๐Ÿ“š Learning: 2025-11-20T13:22:51.519Z
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 98
File: core/domain/src/main/java/com/loopers/core/domain/order/DefaultCoupon.java:15-62
Timestamp: 2025-11-20T13:22:51.519Z
Learning: In the order domain coupon implementation, DefaultCoupon serves as an internal helper/DTO class to carry AbstractCoupon's base fields during database reconstruction. It is used as an intermediate object when converting CouponEntity to specific coupon types (RateDiscountCoupon or AmountDiscountCoupon). The actual discount information (rate/amount) is stored in separate tables (rate_discount_coupon and amount_discount_coupon) and fetched via RateDiscountCouponJpaRepository and AmountDiscountCouponJpaRepository. DefaultCoupon is never returned to callers directly.

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
๐Ÿ“š Learning: 2025-11-27T09:09:24.961Z
Learnt from: sky980221
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 121
File: apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java:22-24
Timestamp: 2025-11-27T09:09:24.961Z
Learning: Product ์—”ํ‹ฐํ‹ฐ (apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java)๋Š” ์œ ์ฆˆ์ผ€์ด์Šค๋ณ„๋กœ ์˜๋„์ ์œผ๋กœ ๋‹ค๋ฅธ ๋ฝ ์ „๋žต์„ ์‚ฌ์šฉํ•œ๋‹ค: ์ข‹์•„์š” ๊ธฐ๋Šฅ์—๋Š” ๋น„๊ด€์  ๋ฝ(findByIdForUpdate)์„, ์žฌ๊ณ  ์ฐจ๊ฐ์—๋Š” ๋‚™๊ด€์  ๋ฝ(Version + ์žฌ์‹œ๋„)์„ ์‚ฌ์šฉํ•œ๋‹ค.

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java
  • apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java
  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheService.java
  • apps/commerce-streamer/src/test/java/com/loopers/application/metrics/ProductMetricsServiceTest.java
  • apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.java
๐Ÿ“š Learning: 2025-12-02T08:12:06.383Z
Learnt from: junoade
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 0
File: :0-0
Timestamp: 2025-12-02T08:12:06.383Z
Learning: ProductQueryService์—์„œ ์ƒํ’ˆ ๋ชฉ๋ก ์กฐํšŒ ์‹œ Redis ์บ์‹œ๋ฅผ ์ ์šฉํ–ˆ์œผ๋ฉฐ, ์บ์‹œ ํ‚ค๋Š” brandId, sortType, pageNumber, pageSize์˜ ์กฐํ•ฉ์œผ๋กœ ๊ตฌ์„ฑ๋˜๊ณ  TTL์€ 5๋ถ„์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ๋‹ค.

Applied to files:

  • apps/commerce-api/src/main/java/com/loopers/application/product/ProductCacheService.java
๐Ÿงฌ Code graph analysis (10)
modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment2_OffsetResetTest.java (3)
modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment1_AutoCommitTest.java (1)
  • DisplayName (31-226)
modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment3_PollIntervalTest.java (1)
  • DisplayName (32-241)
modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment4_BatchSizeTest.java (1)
  • DisplayName (30-357)
apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DeadLetterQueue.java (1)
apps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInbox.java (1)
  • Getter (18-55)
apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java (1)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/example/ExampleV1Dto.java (1)
  • ExampleV1Dto (5-15)
apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.java (4)
apps/commerce-streamer/src/main/java/com/loopers/application/dlq/DeadLetterQueueService.java (1)
  • Slf4j (14-49)
apps/commerce-streamer/src/main/java/com/loopers/application/inbox/EventInboxService.java (1)
  • Slf4j (14-59)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.java (1)
  • Slf4j (23-167)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java (1)
  • Slf4j (24-194)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java (4)
apps/commerce-streamer/src/main/java/com/loopers/application/dlq/DeadLetterQueueService.java (1)
  • Slf4j (14-49)
apps/commerce-streamer/src/main/java/com/loopers/application/inbox/EventInboxService.java (1)
  • Slf4j (14-59)
apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.java (1)
  • Slf4j (15-90)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.java (1)
  • Slf4j (23-167)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxRepositoryImpl.java (2)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueRepositoryImpl.java (1)
  • Repository (11-21)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (1)
  • Repository (12-32)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (2)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueRepositoryImpl.java (1)
  • Repository (11-21)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxRepositoryImpl.java (1)
  • Repository (11-31)
apps/commerce-streamer/src/main/java/com/loopers/application/dlq/DeadLetterQueueService.java (2)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.java (1)
  • Slf4j (23-167)
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/OrderEventConsumer.java (1)
  • Slf4j (24-194)
apps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.java (2)
apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventPublisher.java (1)
  • Slf4j (17-87)
apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.java (1)
  • Slf4j (20-126)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueRepositoryImpl.java (2)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxRepositoryImpl.java (1)
  • Repository (11-31)
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (1)
  • Repository (12-32)
๐Ÿ”‡ Additional comments (70)
modules/redis/src/main/java/com/loopers/config/redis/RedisConfig.java (1)

5-6: ํ•„์š”ํ•œ import๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

ํŒŒ์ผ์—์„œ ์‚ฌ์šฉ๋˜๋Š” List(35, 47์ค„)์™€ Consumer(75์ค„)๋ฅผ ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ import ์„น์…˜์— ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค. Import ๊ตฌ์„ฑ๋„ ๊ด€๋ก€๋ฅผ ๋”ฐ๋ฅด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

modules/jpa/src/testFixtures/java/com/loopers/utils/DatabaseCleanUp.java (1)

7-8: โœ… Import ์ •๋ฆฌ ์ž˜ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

java.util.ArrayList์™€ java.util.List import๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ์œ„์น˜๋กœ ์ •๋ ฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ฝ”๋“œ์—์„œ ์‹ค์ œ๋กœ ์‚ฌ์šฉ๋˜๋Š” import์ด๋ฉฐ (line 19), ํ‘œ์ค€ Java import ์ˆœ์„œ ๊ทœ์น™์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. ์ค‘๋ณต ์ œ๊ฑฐ ๋ฐ ์กฐ์งํ™”๊ฐ€ ๊น”๋”ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

performance-test/k6/product-load-test-fixed.js (1)

2-3: ์Šคํƒ€์ผ ์ •๋ฆฌ๋งŒ ์ ์šฉ๋จ โ€” ๊ธฐ๋Šฅ ๋ณ€๊ฒฝ ์—†์Œ

import ๋ฌธ์˜ ๊ณต๋ฐฑ๋งŒ ์ œ๊ฑฐ๋œ ํฌ๋งทํŒ… ๋ณ€๊ฒฝ์ž…๋‹ˆ๋‹ค. ๊ตฌ๋ฌธ, ๊ธฐ๋Šฅ, ๋˜๋Š” ๋Ÿฐํƒ€์ž„ ๋™์ž‘์— ์˜ํ–ฅ์„ ๋ฏธ์น˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋‹ค๋งŒ ์ด ํŒŒ์ผ์€ Kafka ์ด๋ฒคํŠธ ํŒŒ์ดํ”„๋ผ์ธ PR์˜ ๋ฉ”์ธ ๋ชฉํ‘œ(์ƒ์‚ฐ์ž, ์†Œ๋น„์ž, ํŠธ๋žœ์žญ์…˜ ์•„์›ƒ๋ฐ•์Šค, ์ˆ˜์‹ ํ•จ ํŒจํ„ด ๋“ฑ)์™€ ๋ณ„๊ฐœ์ž…๋‹ˆ๋‹ค. ๋กœ๋“œ ํ…Œ์ŠคํŠธ ๊ฐœ์„ ๊ฐ€ ๊ด€๋ จ ์žˆ๋‹ค๋ฉด ๊ฐ์ฃผ๋กœ ๋ช…์‹œํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/CommerceStreamerApplication.java (1)

4-4: LGTM! ์ค‘๋ณต import ์ œ๊ฑฐ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

์ค‘๋ณต๋œ TimeZone import๋ฅผ ํ•˜๋‚˜๋กœ ํ†ตํ•ฉํ•œ ๊ฒƒ์€ ์ฝ”๋“œ ํ’ˆ์งˆ์„ ๊ฐœ์„ ํ•˜๋Š” ์ข‹์€ ์ •๋ฆฌ ์ž‘์—…์ž…๋‹ˆ๋‹ค.

performance-test/k6/product-load-test.js (1)

15-16: LGTM! ํฌ๋งทํŒ… ๋ณ€๊ฒฝ์ด ์ •์ƒ์ ์œผ๋กœ ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

import ๋ฌธ์—์„œ ์ค‘๊ด„ํ˜ธ ๋‚ด๋ถ€ ๊ณต๋ฐฑ์ด ์ œ๊ฑฐ๋˜์—ˆ์œผ๋ฉฐ, ๋ฌธ๋ฒ•์ ์œผ๋กœ ์ •ํ™•ํ•˜๊ณ  ๊ธฐ๋Šฅ์— ์˜ํ–ฅ์„ ๋ฏธ์น˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

modules/pg-simulator/src/main/kotlin/com/loopers/interfaces/api/payment/PaymentApi.kt (1)

5-8: PR ๋ชฉํ‘œ์™€ ๋ฌด๊ด€ํ•œ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ž…๋‹ˆ๋‹ค.

์ด ๋ณ€๊ฒฝ์‚ฌํ•ญ์€ import ๊ตฌ๋ฌธ ์žฌ์ •๋ ฌ๋งŒ ํฌํ•จํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, Kafka ์ด๋ฒคํŠธ ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌํ˜„์ด๋ผ๋Š” PR์˜ ์ฃผ์š” ๋ชฉํ‘œ์™€ ์ง์ ‘์ ์ธ ์—ฐ๊ด€์„ฑ์ด ์—†์Šต๋‹ˆ๋‹ค. ๋™์ž‘์—๋Š” ์˜ํ–ฅ์ด ์—†์œผ๋ฏ€๋กœ ๋ฌธ์ œ๋Š” ์—†์œผ๋‚˜, ํ–ฅํ›„ PR ๋ฒ”์œ„๋ฅผ ๋ช…ํ™•ํžˆ ํ•˜๊ธฐ ์œ„ํ•ด ๊ด€๋ จ ์—†๋Š” ํฌ๋งทํŒ… ๋ณ€๊ฒฝ์€ ๋ณ„๋„๋กœ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”.

modules/pg-simulator/src/main/kotlin/com/loopers/application/payment/PaymentApplicationService.kt (1)

3-3: LGTM! ์ž„ํฌํŠธ ์ •๋ฆฌ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

๋™์ผํ•œ ๋„๋ฉ”์ธ ํŒจํ‚ค์ง€(com.loopers.domain.payment)์—์„œ ์—ฌ๋Ÿฌ ํด๋ž˜์Šค๋ฅผ ์ž„ํฌํŠธํ•˜๋Š” ๊ฒฝ์šฐ ์™€์ผ๋“œ์นด๋“œ ์ž„ํฌํŠธ๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์€ Kotlin์˜ ์ผ๋ฐ˜์ ์ธ ๊ด€๋ก€๋ฅผ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์ž„ํฌํŠธ๋“ค๊ณผ์˜ ๋„ค์ด๋ฐ ์ถฉ๋Œ ์œ„ํ—˜๋„ ์—†์œผ๋ฉฐ, ์ฝ”๋“œ์˜ ๋™์ž‘์—๋Š” ์˜ํ–ฅ์ด ์—†์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/test/java/com/loopers/application/payment/PaymentServiceCircuitBreakerTest.java (1)

7-7: ์žฌ์‹œ๋„ ๋™์ž‘์„ ๊ณ ๋ คํ•œ ์ ์ ˆํ•œ import ๋ณ€๊ฒฝ์ž…๋‹ˆ๋‹ค.

times์—์„œ atLeast๋กœ ๋ณ€๊ฒฝํ•œ ๊ฒƒ์€ PaymentService์˜ ์žฌ์‹œ๋„ ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ๊ณ ๋ คํ•  ๋•Œ ์ •ํ™•ํ•œ ํ˜ธ์ถœ ํšŸ์ˆ˜๋ฅผ ์˜ˆ์ธกํ•˜๊ธฐ ์–ด๋ ต๊ธฐ ๋•Œ๋ฌธ์— ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” 158๋ฒˆ ๋ผ์ธ์˜ ๊ฒ€์ฆ ๋กœ์ง ๋ณ€๊ฒฝ๊ณผ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.

modules/kafka/src/main/java/com/loopers/confg/kafka/KafkaConfig.java (2)

4-5: ๋ช…์‹œ์  import๋กœ ๊ฐœ์„ ํ•˜์—ฌ ์ข‹์Šต๋‹ˆ๋‹ค.

์™€์ผ๋“œ์นด๋“œ import๋ฅผ ๋ช…์‹œ์ ์ธ import๋กœ ๋ณ€๊ฒฝํ•œ ๊ฒƒ์€ ์ฝ”๋“œ ๊ฐ€๋…์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ค๋Š” ์ข‹์€ ๊ด€ํ–‰์ž…๋‹ˆ๋‹ค.

Also applies to: 13-17


28-28: MAX_POLL_RECORDS ๊ฐ’์ด PR ์„ค๋ช…๊ณผ ๋ถˆ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

์ฝ”๋“œ์˜ Line 28์—์„œ MAX_POLLING_SIZE๊ฐ€ 3000์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ๊ณ , ์ด ๊ฐ’์ด Line 63์—์„œ MAX_POLL_RECORDS_CONFIG๋กœ ์ ์šฉ๋˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. PR ์„ค๋ช…์—์„œ max.poll.records=100์œผ๋กœ ๋ช…์‹œ๋œ ๊ฒฝ์šฐ, ์„ค์ •๋œ ๊ฐ’์ด ์˜๋„ํ•œ ๊ฐ’๊ณผ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•ด ์ฃผ์„ธ์š”. 3000์€ ๋ฐฐ์น˜ ํฌ๊ธฐ์™€ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์— ์˜ํ–ฅ์„ ๋ฏธ์นฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/DemoKafkaConsumer.java (1)

4-4: Import ์ •๋ฆฌ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

Import ์ˆœ์„œ๋ฅผ ์ •๋ฆฌํ•œ ๋ณ€๊ฒฝ์‚ฌํ•ญ์œผ๋กœ, ์ฝ”๋“œ ๊ตฌ์กฐ์—๋Š” ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment2_OffsetResetTest.java (4)

1-44: ํ•™์Šต ํ…Œ์ŠคํŠธ ๊ตฌ์กฐ๊ฐ€ ์ž˜ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

auto.offset.reset ์„ค์ •์˜ ๋™์ž‘ ๋ฐฉ์‹์„ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ณด์—ฌ์ฃผ๋Š” ์„ธ ๊ฐ€์ง€ ์‹œ๋‚˜๋ฆฌ์˜ค(earliest, latest, ๊ธฐ์กด ์˜คํ”„์…‹ ์กด์žฌ)๊ฐ€ ์ž˜ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. EmbeddedKafka ์„ค์ •๊ณผ ์ƒ์ˆ˜ ์ •์˜๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.


45-89: LGTM!

earliest ์„ค์ •์˜ ๋™์ž‘์„ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ณด์—ฌ์ฃผ๋Š” ํ…Œ์ŠคํŠธ์ž…๋‹ˆ๋‹ค. Producer๊ฐ€ ๋จผ์ € ๋ฉ”์‹œ์ง€๋ฅผ ์ „์†กํ•˜๊ณ , Consumer๊ฐ€ ๋‚˜์ค‘์— ์‹œ์ž‘ํ•ด๋„ ๊ณผ๊ฑฐ ๋ฉ”์‹œ์ง€๋ฅผ ๋ชจ๋‘ ์ฝ๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค๊ฐ€ ์ž˜ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.


91-145: LGTM!

latest ์„ค์ • ํ…Œ์ŠคํŠธ๊ฐ€ ์ž˜ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. Consumer ์‹œ์ž‘ ์ „ ๋ฉ”์‹œ์ง€๋Š” ๋ฌด์‹œํ•˜๊ณ , ์‹œ์ž‘ ํ›„ ๋ฉ”์‹œ์ง€๋งŒ ์ฝ๋Š” ๋™์ž‘์„ ๋‘ ๋ฒˆ์˜ poll๋กœ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.


147-207: LGTM!

๊ธฐ์กด ์˜คํ”„์…‹์ด ์žˆ์„ ๋•Œ auto.offset.reset ์„ค์ •์ด ๋ฌด์‹œ๋˜๋Š” ๋™์ž‘์„ ์ž˜ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ์ฒซ ์‹คํ–‰์—์„œ ์ปค๋ฐ‹ ํ›„ ์žฌ์‹คํ–‰ ์‹œ ๋งˆ์ง€๋ง‰ ์ปค๋ฐ‹ ์œ„์น˜๋ถ€ํ„ฐ ์ฝ๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค๊ฐ€ ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

modules/kafka/src/test/java/com/loopers/kafka/learning/README.md (1)

1-36: ๋ฌธ์„œ ๊ตฌ์กฐ๊ฐ€ ์ž˜ ์ •๋ฆฌ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

Embedded Kafka ์‚ฌ์šฉ์œผ๋กœ Docker ๋ถˆํ•„์š”, ์ž๋™ ํ† ํ”ฝ ์ƒ์„ฑ ๋“ฑ ์‚ฌ์ „ ์ค€๋น„ ์‚ฌํ•ญ์ด ๋ช…ํ™•ํ•˜๊ฒŒ ์„ค๋ช…๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment4_BatchSizeTest.java (2)

1-44: ๋ฐฐ์น˜ ํฌ๊ธฐ ํ•™์Šต ํ…Œ์ŠคํŠธ๊ฐ€ ์ž˜ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

max.poll.records ์„ค์ •์˜ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„(์ฒ˜๋ฆฌ๋Ÿ‰ vs ์•ˆ์ •์„ฑ, ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ, ์žฌ์ฒ˜๋ฆฌ ๋ฒ”์œ„)๋ฅผ ๋‹ค์–‘ํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค๋กœ ๋ณด์—ฌ์ฃผ๋Š” ๊ตฌ์กฐ๊ฐ€ ์ข‹์Šต๋‹ˆ๋‹ค.


185-253: compareBatchSizes ํ…Œ์ŠคํŠธ๊ฐ€ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

์—ฌ๋Ÿฌ ๋ฐฐ์น˜ ํฌ๊ธฐ(10, 50, 100, 200)๋ฅผ ์ˆœ์ฐจ์ ์œผ๋กœ ๋น„๊ตํ•˜์—ฌ trade-off๋ฅผ ์ง์ ‘ ์ฒดํ—˜ํ•  ์ˆ˜ ์žˆ๋Š” ์ข‹์€ ํ•™์Šต ํ…Œ์ŠคํŠธ์ž…๋‹ˆ๋‹ค.

modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment3_PollIntervalTest.java (3)

1-46: LGTM!

max.poll.interval.ms ํ•™์Šต ํ…Œ์ŠคํŠธ๊ฐ€ ์ž˜ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ํƒ€์ž„์•„์›ƒ ๋ฐœ์ƒ ์‹œ ๋ฆฌ๋ฐธ๋Ÿฐ์‹ฑ, ์ •์ƒ ์ฒ˜๋ฆฌ, ๋ฐฐ์น˜ ํฌ๊ธฐ ์กฐ์ •์„ ํ†ตํ•œ ํƒ€์ž„์•„์›ƒ ๋ฐฉ์ง€ ๋“ฑ ํ•ต์‹ฌ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ๋‹ค๋ฃจ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.


47-106: ํƒ€์ž„์•„์›ƒ ์‹œ๋‚˜๋ฆฌ์˜ค๊ฐ€ ์ž˜ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

40์ดˆ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ vs 30์ดˆ ํƒ€์ž„์•„์›ƒ ์„ค์ •์œผ๋กœ ๋ฆฌ๋ฐธ๋Ÿฐ์‹ฑ ๋ฐœ์ƒ์„ ์ง์ ‘ ์ฒดํ—˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ์†Œ์š” ์‹œ๊ฐ„(์•ฝ 2๋ถ„)์— ๋Œ€ํ•œ ๊ฒฝ๊ณ ๋„ ์ ์ ˆํžˆ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.


159-210: LGTM!

๋ฐฐ์น˜ ํฌ๊ธฐ ์กฐ์ •(10๊ฐœ)์œผ๋กœ ํƒ€์ž„์•„์›ƒ์„ ๋ฐฉ์ง€ํ•˜๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค๊ฐ€ ์ž˜ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. "๋ฐฐ์น˜ ํฌ๊ธฐ ร— ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ < max.poll.interval.ms" ๊ณต์‹์„ ์‹ค์ œ๋กœ ๋ณด์—ฌ์ฃผ๋Š” ์ข‹์€ ์˜ˆ์ œ์ž…๋‹ˆ๋‹ค.

modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment1_AutoCommitTest.java (3)

1-45: Auto Commit vs Manual Commit ํ•™์Šต ํ…Œ์ŠคํŠธ๊ฐ€ ์ž˜ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

์ปค๋ฐ‹ ํƒ€์ด๋ฐ์ด ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ์— ๋ฏธ์น˜๋Š” ์˜ํ–ฅ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ณด์—ฌ์ฃผ๋Š” ๋‘ ๊ฐ€์ง€ ์‹œ๋‚˜๋ฆฌ์˜ค๊ฐ€ ์ž˜ ์ค€๋น„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.


46-119: Auto Commit ๋ฉ”์‹œ์ง€ ์œ ์‹ค ์‹œ๋‚˜๋ฆฌ์˜ค๊ฐ€ ์ž˜ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

4์ดˆ ๋Œ€๊ธฐ๋กœ 3์ดˆ auto.commit.interval์„ ์ดˆ๊ณผํ•˜์—ฌ ์ž๋™ ์ปค๋ฐ‹์ด ๋ฐœ์ƒํ•˜๊ณ , ์ฒ˜๋ฆฌ ์‹คํŒจํ•œ ๋ฉ”์‹œ์ง€(5~9)๊ฐ€ ์œ ์‹ค๋˜๋Š” ์ƒํ™ฉ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.


121-195: Manual Commit ํ…Œ์ŠคํŠธ๊ฐ€ At-Least-Once ๋ณด์žฅ์„ ์ž˜ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

์ปค๋ฐ‹ํ•˜์ง€ ์•Š๊ณ  ์ข…๋ฃŒ ์‹œ ์žฌ์‹œ์ž‘ ํ›„ ๋ชจ๋“  ๋ฉ”์‹œ์ง€๋ฅผ ๋‹ค์‹œ ์ฝ์„ ์ˆ˜ ์žˆ์Œ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ณด์—ฌ์ฃผ๋ฉฐ, ๋ฉฑ๋“ฑ์„ฑ์˜ ํ•„์š”์„ฑ๋„ ์–ธ๊ธ‰ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/example/ExampleRepositoryImpl.java (1)

5-5: ์ž„ํฌํŠธ ์ถ”๊ฐ€ ํ™•์ธ ์™„๋ฃŒ

Optional ์ž„ํฌํŠธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ถ”๊ฐ€๋˜์—ˆ์œผ๋ฉฐ, ๋ฉ”์„œ๋“œ์˜ ๋ฐ˜ํ™˜ ํƒ€์ž…์— ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

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

8-11: ์ž„ํฌํŠธ ์ •๋ฆฌ ํ™•์ธ ์™„๋ฃŒ

์ค‘๋ณต๋œ ์ž„ํฌํŠธ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ๋ช…์‹œ์ ์œผ๋กœ ์„ ์–ธํ•˜์—ฌ ๊ฐ€๋…์„ฑ์ด ๊ฐœ์„ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/test/java/com/loopers/interfaces/api/ExampleV1ApiE2ETest.java (1)

3-5: ํ…Œ์ŠคํŠธ ์ž„ํฌํŠธ ์ •๋ฆฌ ํ™•์ธ ์™„๋ฃŒ

ํ…Œ์ŠคํŠธ์— ํ•„์š”ํ•œ static ์ž„ํฌํŠธ์™€ Function ์ž„ํฌํŠธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

Also applies to: 11-11

apps/commerce-api/src/test/java/com/loopers/support/error/CoreExceptionTest.java (1)

3-4: Static ์ž„ํฌํŠธ ์ •๋ฆฌ ํ™•์ธ ์™„๋ฃŒ

์ค‘๋ณต๋œ static ์ž„ํฌํŠธ๊ฐ€ ์ œ๊ฑฐ๋˜๊ณ  ๊น”๋”ํ•˜๊ฒŒ ์ •๋ฆฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/application/dataplatform/DataPlatformEventHandler.java (1)

8-9: ์ž„ํฌํŠธ ์žฌ์ •๋ ฌ ํ™•์ธ ์™„๋ฃŒ

์ž„ํฌํŠธ๊ฐ€ ํŒŒ์ผ ์ƒ๋‹จ์œผ๋กœ ์ด๋™๋˜์–ด ๊ฐ€๋…์„ฑ์ด ๊ฐœ์„ ๋˜์—ˆ์œผ๋ฉฐ, ์ค‘๋ณต์ด ์ œ๊ฑฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/infrastructure/dataplatform/DataPlatformClientConfig.java (1)

7-7: ์ž„ํฌํŠธ ์žฌ์ •๋ ฌ ํ™•์ธ ์™„๋ฃŒ

์ž„ํฌํŠธ ์ˆœ์„œ๊ฐ€ ์กฐ์ •๋˜์—ˆ์œผ๋ฉฐ, Feign ํด๋ผ์ด์–ธํŠธ ์„ค์ • ํด๋ž˜์Šค๋Š” @Configuration ์—†์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java (1)

4-4: ์ž„ํฌํŠธ ์œ„์น˜ ๋ณ€๊ฒฝ ํ™•์ธ ์™„๋ฃŒ

TimeZone ์ž„ํฌํŠธ๊ฐ€ ์ ์ ˆํ•œ ์œ„์น˜๋กœ ์ด๋™๋˜์—ˆ์œผ๋ฉฐ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ดˆ๊ธฐํ™” ๋กœ์ง์€ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

modules/jpa/src/main/java/com/loopers/domain/BaseEntity.java (1)

11-11: Lombok ์ž„ํฌํŠธ ์œ„์น˜ ๋ณ€๊ฒฝ ํ™•์ธ ์™„๋ฃŒ

Getter ์ž„ํฌํŠธ๊ฐ€ ์žฌ์ •๋ ฌ๋˜์—ˆ์œผ๋ฉฐ, BaseEntity์˜ ๊ธฐ๋Šฅ์—๋Š” ๋ณ€๊ฒฝ์ด ์—†์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/resources/application.yml (2)

17-17: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ด๋ฆ„ ์ˆ˜์ •์ด ์˜ฌ๋ฐ”๋ฆ…๋‹ˆ๋‹ค.

commerce-api์—์„œ commerce-streamer๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๋ชจ๋“ˆ๋ช…๊ณผ ์ผ์น˜ํ•˜๋„๋ก ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.


28-32: Kafka ํ† ํ”ฝ ์„ค์ •์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

์นดํƒˆ๋กœ๊ทธ ์ด๋ฒคํŠธ์™€ ์ฃผ๋ฌธ ์ด๋ฒคํŠธ๋ฅผ ์œ„ํ•œ ํ† ํ”ฝ ๋งคํ•‘์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ์„ฑ๋˜์—ˆ์œผ๋ฉฐ, EventKafkaProducer์˜ ํ† ํ”ฝ ํ•ด๊ฒฐ ๋กœ์ง๊ณผ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/build.gradle.kts (1)

5-5: Kafka ๋ชจ๋“ˆ ์˜์กด์„ฑ ์ถ”๊ฐ€๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

Kafka ํ†ตํ•ฉ์„ ์œ„ํ•ด ํ•„์š”ํ•œ ์˜์กด์„ฑ์ด๋ฉฐ, ๋‹ค๋ฅธ ๋ชจ๋“ˆ ์˜์กด์„ฑ๋“ค๊ณผ ์ผ๊ด€๋˜๊ฒŒ ๊ตฌ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/resources/application.yml (3)

24-24: Kafka ์„ค์ • import๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

Kafka ํ†ตํ•ฉ์„ ์œ„ํ•ด ํ•„์š”ํ•œ ์„ค์ • ํŒŒ์ผ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ import๋˜์—ˆ์Šต๋‹ˆ๋‹ค.


30-34: Kafka ํ† ํ”ฝ ๋งคํ•‘์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

catalog-events์™€ order-events ํ† ํ”ฝ์ด ์ •์˜๋˜์—ˆ์œผ๋ฉฐ, EventKafkaProducer์˜ aggregate type ๊ธฐ๋ฐ˜ ํ† ํ”ฝ ํ•ด๊ฒฐ ๋กœ์ง๊ณผ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.


47-50: ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ Outbox Publisher ๋น„ํ™œ์„ฑํ™”๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

๋กœ์ปฌ ๋ฐ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค์ผ€์ค„๋Ÿฌ์— ์˜ํ•œ ์ด๋ฒคํŠธ ๋ฐœํ–‰์„ ๋ฐฉ์ง€ํ•˜์—ฌ ํ…Œ์ŠคํŠธ ๊ฒฉ๋ฆฌ์„ฑ์„ ๋ณด์žฅํ•˜๋Š” ์„ค์ •์ž…๋‹ˆ๋‹ค. ์ด๋Š” @ConditionalOnProperty๋กœ ๊ตฌํ˜„๋œ OutboxEventPublisher์™€ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/domain/dlq/DeadLetterQueueRepository.java (1)

1-12: DLQ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๋‹จ์ผ ์ฑ…์ž„ ์›์น™์„ ๋”ฐ๋ฅด๋Š” ๊ฐ„๊ฒฐํ•œ ์ธํ„ฐํŽ˜์ด์Šค๋กœ, DeadLetterQueue ์—”ํ‹ฐํ‹ฐ์˜ ์˜์†์„ฑ์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. DLQ ์„œ๋น„์Šค์—์„œ ์‹คํŒจํ•œ ๋ฉ”์‹œ์ง€๋ฅผ ์ €์žฅํ•˜๋Š” ๋ฐ ์ ์ ˆํ•˜๊ฒŒ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

docker/init-db.sql (1)

27-41: ์ง‘๊ณ„ ํ…Œ์ด๋ธ” ์„ค๊ณ„๊ฐ€ ์ ์ ˆํ•จ

product_metrics ํ…Œ์ด๋ธ” ์„ค๊ณ„๊ฐ€ ์ž˜ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค:

  • version ์ปฌ๋Ÿผ์„ ํ†ตํ•œ ๋‚™๊ด€์  ๋ฝ ๊ตฌํ˜„์œผ๋กœ ๋™์‹œ์„ฑ ์ œ์–ด
  • DESC ์ธ๋ฑ์Šค๋ฅผ ํ†ตํ•ด ์ธ๊ธฐ ์ƒํ’ˆ ์กฐํšŒ ์ตœ์ ํ™” (์ข‹์•„์š”/์กฐํšŒ/์ฃผ๋ฌธ ์ˆ˜ ๊ธฐ์ค€)
  • ์ ์ ˆํ•œ ๋ฐ์ดํ„ฐ ํƒ€์ž… ์„ ํƒ (INT, DECIMAL(15,2))
apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsJpaRepository.java (1)

1-13: LGTM!

JPA Repository ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„์ด ์ ์ ˆํ•˜๋ฉฐ, Spring Data JPA ์ปจ๋ฒค์…˜์„ ๋”ฐ๋ฅด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ฝ”๋“œ๋ฒ ์ด์Šค์˜ ๋‹ค๋ฅธ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ํŒจํ„ด๊ณผ๋„ ์ผ๊ด€์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java (1)

6-6: ์บ์‹œ ์„œ๋น„์Šค ์˜์กด์„ฑ ์ถ”๊ฐ€๊ฐ€ ์ ์ ˆํ•จ

์žฌ๊ณ  ์†Œ์ง„ ์‹œ ์บ์‹œ ๋ฌดํšจํ™”๋ฅผ ์œ„ํ•ด ProductCacheService ์˜์กด์„ฑ์„ ์ฃผ์ž…ํ•œ ๊ฒƒ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

Also applies to: 42-42

apps/commerce-streamer/src/main/java/com/loopers/application/inbox/EventInboxService.java (1)

45-58: LGTM!

Inbox ์ €์žฅ ๋กœ์ง์ด ๋ช…ํ™•ํ•˜๊ณ  ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค. Builder ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ€๋…์„ฑ์ด ์ข‹์œผ๋ฉฐ, ์ ์ ˆํ•œ ๋กœ๊น…๋„ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•™์Šต ๋‚ด์šฉ(Based on learnings): EventInboxAspect๊ฐ€ ์‹คํŒจ ์ฒ˜๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•˜๋ฏ€๋กœ, ์ด ์„œ๋น„์Šค๋Š” ์„ฑ๊ณต ์ผ€์ด์Šค์—๋งŒ ์ง‘์ค‘ํ•˜๋Š” ๊ฒƒ์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueRepositoryImpl.java (1)

1-21: LGTM!

DLQ Repository ๊ตฌํ˜„์ด ์ฝ”๋“œ๋ฒ ์ด์Šค์˜ ๋‹ค๋ฅธ Repository ๊ตฌํ˜„์ฒด(EventInboxRepositoryImpl, ProductMetricsRepositoryImpl)์™€ ์ผ๊ด€๋œ ํŒจํ„ด์„ ๋”ฐ๋ฅด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. JPA Repository๋ฅผ ๊ฐ์‹ธ๋Š” ๊ตฌ์กฐ๊ฐ€ ๋ช…ํ™•ํ•˜๊ณ  ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/test/java/com/loopers/application/inbox/EventInboxServiceTest.java (1)

30-69: LGTM!

์„ธ ๊ฐ€์ง€ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๊ฐ€ EventInboxService์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์„ ์ž˜ ๊ฒ€์ฆํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค:

  • ์ค‘๋ณต ์ด๋ฒคํŠธ ๊ฐ์ง€
  • ์‹ ๊ทœ ์ด๋ฒคํŠธ ํ™•์ธ
  • Inbox ์ €์žฅ ๋ฐ ๊ฒ€์ฆ

ํ…Œ์ŠคํŠธ ๋กœ์ง์ด ๋ช…ํ™•ํ•˜๊ณ , Given-When-Then ํŒจํ„ด์„ ์ž˜ ๋”ฐ๋ฅด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInboxRepository.java (1)

1-22: LGTM!

Repository ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ๋ช…ํ™•ํ•˜๊ณ  ๊ฐ„๊ฒฐํ•ฉ๋‹ˆ๋‹ค. ๋ฉฑ๋“ฑ์„ฑ ์ฒดํฌ๋ฅผ ์œ„ํ•œ existsByEventId(), ์ €์žฅ์„ ์œ„ํ•œ save(), ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ deleteAll() ๋ฉ”์„œ๋“œ๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

์ฝ”๋“œ๋ฒ ์ด์Šค์˜ ๋‹ค๋ฅธ Repository ์ธํ„ฐํŽ˜์ด์Šค๋“ค๊ณผ ์ผ๊ด€๋œ ํŒจํ„ด์„ ๋”ฐ๋ฅด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/ConsumerSmokeTest.java (1)

23-50: ์Šค๋ชจํฌ ํ…Œ์ŠคํŠธ์™€ Consumer ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ๊ณ„์ธตํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค

Bean ์ƒ์„ฑ๊ณผ Context ๋กœ๋”ฉ์„ ๊ฒ€์ฆํ•˜๋Š” ์Šค๋ชจํฌ ํ…Œ์ŠคํŠธ๊ฐ€ ์ž˜ ์ž‘์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์‹ค์ œ Consumer ๋™์ž‘ ๊ฒ€์ฆ์€ CatalogEventConsumerTest ๋ฐ OrderEventConsumerTest์—์„œ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ์œผ๋ฉฐ, modules/kafka ํ•˜์œ„์—๋Š” @embeddedkafka๋ฅผ ์‚ฌ์šฉํ•œ 4๊ฐœ์˜ ํ•™์Šต ํ…Œ์ŠคํŠธ(Experiment1~4)๊ฐ€ ์กด์žฌํ•˜์—ฌ ํ…Œ์ŠคํŠธ ๊ณ„์ธตํ™”๊ฐ€ ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxJpaRepository.java (1)

1-12: ๊ตฌํ˜„์ด ๊น”๋”ํ•ฉ๋‹ˆ๋‹ค.

Spring Data JPA์˜ ํ‘œ์ค€ ๋ช…๋ช… ๊ทœ์น™์„ ๋”ฐ๋ฅด๋ฉฐ, Inbox ํŒจํ„ด์˜ ์ค‘๋ณต ์ฒดํฌ๋ฅผ ์œ„ํ•œ existsByEventId ๋ฉ”์„œ๋“œ๊ฐ€ ์ ์ ˆํ•˜๊ฒŒ ์ •์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/infrastructure/dlq/DeadLetterQueueJpaRepository.java (1)

1-10: DLQ ์ €์žฅ์†Œ ๊ตฌํ˜„์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ์š”๊ตฌ์‚ฌํ•ญ์— ํ•„์š”ํ•œ ๊ธฐ๋ณธ CRUD ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋ฉฐ, ํ–ฅํ›„ ํ•„์š” ์‹œ ์ปค์Šคํ…€ ์ฟผ๋ฆฌ ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋Š” ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventPublisher.java (1)

8-8: Feature toggle ์ถ”๊ฐ€๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

@ConditionalOnProperty๋ฅผ ํ†ตํ•ด Outbox Publisher๋ฅผ ์„ ํƒ์ ์œผ๋กœ ํ™œ์„ฑํ™”ํ•  ์ˆ˜ ์žˆ์–ด, ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์ด๋‚˜ ํŠน์ • ํ”„๋กœํŒŒ์ผ์—์„œ ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ๋น„ํ™œ์„ฑํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. matchIfMissing=true ์„ค์ •์œผ๋กœ ๊ธฐ๋ณธ๊ฐ’์ด ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์–ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.

Also applies to: 20-20

apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetricsRepository.java (1)

1-24: ๋„๋ฉ”์ธ ์ €์žฅ์†Œ ์ธํ„ฐํŽ˜์ด์Šค ์„ค๊ณ„๊ฐ€ ์šฐ์ˆ˜ํ•ฉ๋‹ˆ๋‹ค.

DIP(Dependency Inversion Principle)๋ฅผ ์ค€์ˆ˜ํ•˜๋ฉฐ, ๋ฉ”์„œ๋“œ ์‹œ๊ทธ๋‹ˆ์ฒ˜๊ฐ€ ๋ช…ํ™•ํ•˜๊ณ  ๋ฌธ์„œํ™”๋„ ์ž˜ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. deleteAll() ๋ฉ”์„œ๋“œ์— ํ…Œ์ŠคํŠธ์šฉ์ž„์„ ๋ช…์‹œํ•œ ์ฃผ์„๋„ ์ข‹์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/infrastructure/metrics/ProductMetricsRepositoryImpl.java (1)

1-32: ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๊ตฌํ˜„์ด ์ผ๊ด€์„ฑ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฝ”๋“œ๋ฒ ์ด์Šค์˜ ๋‹ค๋ฅธ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๊ตฌํ˜„์ฒด๋“ค(EventInboxRepositoryImpl, DeadLetterQueueRepositoryImpl)๊ณผ ๋™์ผํ•œ ํŒจํ„ด์„ ๋”ฐ๋ฅด๊ณ  ์žˆ์–ด ์œ ์ง€๋ณด์ˆ˜์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.java (1)

63-69: ํ† ํ”ฝ ๋ผ์šฐํŒ… ๋กœ์ง์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

toUpperCase()๋ฅผ ์‚ฌ์šฉํ•œ ๋Œ€์†Œ๋ฌธ์ž ๋ฌด๊ด€ ๋งค์นญ๊ณผ ๋ช…ํ™•ํ•œ switch ํ‘œํ˜„์‹, ๊ทธ๋ฆฌ๊ณ  ์•Œ ์ˆ˜ ์—†๋Š” ํƒ€์ž…์— ๋Œ€ํ•œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ์ž˜ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/application/dlq/DeadLetterQueueService.java (1)

31-31: ํŠธ๋žœ์žญ์…˜๊ณผ ๋กœ๊น… ์ฒ˜๋ฆฌ๊ฐ€ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

@Transactional ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ DLQ ์ €์žฅ์˜ ์›์ž์„ฑ์„ ๋ณด์žฅํ•˜๊ณ , ์—๋Ÿฌ ๋ ˆ๋ฒจ ๋กœ๊ทธ์— ํ•„์š”ํ•œ ๋ชจ๋“  ์ •๋ณด(ํ† ํ”ฝ, ์ด๋ฒคํŠธID, ์žฌ์‹œ๋„ ํšŸ์ˆ˜, ์—๋Ÿฌ ๋ฉ”์‹œ์ง€)๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์–ด ์šด์˜ ์ค‘ ๋ฌธ์ œ ์ถ”์ ์ด ์šฉ์ดํ•ฉ๋‹ˆ๋‹ค.

Also applies to: 44-47

apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/CatalogEventConsumerTest.java (2)

59-77: ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๊ฐ€ ์šฐ์ˆ˜ํ•ฉ๋‹ˆ๋‹ค.

์ข‹์•„์š” ์ƒ์„ฑ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ์˜ ์ •์ƒ ํ”Œ๋กœ์šฐ๋ฅผ ์ž˜ ๊ฒ€์ฆํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. Inbox ์ €์žฅ๊ณผ ๋ฉ”ํŠธ๋ฆญ ์ฆ๊ฐ€๋ฅผ ๋ชจ๋‘ ํ™•์ธํ•˜๊ณ  ์žˆ์–ด ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.


198-210: ํ—ฌํผ ๋ฉ”์„œ๋“œ๊ฐ€ ์ž˜ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ๋กœ์ง์„ ๋ณ„๋„ ๋ฉ”์„œ๋“œ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ฐ€๋…์„ฑ๊ณผ ์žฌ์‚ฌ์šฉ์„ฑ์„ ๋†’์˜€์Šต๋‹ˆ๋‹ค. ObjectMapper๋ฅผ ์‚ฌ์šฉํ•œ ์ง๋ ฌํ™”๋„ ์‹ค์ œ ๋Ÿฐํƒ€์ž„ ๋™์ž‘๊ณผ ์ผ์น˜ํ•˜์—ฌ ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/infrastructure/inbox/EventInboxRepositoryImpl.java (1)

11-30: ํ‘œ์ค€์ ์ธ Repository ๊ตฌํ˜„์ž…๋‹ˆ๋‹ค.

JPA Repository๋กœ์˜ ๋‹จ์ˆœ ์œ„์ž„ ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ๋‹ค๋ฅธ Repository ๊ตฌํ˜„์ฒด๋“ค(DeadLetterQueueRepositoryImpl, ProductMetricsRepositoryImpl)๊ณผ ์ผ๊ด€๋œ ๊ตฌ์กฐ๋ฅผ ์œ ์ง€ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. Repository ๋ ˆ์ด์–ด์— ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ์—†์–ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

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

122-135: ์žฌ๊ณ  ์†Œ์ง„ ์‹œ ์บ์‹œ ๋ฌดํšจํ™” ๋กœ์ง์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

์žฌ๊ณ ๊ฐ€ 0์ด ๋˜์—ˆ์„ ๋•Œ ํ•ด๋‹น ์ƒํ’ˆ์˜ ์บ์‹œ๋ฅผ ๋ฌดํšจํ™”ํ•˜์—ฌ ๋‹ค์Œ ์กฐํšŒ ์‹œ ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ DB์—์„œ ๊ฐ€์ ธ์˜ค๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. OrderFacade์™€์˜ ํ†ตํ•ฉ๋„ ์ž˜ ๋˜์–ด ์žˆ์œผ๋ฉฐ, ๋กœ๊น…๋„ ์ ์ ˆํžˆ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.


137-144: ์ „์ฒด ์บ์‹œ ์‚ญ์ œ ๋ฉ”์„œ๋“œ๋Š” ์‹ ์ค‘ํ•˜๊ฒŒ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

allEntries = true ์˜ต์…˜์€ ์บ์‹œ์˜ ๋ชจ๋“  ํ•ญ๋ชฉ์„ ์ œ๊ฑฐํ•˜๋ฏ€๋กœ, ์บ์‹œ์— ๋‹ค๋ฅธ ๊ด€๋ จ ํ•ญ๋ชฉ์ด ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฉด ์„ฑ๋Šฅ ์ €ํ•˜๋ฅผ ์ดˆ๋ž˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋Œ€๋Ÿ‰ ์—…๋ฐ์ดํŠธ ๋“ฑ ํŠน์ˆ˜ํ•œ ๊ฒฝ์šฐ์—๋งŒ ์‚ฌ์šฉํ•˜๋„๋ก ์ œํ•œํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

apps/commerce-streamer/src/test/java/com/loopers/interfaces/consumer/OrderEventConsumerTest.java (3)

92-121: String payload ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ๊ฐ€ ์ค‘์š”ํ•œ ์—ฃ์ง€ ์ผ€์ด์Šค๋ฅผ ์ปค๋ฒ„ํ•ฉ๋‹ˆ๋‹ค.

payload๊ฐ€ Map์ด ์•„๋‹Œ JSON String์œผ๋กœ ์ „๋‹ฌ๋˜๋Š” ๊ฒฝ์šฐ๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์‹ค์ œ Kafka ๋ฉ”์‹œ์ง€์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์ง๋ ฌํ™” ๋ฐฉ์‹ ์ฐจ์ด๋ฅผ ๋ฐ˜์˜ํ•˜๋ฉฐ, Consumer์˜ ๊ฒฌ๊ณ ์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ์ข‹์€ ํ…Œ์ŠคํŠธ์ž…๋‹ˆ๋‹ค.


148-166: ๊ฒฐ์ œ ์„ฑ๊ณต ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋กœ์ง์˜ ํ–ฅํ›„ ํ™•์žฅ ๊ฐ€๋Šฅ์„ฑ์„ ๊ณ ๋ คํ•˜์„ธ์š”.

ํ˜„์žฌ๋Š” Inbox์—๋งŒ ์ €์žฅํ•˜๊ณ  ์ถ”๊ฐ€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ์—†๋‹ค๊ณ  ๋ช…์‹œ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ํ–ฅํ›„ ๊ฒฐ์ œ ์„ฑ๊ณต ์‹œ ์ถ”๊ฐ€ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•  ๊ฒฝ์šฐ(์˜ˆ: ํฌ์ธํŠธ ์ ๋ฆฝ, ์•Œ๋ฆผ ๋ฐœ์†ก ๋“ฑ) ์ด ๋ถ€๋ถ„์„ ํ™•์žฅํ•  ์ˆ˜ ์žˆ๋„๋ก ์ค€๋น„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.


174-218: ์˜ˆ์™ธ ์ƒํ™ฉ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฒฌ๊ณ ํ•ฉ๋‹ˆ๋‹ค.

payload ๋ˆ„๋ฝ ๋ฐ ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ๋ชจ๋‘ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ๊ฐ ๊ฒฝ์šฐ์— ์ ์ ˆํ•œ ๋™์ž‘(Inbox ์ €์žฅ์€ ํ•˜๋˜ ๋ฉ”ํŠธ๋ฆญ ์—…๋ฐ์ดํŠธ๋Š” ํ•˜์ง€ ์•Š์Œ)์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๋ถ€๋ถ„ ์‹คํŒจ ์‹œ์—๋„ ์‹œ์Šคํ…œ์ด ์•ˆ์ •์ ์œผ๋กœ ๋™์ž‘ํ•˜๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/domain/metrics/ProductMetrics.java (3)

25-33: ์ธ๋ฑ์Šค ์ „๋žต์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

๋ฉ”ํŠธ๋ฆญ ์ปฌ๋Ÿผ์— DESC ์ธ๋ฑ์Šค๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ์ธ๊ธฐ ์ƒํ’ˆ ์กฐํšŒ ์ฟผ๋ฆฌ(์ข‹์•„์š” ์ˆ˜/์กฐํšŒ ์ˆ˜/์ฃผ๋ฌธ ์ˆ˜ ๊ธฐ์ค€ ์ •๋ ฌ)๋ฅผ ํšจ์œจ์ ์œผ๋กœ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. updated_at ์ธ๋ฑ์Šค๋„ ์ตœ๊ทผ ์—…๋ฐ์ดํŠธ๋œ ๋ฉ”ํŠธ๋ฆญ ์กฐํšŒ์— ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.


50-51: BigDecimal precision ์„ค์ •์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

precision = 15, scale = 2๋กœ ์„ค์ •ํ•˜์—ฌ ์ตœ๋Œ€ 9,999,999,999,999.99๊นŒ์ง€ ํ‘œํ˜„ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ์ด๋Š” ๋Œ€๋ถ€๋ถ„์˜ e-commerce ํŒ๋งค ๊ธˆ์•ก์„ ์ถฉ๋ถ„ํžˆ ์ปค๋ฒ„ํ•ฉ๋‹ˆ๋‹ค.


111-114: ์ฃผ๋ฌธ ์ˆ˜ ๋ฐ ํŒ๋งค ๊ธˆ์•ก ์ฆ๊ฐ€ ๋กœ์ง์ด ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค.

์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰๊ณผ ํŒ๋งค ๊ธˆ์•ก์„ ๋™์‹œ์— ์—…๋ฐ์ดํŠธํ•˜๋ฉฐ, BigDecimal์˜ add ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ •๋ฐ€๋„๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ๋‚™๊ด€์  ๋ฝ(@Version)๊ณผ ํ•จ๊ป˜ ๋™์‹œ์„ฑ ์ด์Šˆ๋ฅผ ์ ์ ˆํžˆ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.java (1)

64-64: ํŠธ๋žœ์žญ์…˜ ์ „ํŒŒ ์„ค์ •์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

REQUIRES_NEW๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋ฒคํŠธ ๋ฐœํ–‰ ๋กœ์ง์„ ๋ณ„๋„ ํŠธ๋žœ์žญ์…˜์—์„œ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ํŠธ๋žœ์žญ์…˜(saveEvent)๊ณผ ๋ฐœํ–‰ ํŠธ๋žœ์žญ์…˜์„ ๋ถ„๋ฆฌํ•˜์—ฌ, ๋ฐœํ–‰ ์‹คํŒจ๊ฐ€ ์›๋ณธ ํŠธ๋žœ์žญ์…˜์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

apps/commerce-streamer/src/main/java/com/loopers/domain/inbox/EventInbox.java (2)

20-27: ์ธ๋ฑ์Šค ์„ค๊ณ„๊ฐ€ idempotency์™€ ์กฐํšŒ ํŒจํ„ด์„ ์ž˜ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

  • idx_event_id (unique): ์ค‘๋ณต ์ด๋ฒคํŠธ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ํ•ต์‹ฌ ์ธ๋ฑ์Šค
  • idx_aggregate: ํŠน์ • aggregate(์˜ˆ: ์ฃผ๋ฌธ 123๋ฒˆ)์— ๋Œ€ํ•œ ๋ชจ๋“  ์ด๋ฒคํŠธ ์กฐํšŒ์— ์œ ์šฉ
  • idx_processed_at: ์˜ค๋ž˜๋œ ์ด๋ฒคํŠธ ์ •๋ฆฌ/์•„์นด์ด๋น™ ์ฟผ๋ฆฌ์— ํ™œ์šฉ

31-32: eventId ๊ธธ์ด๋Š” ํ˜„์žฌ ์„ค์ •์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

EventOutbox๋Š” IDENTITY ์ „๋žต์œผ๋กœ ์ƒ์„ฑ๋œ Long ํƒ€์ž…์˜ ์ž๋™ ์ฆ๊ฐ€ ID๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ, ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ ์‹œ ์ตœ๋Œ€ 19์ž์ž…๋‹ˆ๋‹ค. ํ˜„์žฌ length=50์€ ์ด๋ฏธ ์ถฉ๋ถ„ํ•œ ์—ฌ์œ ๋ฅผ ์ œ๊ณตํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ํ–ฅํ›„ ID ํ˜•์‹ ๋ณ€๊ฒฝ ์‹œ์—๋„ ๋ฌธ์ œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์กฐ์ •์ด ํ•„์š” ์—†์Šต๋‹ˆ๋‹ค.

Likely an incorrect or invalid review comment.

apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.java (2)

27-34: ์„œ๋น„์Šค ๋ฉ”์„œ๋“œ ๊ตฌ์กฐ๊ฐ€ ์ผ๊ด€์ ์ด๊ณ  ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

๋ชจ๋“  ๋ฉ”ํŠธ๋ฆญ ์—…๋ฐ์ดํŠธ ๋ฉ”์„œ๋“œ๊ฐ€ ๋™์ผํ•œ ํŒจํ„ด์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค: ์กฐํšŒ/์ƒ์„ฑ โ†’ ์—…๋ฐ์ดํŠธ โ†’ ์ €์žฅ โ†’ ๋กœ๊น…. @transactional ์ ์šฉ์œผ๋กœ ์›์ž์„ฑ๋„ ๋ณด์žฅ๋ฉ๋‹ˆ๋‹ค.


71-79: ์ฃผ๋ฌธ ์ˆ˜ ์ฆ๊ฐ€ ๋กœ์ง์ด ์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค.

์ˆ˜๋Ÿ‰(quantity)๊ณผ ๊ธˆ์•ก(amount)์„ ๋ชจ๋‘ ์ „๋‹ฌํ•˜์—ฌ ์ฃผ๋ฌธ ์ˆ˜์™€ ํŒ๋งค ๊ธˆ์•ก์„ ๋™์‹œ์— ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค. ๋กœ๊น…์—๋„ ๋‘ ๊ฐ’์ด ๋ชจ๋‘ ํฌํ•จ๋˜์–ด ์žˆ์–ด ๋ชจ๋‹ˆํ„ฐ๋ง์— ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

Comment on lines +68 to +89
kafkaProducer.publish(outbox)
.thenAccept(result -> {
// ๋ฐœํ–‰ ์„ฑ๊ณต ์‹œ Outbox ์ƒํƒœ ์—…๋ฐ์ดํŠธ
outbox.markAsPublished();
outboxRepository.save(outbox);

log.info("Outbox โ†’ Kafka ๋ฐœํ–‰ ์™„๋ฃŒ - outboxId: {}, eventType: {}, offset: {}",
outbox.getId(),
outbox.getEventType(),
result.getRecordMetadata().offset());
})
.exceptionally(ex -> {
// ๋ฐœํ–‰ ์‹คํŒจ ์‹œ Outbox ์ƒํƒœ ์—…๋ฐ์ดํŠธ
outbox.markAsFailed(ex.getMessage());
outboxRepository.save(outbox);

log.error("Outbox โ†’ Kafka ๋ฐœํ–‰ ์‹คํŒจ - outboxId: {}, error: {}",
outbox.getId(), ex.getMessage(), ex);

return null;
})
.join(); // ๋™๊ธฐ ๋Œ€๊ธฐ (ํŠธ๋žœ์žญ์…˜ ๋‚ด์—์„œ ์™„๋ฃŒ ๋ณด์žฅ)
Copy link

Choose a reason for hiding this comment

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

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

๐Ÿงฉ Analysis chain

๐Ÿ Script executed:

find . -type f -name "OutboxEventService.java" -o -name "*EventKafkaProducer*" 2>/dev/null | head -20

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

Length of output: 264


๐Ÿ Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.java | sed -n '60,105p'

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

Length of output: 2150


๐Ÿ Script executed:

cat -n apps/commerce-api/src/main/java/com/loopers/application/kafka/EventKafkaProducer.java | head -80

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

Length of output: 3056


Kafka ๋ฐœํ–‰ ๋กœ์ง๊ณผ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ์— ์ฃผ์˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

join()์ด ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„ ๋‚ด ๋™๊ธฐ ์™„๋ฃŒ๋ฅผ ๋ณด์žฅํ•˜๋Š” ๊ฒƒ์€ ์˜ฌ๋ฐ”๋ฅด์ง€๋งŒ, ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๊ตฌ์กฐ์— ์ค‘๋ณต ์œ„ํ—˜์ด ์žˆ์Šต๋‹ˆ๋‹ค. EventKafkaProducer์˜ exceptionally() ๋ธ”๋ก(line 56)์—์„œ RuntimeException์„ ๋˜์ง€๊ณ , OutboxEventService์˜ exceptionally() ๋ธ”๋ก(lines 79-88)์—์„œ ์ด๋ฅผ ์ฒ˜๋ฆฌํ•˜๋ฉฐ markAsFailed()์™€ save()๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์ด save() ํ˜ธ์ถœ์ด ์‹คํŒจํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ์ „ํŒŒ๋˜์–ด join()์ด CompletionException์„ ๋˜์ง€๊ณ , ์™ธ๋ถ€ catch ๋ธ”๋ก(lines 91-99)์ด ๋‹ค์‹œ markAsFailed()์™€ save()๋ฅผ ์‹œ๋„ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ด๋Š” ์ค‘๋ณต ์ƒํƒœ ์—…๋ฐ์ดํŠธ์™€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ œ์•ฝ ์กฐ๊ฑด ์œ„๋ฐ˜์„ ์ดˆ๋ž˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

exceptionally() ๋ธ”๋ก์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ๋ฅผ ์™ธ๋ถ€ catch์—์„œ ๋ณ„๋„ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š๋„๋ก ๋ถ„๋ฆฌํ•˜๊ฑฐ๋‚˜, ์ค‘๋ณต ํ˜ธ์ถœ์„ ๋ฐฉ์ง€ํ•˜๋Š” ํ”Œ๋ž˜๊ทธ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
In
apps/commerce-api/src/main/java/com/loopers/application/outbox/OutboxEventService.java
around lines 68 to 89, avoid double-updating Outbox status when
kafkaProducer.exceptionally already marks/saves failures: either stop rethrowing
from EventKafkaProducer.exceptionally (so it handles markAsFailed/save and
returns a completed result) or add a guard here before calling markAsFailed/save
(e.g., check outbox status/isFinalState and only update if not already
failed/published). Update the code so only one place performs markAsFailed/save
(remove the duplicate update or add the status check) and ensure kafka publish
exceptions are not rethrown into join() to prevent the outer catch from
repeating the save.

Comment on lines +24 to +27
// test - awaitility for async testing
testImplementation("org.awaitility:awaitility:4.2.0")
// test - kafka testcontainers
testImplementation("org.testcontainers:kafka")
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

ํ…Œ์ŠคํŠธ ์˜์กด์„ฑ ์ถ”๊ฐ€๊ฐ€ ์ ์ ˆํ•˜๋ฉฐ, awaitility ๋ฒ„์ „ ์—…๊ทธ๋ ˆ์ด๋“œ๋ฅผ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

Kafka ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ์˜์กด์„ฑ ์ถ”๊ฐ€๊ฐ€ PR ๋ชฉํ‘œ์™€ ์ž˜ ๋ถ€ํ•ฉํ•ฉ๋‹ˆ๋‹ค:

  • Awaitility: ๋น„๋™๊ธฐ Kafka ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ๋Œ€๊ธฐ์— ํ™œ์šฉ
  • Testcontainers: ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ์šฉ Kafka ์ปจํ…Œ์ด๋„ˆ ๊ตฌ๋™์— ํ™œ์šฉ

testImplementation ์Šค์ฝ”ํ”„ ์‚ฌ์šฉ๋„ ์˜ฌ๋ฐ”๋ฅด๋ฉฐ, testcontainers:kafka๊ฐ€ BOM์„ ํ†ตํ•ด ๋ฒ„์ „ ๊ด€๋ฆฌ๋˜๋Š” ๊ฒƒ๋„ ์ข‹์€ ๊ด€ํ–‰์ž…๋‹ˆ๋‹ค.

๋‹ค๋งŒ awaitility 4.3.0์ด 2025๋…„ 2์›” 21์ผ์— ๋ฆด๋ฆฌ์Šค๋˜์—ˆ๊ณ  ๋” ๋‚˜์€ Kotlin ์‹œ๊ฐ„ ์ง€์›๊ณผ ์ƒˆ๋กœ์šด untilAsserted ์‚ฌ์šฉ ๋ฐฉ์‹์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘์ธ 4.2.0์€ 2022๋…„ 3์›”์˜ ๋ฒ„์ „์ด๋ฏ€๋กœ, 4.3.0์œผ๋กœ ์—…๊ทธ๋ ˆ์ด๋“œํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
In apps/commerce-streamer/build.gradle.kts around lines 24 to 27, the Awaitility
test dependency is pinned to 4.2.0; update the version to 4.3.0 to pick up
improved Kotlin time support and untilAsserted APIs โ€” replace the
testImplementation("org.awaitility:awaitility:4.2.0") entry with
testImplementation("org.awaitility:awaitility:4.3.0") and run a quick test build
to ensure compatibility.

Comment on lines +84 to +89
private ProductMetrics getOrCreate(Long productId) {
return productMetricsRepository.findByProductId(productId)
.orElseGet(() -> ProductMetrics.builder()
.productId(productId)
.build());
}
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

getOrCreate ํŒจํ„ด์˜ ๋™์‹œ์„ฑ ์ด์Šˆ๋ฅผ ๊ณ ๋ คํ•˜์„ธ์š”.

findByProductId๊ฐ€ ๋น„์–ด์žˆ์„ ๋•Œ ์ƒˆ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ƒ์„ฑํ•˜๋Š”๋ฐ, ๋‘ ์Šค๋ ˆ๋“œ๊ฐ€ ๋™์‹œ์— ๊ฐ™์€ productId๋กœ ์ฒ˜์Œ ๋ฉ”ํŠธ๋ฆญ์„ ์ƒ์„ฑํ•˜๋ ค ํ•˜๋ฉด unique constraint ์œ„๋ฐ˜์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ProductMetrics ์—”ํ‹ฐํ‹ฐ๊ฐ€ @Version์„ ์‚ฌ์šฉํ•œ ๋‚™๊ด€์  ๋ฝ์„ ์ ์šฉํ•˜๊ณ  ์žˆ์ง€๋งŒ, ์ตœ์ดˆ ์ƒ์„ฑ ์‹œ์ ์—๋Š” ๋„์›€์ด ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์‹ค์ œ ํ™˜๊ฒฝ์—์„œ ๋™์‹œ์— ๊ฐ™์€ ์ƒํ’ˆ์˜ ์ฒซ ์ด๋ฒคํŠธ๊ฐ€ ์ฒ˜๋ฆฌ๋  ๊ฐ€๋Šฅ์„ฑ์ด ๋‚ฎ๋‹ค๋ฉด ๋ฌธ์ œ์—†์ง€๋งŒ, ๋ฐœ์ƒ ์‹œ ์žฌ์‹œ๋„ ๋กœ์ง์ด๋‚˜ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ๋Œ€์‘ ๋ฐฉ์•ˆ
  1. ์ƒ์œ„ ๋ ˆ์ด์–ด(Consumer)์—์„œ OptimisticLockException์„ ์žก์•„ ์žฌ์‹œ๋„
  2. ProductMetrics๋ฅผ ์ƒํ’ˆ ์ƒ์„ฑ ์‹œ์ ์— ๋ฏธ๋ฆฌ ์ดˆ๊ธฐํ™”
  3. ๋˜๋Š” save ์‹คํŒจ ์‹œ ๋‹ค์‹œ ์กฐํšŒ ์‹œ๋„ํ•˜๋Š” ๋กœ์ง ์ถ”๊ฐ€

ํ˜„์žฌ Kafka consumer์˜ ์žฌ์‹œ๋„ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด ์ด๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.

๐Ÿค– Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/application/metrics/ProductMetricsService.java
around lines 84-89: the current getOrCreate() can produce a unique-constraint
race when two threads create the same productId concurrently; change the flow so
creation is attempted but save failures due to duplicate key/constraint (e.g.,
DataIntegrityViolationException, ConstraintViolationException) are caught and
handled by re-querying the repository and returning the existing record (or
retrying a bounded number of times); alternatively ensure the caller (Kafka
consumer) catches OptimisticLockException/DataIntegrityViolationException and
retries โ€” implement the catch-and-requery pattern in this service: try to save
the new ProductMetrics, on constraint exception fetch findByProductId() again
and return that result (or bubble up after N retries).

Comment on lines +50 to +69
for (ConsumerRecord<Object, Object> record : records) {
try {
boolean processed = processEvent(record);
if (processed) {
successCount++;
} else {
skipCount++; // ์ค‘๋ณต ์ด๋ฒคํŠธ
}
} catch (Exception e) {
failCount++;
log.error("โŒ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์‹คํŒจ - partition: {}, offset: {}, key: {}, error: {}",
record.partition(), record.offset(), record.key(), e.getMessage(), e);

// DLQ์— ์ „์†ก
sendToDLQ(record, e, 0);
}
}

// Offset ์ปค๋ฐ‹ (๋ฐฐ์น˜ ๋‹จ์œ„)
acknowledgment.acknowledge();
Copy link

@coderabbitai coderabbitai bot Dec 18, 2025

Choose a reason for hiding this comment

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

โš ๏ธ Potential issue | ๐Ÿ”ด Critical

OrderEventConsumer์™€ ๋™์ผํ•œ ๋ฐฐ์น˜ ์ปค๋ฐ‹ ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ํŒŒ์ผ๋„ OrderEventConsumer์™€ ๋™์ผํ•œ critical ์ด์Šˆ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฐฐ์น˜ ๋‚ด ์ผ๋ถ€ ๋ ˆ์ฝ”๋“œ๊ฐ€ ์‹คํŒจํ•˜์—ฌ DLQ๋กœ ์ „์†ก๋˜๋”๋ผ๋„(Line 64), Line 69์—์„œ ์ „์ฒด ๋ฐฐ์น˜์˜ ์˜คํ”„์…‹์„ ์ปค๋ฐ‹ํ•˜์—ฌ ์‹คํŒจํ•œ ๋ฉ”์‹œ์ง€๊ฐ€ ์žฌ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

ํ•ด๊ฒฐ์ฑ…:
OrderEventConsumer์— ์ œ์•ˆ๋œ ๊ฒƒ๊ณผ ๋™์ผํ•œ ์ˆ˜์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์‹คํŒจํ•œ ๋ ˆ์ฝ”๋“œ๊ฐ€ ์žˆ์œผ๋ฉด ์˜คํ”„์…‹์„ ์ปค๋ฐ‹ํ•˜์ง€ ์•Š๊ฑฐ๋‚˜ ์˜ˆ์™ธ๋ฅผ ๋˜์ ธ ์žฌ์ฒ˜๋ฆฌ๋ฅผ ํŠธ๋ฆฌ๊ฑฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.java
around lines 50 to 69, the code always calls acknowledgment.acknowledge() after
processing the batch which commits offsets even when some records failed and
were sent to DLQ; change the flow so that if any record failed (failCount > 0)
you do NOT acknowledge the batch and instead throw a RuntimeException (or
propagate the caught exception) after sending to DLQ to force a retry, otherwise
acknowledge only when failCount == 0; ensure DLQ send remains inside the catch
but move/keep the ack conditional so offsets are committed only when the entire
batch succeeded.

Copy link
Contributor

Choose a reason for hiding this comment

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

์š”๊ฑฐ ์‹ค์ œ๋กœ ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค. ํ™•์ธ๋ถ€ํƒ๋“œ๋ ค์š”

Copy link

Choose a reason for hiding this comment

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

@looper88888, ๋„ค, ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค. CodeRabbit์ด ์ง€์ ํ•œ ๋ฌธ์ œ๊ฐ€ ์‹ค์ œ๋กœ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”ด ๋ฌธ์ œ ์ƒํ™ฉ

ํ˜„์žฌ CatalogEventConsumer์˜ Line 50-69 ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด:

for (ConsumerRecord<Object, Object> record : records) {
    try {
        boolean processed = processEvent(record);
        if (processed) {
            successCount++;
        } else {
            skipCount++;
        }
    } catch (Exception e) {
        failCount++;
        // DLQ์— ์ „์†ก
        sendToDLQ(record, e, 0);
    }
}

// โš ๏ธ ๋ฌธ์ œ: ์‹คํŒจ ์—ฌ๋ถ€์™€ ๊ด€๊ณ„์—†์ด ํ•ญ์ƒ ์ปค๋ฐ‹
acknowledgment.acknowledge();

๋ฌธ์ œ์ :

  1. ๋ฐฐ์น˜ ๋‚ด ์ผ๋ถ€ ๋ ˆ์ฝ”๋“œ๊ฐ€ ์ฒ˜๋ฆฌ ์‹คํŒจํ•˜๋ฉด failCount๊ฐ€ ์ฆ๊ฐ€ํ•˜๊ณ  DLQ๋กœ ์ „์†ก๋ฉ๋‹ˆ๋‹ค
  2. ํ•˜์ง€๋งŒ Line 69์—์„œ ๋ฌด์กฐ๊ฑด acknowledgment.acknowledge()๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค
  3. ์ด๋Š” ์‹คํŒจํ•œ ๋ ˆ์ฝ”๋“œ์˜ ์˜คํ”„์…‹๋„ ์ปค๋ฐ‹๋˜์–ด ์žฌ์ฒ˜๋ฆฌ ๊ธฐํšŒ๋ฅผ ์žƒ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค
  4. ๊ฒฐ๊ณผ์ ์œผ๋กœ ์‹คํŒจํ•œ ๋ฉ”์‹œ์ง€๋Š” DLQ์—๋งŒ ์ €์žฅ๋˜๊ณ , ์›๋ณธ ํ† ํ”ฝ์—์„œ๋Š” ์˜๊ตฌ์ ์œผ๋กœ ์†Œ๋น„๋œ ๊ฒƒ์œผ๋กœ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค

๐Ÿ’ก ํ•ด๊ฒฐ ๋ฐฉ์•ˆ

์‹คํŒจํ•œ ๋ ˆ์ฝ”๋“œ๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ ์˜คํ”„์…‹์„ ์ปค๋ฐ‹ํ•˜์ง€ ์•Š์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค:

// Offset ์ปค๋ฐ‹ (๋ฐฐ์น˜ ๋‹จ์œ„)
if (failCount > 0) {
    log.warn("โš ๏ธ ์‹คํŒจํ•œ ์ด๋ฒคํŠธ๊ฐ€ ์žˆ์–ด Offset์„ ์ปค๋ฐ‹ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค - fail: {}, success: {}, skip: {}", 
        failCount, successCount, skipCount);
    throw new RuntimeException(
        String.format("๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์ค‘ %d๊ฐœ ์‹คํŒจ, ์žฌ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด Offset ์ปค๋ฐ‹ ๊ฑฐ๋ถ€", failCount)
    );
}

acknowledgment.acknowledge();
log.info("โœ… Catalog ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์™„๋ฃŒ - success: {}, skip: {}, fail: {}", 
    successCount, skipCount, failCount);

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด:

  • ์‹คํŒจํ•œ ๋ ˆ์ฝ”๋“œ๊ฐ€ ์žˆ์œผ๋ฉด ์˜ˆ์™ธ๋ฅผ ๋˜์ ธ Kafka๊ฐ€ ํ•ด๋‹น ๋ฐฐ์น˜๋ฅผ ์žฌ์ฒ˜๋ฆฌํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค
  • ์‹คํŒจํ•œ ๋ ˆ์ฝ”๋“œ๋Š” DLQ์—๋„ ์ €์žฅ๋˜๊ณ , ๋™์‹œ์— ์žฌ์ฒ˜๋ฆฌ ๊ธฐํšŒ๋„ ์–ป์Šต๋‹ˆ๋‹ค
  • ๋ชจ๋“  ๋ ˆ์ฝ”๋“œ๊ฐ€ ์„ฑ๊ณตํ–ˆ์„ ๋•Œ๋งŒ ์˜คํ”„์…‹์„ ์ปค๋ฐ‹ํ•ฉ๋‹ˆ๋‹ค

๋™์ผํ•œ ๋ฌธ์ œ๊ฐ€ OrderEventConsumer์—๋„ ์กด์žฌํ•˜๋ฏ€๋กœ ํ•จ๊ป˜ ์ˆ˜์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.


๐Ÿง  Learnings used
Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 190
File: apps/commerce-streamer/src/main/java/com/loopers/applications/streamer/consumer/product/IncreaseProductViewKafkaConsumer.java:25-35
Timestamp: 2025-12-18T13:24:54.339Z
Learning: In this codebase, Kafka consumers delegate error handling and event tracking to the service layer via EventInboxAspect. Service methods annotated with InboxEvent are intercepted by the aspect, which handles failures by saving failed EventInbox entries and logging errors. This centralized approach avoids duplicating error handling logic across multiple consumers.

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:50.009Z
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.

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:16.424Z
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.

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:08.018Z
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.

eventInboxService.save(eventId, aggregateType, aggregateId, eventType);

// 3. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ฒ˜๋ฆฌ
Long productId = Long.parseLong(aggregateId);
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

Long.parseLong์— ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

aggregateId๋ฅผ Long.parseLong์œผ๋กœ ํŒŒ์‹ฑํ•  ๋•Œ(Line 108) NumberFormatException์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

๋ฌธ์ œ์ :

  • ์ž˜๋ชป๋œ ํ˜•์‹์˜ aggregateId๋กœ ์ธํ•ด ์ „์ฒด ํŠธ๋žœ์žญ์…˜์ด ์‹คํŒจ
  • ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ํŒŒ์‹ฑ ์‹คํŒจ ์›์ธ์„ ๋ช…ํ™•ํžˆ ๋‚˜ํƒ€๋‚ด์ง€ ๋ชปํ•จ

๊ถŒ์žฅ ํ•ด๊ฒฐ์ฑ…:
์ˆซ์ž ํŒŒ์‹ฑ ์ „์— ์œ ํšจ์„ฑ ๊ฒ€์ฆ์„ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ try-catch๋กœ ๋” ๋ช…ํ™•ํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ์ œ๊ณตํ•˜์„ธ์š”.

๐Ÿ”Ž ์ˆซ์ž ํŒŒ์‹ฑ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ์ถ”๊ฐ€
         // 3. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ฒ˜๋ฆฌ
-        Long productId = Long.parseLong(aggregateId);
+        Long productId;
+        try {
+            productId = Long.parseLong(aggregateId);
+        } catch (NumberFormatException e) {
+            log.error("โš ๏ธ aggregateId ํŒŒ์‹ฑ ์‹คํŒจ - aggregateId: {}", aggregateId, e);
+            throw new IllegalArgumentException("์ž˜๋ชป๋œ aggregateId ํ˜•์‹: " + aggregateId, e);
+        }
 
         switch (eventType) {
๐Ÿ“ 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
Long productId = Long.parseLong(aggregateId);
// 3. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ฒ˜๋ฆฌ
Long productId;
try {
productId = Long.parseLong(aggregateId);
} catch (NumberFormatException e) {
log.error("โš ๏ธ aggregateId ํŒŒ์‹ฑ ์‹คํŒจ - aggregateId: {}", aggregateId, e);
throw new IllegalArgumentException("์ž˜๋ชป๋œ aggregateId ํ˜•์‹: " + aggregateId, e);
}
switch (eventType) {
๐Ÿค– Prompt for AI Agents
In
apps/commerce-streamer/src/main/java/com/loopers/interfaces/consumer/CatalogEventConsumer.java
around line 108, Long.parseLong(aggregateId) can throw NumberFormatException and
currently lacks handling; wrap the parse in a try-catch (or pre-validate with a
regex/NumberUtils) to catch invalid format, log a clear, contextual error
including the aggregateId and event metadata, and either skip/acknowledge the
bad event or rethrow a domain-specific exception with a descriptive message so
the transaction doesn't fail silently due to an unhandled parsing error.

Comment on lines +38 to +40
properties:
auto:
offset.reset: earliest # ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋Š” earliest๋ถ€ํ„ฐ ์ฝ๊ธฐ
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:

head -50 modules/kafka/src/main/resources/kafka.yml

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

Length of output: 1650


ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์˜ offset ์„ค์ •์„ ๋ธŒ๋ž˜ํ‚ท ํ‘œ๊ธฐ๋ฒ•์œผ๋กœ ์ˆ˜์ •ํ•ด์ฃผ์„ธ์š”.

ํ˜„์žฌ YAML ์„ค์ •์—์„œ properties ๋งต ๋‚ด๋ถ€์˜ Kafka ์†์„ฑ์ด ์ค‘์ฒฉ๋œ ๊ตฌ์กฐ๋กœ ์ž‘์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. Spring Boot ๊ณต์‹ ๋ฌธ์„œ์— ๋”ฐ๋ฅด๋ฉด, properties ๋งต ๋‚ด๋ถ€์˜ Kafka ํด๋ผ์ด์–ธํŠธ ์†์„ฑ์€ ๋ธŒ๋ž˜ํ‚ท ํ‘œ๊ธฐ๋ฒ•์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ๊ตฌ์กฐ:

properties:
  auto:
    offset.reset: earliest

์ˆ˜์ • ํ•„์š”:

properties:
  "[auto.offset.reset]": earliest

YAML ํŒŒ์ผ์—์„œ๋Š” ๋ธŒ๋ž˜ํ‚ท์ด ํ‚ค๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํŒŒ์‹ฑํ•˜๊ธฐ ์œ„ํ•ด ๋”ฐ์˜ดํ‘œ๋กœ ๊ฐ์‹ธ์ ธ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” lines 5-14์˜ main profile์—์„œ๋„ ๋™์ผํ•˜๊ฒŒ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
In modules/kafka/src/main/resources/kafka.yml around lines 38 to 40 (and apply
same change to the main profile at lines 5 to 14), the Kafka client property is
written as a nested map (auto: offset.reset: earliest); replace it with bracket
notation and quoted key so the properties map parses correctly by Spring Boot:
change the nested key to "[auto.offset.reset]" and set its value to earliest;
apply the identical quoted bracket-key change in the main profile section as
well.

log.info("- ๋ฐฐ์น˜ ํฌ๊ธฐ: 500๊ฐœ");
log.info("- poll ํšŸ์ˆ˜: {}ํšŒ", pollCount);
log.info("- ์ด ์‹œ๊ฐ„: {}ms", totalTime);
log.info("- ์ดˆ๋‹น ์ฒ˜๋ฆฌ๋Ÿ‰: {:.2f}๊ฐœ/์ดˆ", (totalProcessed * 1000.0) / totalTime);
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

SLF4J ๋กœ๊ทธ ํฌ๋งท ์˜ค๋ฅ˜

{:.2f}๋Š” Python ํฌ๋งท ๋ฌธ๋ฒ•์ด๋ฉฐ, SLF4J์—์„œ๋Š” ์ง€์›๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. SLF4J๋Š” ๋‹จ์ˆœํžˆ {}๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ, ์†Œ์ˆ˜์  ํฌ๋งทํŒ…์€ ๋ณ„๋„๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”Ž ์ˆ˜์ • ๋ฐฉ์•ˆ:
-        log.info("- ์ดˆ๋‹น ์ฒ˜๋ฆฌ๋Ÿ‰: {:.2f}๊ฐœ/์ดˆ", (totalProcessed * 1000.0) / totalTime);
+        log.info("- ์ดˆ๋‹น ์ฒ˜๋ฆฌ๋Ÿ‰: {}๊ฐœ/์ดˆ", String.format("%.2f", (totalProcessed * 1000.0) / totalTime));

๋˜๋Š” Lines 175, 241, 242์—๋„ ๋™์ผํ•œ ์ˆ˜์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค:

-        log.info("- ์ดˆ๋‹น ์ฒ˜๋ฆฌ๋Ÿ‰: {:.2f}๊ฐœ/์ดˆ", (totalProcessed * 1000.0) / totalTime);
+        log.info("- ์ดˆ๋‹น ์ฒ˜๋ฆฌ๋Ÿ‰: {}๊ฐœ/์ดˆ", String.format("%.2f", (totalProcessed * 1000.0) / totalTime));

-        log.info("  - poll๋‹น ํ‰๊ท : {:.2f}ms", avgTimePerPoll);
+        log.info("  - poll๋‹น ํ‰๊ท : {}ms", String.format("%.2f", avgTimePerPoll));

-        log.info("  - ์ดˆ๋‹น ์ฒ˜๋ฆฌ๋Ÿ‰: {:.2f}๊ฐœ/์ดˆ\n", (totalProcessed * 1000.0) / totalTime);
+        log.info("  - ์ดˆ๋‹น ์ฒ˜๋ฆฌ๋Ÿ‰: {}๊ฐœ/์ดˆ\n", String.format("%.2f", (totalProcessed * 1000.0) / totalTime));
๐Ÿ“ 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
log.info("- ์ดˆ๋‹น ์ฒ˜๋ฆฌ๋Ÿ‰: {:.2f}๊ฐœ/์ดˆ", (totalProcessed * 1000.0) / totalTime);
log.info("- ์ดˆ๋‹น ์ฒ˜๋ฆฌ๋Ÿ‰: {}๊ฐœ/์ดˆ", String.format("%.2f", (totalProcessed * 1000.0) / totalTime));
๐Ÿค– Prompt for AI Agents
In
modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment4_BatchSizeTest.java
around line 105 (and similarly lines 175, 241, 242), the SLF4J log call
incorrectly uses Python-style "{:.2f}" formatting; replace it by formatting the
numeric value to the desired precision before passing it to log (e.g., use
String.format("%.2f", value) or BigDecimal#setScale) and then call
log.info("...: {}๊ฐœ/์ดˆ", formattedValue) so SLF4J receives a "{}" placeholder with
a pre-formatted string.

Comment on lines +284 to +287
int processedCount = 0;
records.forEach(record -> {
log.info("์ฒ˜๋ฆฌ: offset={}, value={}", record.offset(), record.value());
});
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

์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ๋ณ€์ˆ˜

processedCount ๋ณ€์ˆ˜๊ฐ€ ์„ ์–ธ๋˜์—ˆ์ง€๋งŒ ์‚ฌ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ œ๊ฑฐํ•˜๊ฑฐ๋‚˜ ์‹ค์ œ ์นด์šดํŒ… ๋กœ์ง์„ ์ถ”๊ฐ€ํ•ด ์ฃผ์„ธ์š”.

๐Ÿ”Ž ์ˆ˜์ • ๋ฐฉ์•ˆ:
-                int processedCount = 0;
                 records.forEach(record -> {
                     log.info("์ฒ˜๋ฆฌ: offset={}, value={}", record.offset(), record.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
int processedCount = 0;
records.forEach(record -> {
log.info("์ฒ˜๋ฆฌ: offset={}, value={}", record.offset(), record.value());
});
records.forEach(record -> {
log.info("์ฒ˜๋ฆฌ: offset={}, value={}", record.offset(), record.value());
});
๐Ÿค– Prompt for AI Agents
In
modules/kafka/src/test/java/com/loopers/kafka/learning/Experiment4_BatchSizeTest.java
around lines 284 to 287 the local variable processedCount is declared but never
used; either remove the unused declaration or implement counting by incrementing
processedCount inside the forEach (or replace forEach with an indexed loop) and
assert or log the final count as appropriate for the test; pick one approach and
apply it consistently so there are no unused variables.

Comment on lines +166 to +181
### 3. Consumer Group ๊ด€๋ฆฌ

```bash
# Consumer Group ๋ชฉ๋ก
kafka-consumer-groups.sh --bootstrap-server localhost:19092 --list

# ์˜คํ”„์…‹ ํ™•์ธ
kafka-consumer-groups.sh --bootstrap-server localhost:19092 \
--group learning-group \
--describe

# Group ์‚ญ์ œ (์žฌ์‹คํ—˜ ์‹œ)
kafka-consumer-groups.sh --bootstrap-server localhost:19092 \
--group learning-group \
--delete
```
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

ํฌํŠธ ๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜ ํ™•์ธ ํ•„์š”

Consumer Group ๊ด€๋ฆฌ ์„น์…˜์˜ Kafka CLI ๋ช…๋ น์–ด๋Š” localhost:19092๋ฅผ ์‚ฌ์šฉํ•˜์ง€๋งŒ, Embedded Kafka ํ…Œ์ŠคํŠธ๋Š” localhost:9092๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ํ˜ผ๋™์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ํฌํŠธ๋ฅผ ํ†ต์ผํ•˜๊ฑฐ๋‚˜, ๋‘ ํ™˜๊ฒฝ(Embedded vs Docker)์— ๋Œ€ํ•œ ์„ค๋ช…์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
In modules/kafka/src/test/java/com/loopers/kafka/learning/README.md around lines
166 to 181, the Kafka CLI examples use port 19092 while the Embedded Kafka tests
use 9092; either make the ports consistent or clarify environments. Update the
CLI examples to use the same port as the tests (change 19092 to 9092) if the
README is intended for the Embedded Kafka setup, or add a one-line note above
this section explaining that 19092 is for the Docker/standalone broker and 9092
is for Embedded Kafka tests so readers know which port to use for each
environment.

Comment on lines 212 to 216
## ๐Ÿ“š ์ฐธ๊ณ  ๋ฌธ์„œ

- `.claude/round-8/kafka-configuration-guide.md` - ์ƒ์„ธ ์„ค์ • ๊ฐ€์ด๋“œ
- `.claude/round-8/exactly-once-semantics.md` - Exactly-Once ๊ฐœ๋…
- `.claude/round-8/inbox-pattern-analysis.md` - Inbox ํŒจํ„ด ๋ถ„์„
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
# Check if referenced documentation files exist
for file in ".claude/round-8/kafka-configuration-guide.md" \
             ".claude/round-8/exactly-once-semantics.md" \
             ".claude/round-8/inbox-pattern-analysis.md"; do
  if [ -f "$file" ]; then
    echo "โœ… ์กด์žฌ: $file"
  else
    echo "โŒ ์—†์Œ: $file"
  fi
done

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

Length of output: 234


์ €์žฅ์†Œ์—์„œ ์ฐธ๊ณ  ๋ฌธ์„œ ํŒŒ์ผ๋“ค์ด ๋ˆ„๋ฝ๋จ

.claude/round-8/ ๋””๋ ‰ํ† ๋ฆฌ์˜ ์„ธ ๋ฌธ์„œ ํŒŒ์ผ์ด ๋ชจ๋‘ ์ปค๋ฐ‹๋˜์–ด ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค:

  • kafka-configuration-guide.md - ์—†์Œ
  • exactly-once-semantics.md - ์—†์Œ
  • inbox-pattern-analysis.md - ์—†์Œ

์ด ํŒŒ์ผ๋“ค์„ ์ €์žฅ์†Œ์— ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ README์—์„œ ์ฐธ์กฐ๋ฅผ ์ œ๊ฑฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿค– Prompt for AI Agents
In modules/kafka/src/test/java/com/loopers/kafka/learning/README.md around lines
212 to 216, the README references three files under .claude/round-8 that are not
present in the repo; either add the missing files to the repository at
.claude/round-8/{kafka-configuration-guide.md, exactly-once-semantics.md,
inbox-pattern-analysis.md} with appropriate content and commit them, or remove
or replace the three bullet references in the README to avoid broken
documentation links; ensure commits include correct paths and update any CI or
packaging rules if those files need to be included.

@looper88888
Copy link
Contributor

โœ… Checklist

๐ŸŽพ Producer
[ ] ๋„๋ฉ”์ธ(์• ํ”Œ๋ฆฌ์ผ€์ด์…˜) ์ด๋ฒคํŠธ ์„ค๊ณ„
[ ] Producer ์•ฑ์—์„œ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ฐœํ–‰ (catalog-events, order-events, ๋“ฑ)
[ ] PartitionKey ๊ธฐ๋ฐ˜์˜ ์ด๋ฒคํŠธ ์ˆœ์„œ ๋ณด์žฅ
[ ] ๋ฉ”์„ธ์ง€ ๋ฐœํ–‰์ด ์‹คํŒจํ–ˆ์„ ๊ฒฝ์šฐ์— ๋Œ€ํ•ด ๊ณ ๋ฏผํ•ด๋ณด๊ธฐ

โšพ Consumer
[ ] Consumer ๊ฐ€ Metrics ์ง‘๊ณ„ ์ฒ˜๋ฆฌ
[ ] event_handled ํ…Œ์ด๋ธ”์„ ํ†ตํ•œ ๋ฉฑ๋“ฑ ์ฒ˜๋ฆฌ ๊ตฌํ˜„
[ ] ์žฌ๊ณ  ์†Œ์ง„ ์‹œ ์ƒํ’ˆ ์บ์‹œ ๊ฐฑ์‹ 
[ ] ์ค‘๋ณต ๋ฉ”์„ธ์ง€ ์žฌ์ „์†ก ํ…Œ์ŠคํŠธ โ†’ ์ตœ์ข… ๊ฒฐ๊ณผ๊ฐ€ ํ•œ ๋ฒˆ๋งŒ ๋ฐ˜์˜๋˜๋Š”์ง€ ํ™•์ธ

@coderabbitai ์ฒดํฌ๋ฆฌ์ŠคํŠธ ํ™•์ธํ•ด

@coderabbitai
Copy link

coderabbitai bot commented Dec 20, 2025

Tip

For best results, initiate chat on the files or code changes.

@looper88888, ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค! ๊ฐ ํ•ญ๋ชฉ๋ณ„๋กœ ๊ฒ€์ฆ ๊ฒฐ๊ณผ๋ฅผ ๊ณต์œ ๋“œ๋ฆฝ๋‹ˆ๋‹ค:

๐ŸŽพ Producer

โœ… ๋„๋ฉ”์ธ(์• ํ”Œ๋ฆฌ์ผ€์ด์…˜) ์ด๋ฒคํŠธ ์„ค๊ณ„

  • ์ด๋ฒคํŠธ๊ฐ€ ์ž˜ ์„ค๊ณ„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค
  • Catalog ์ด๋ฒคํŠธ: LikeCreatedEvent, LikeDeletedEvent, ProductViewedEvent
  • Order ์ด๋ฒคํŠธ: OrderCreatedEvent, PaymentSuccessEvent
  • ๊ฐ ์ด๋ฒคํŠธ๋Š” eventId, aggregateType, aggregateId, eventType ํ•„๋“œ๋ฅผ ํฌํ•จ

โœ… Producer ์•ฑ์—์„œ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ฐœํ–‰

  • EventKafkaProducer ํด๋ž˜์Šค๊ฐ€ Outbox ์ด๋ฒคํŠธ๋ฅผ Kafka๋กœ ๋ฐœํ–‰
  • Topic ๋ผ์šฐํŒ… ๋กœ์ง:
    • ORDER/PAYMENT โ†’ order-events topic
    • PRODUCT/LIKE โ†’ catalog-events topic
  • OutboxEventPublisher๊ฐ€ ์Šค์ผ€์ค„๋Ÿฌ๋กœ PENDING ์ƒํƒœ์˜ ์ด๋ฒคํŠธ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ๋ฐœํ–‰

โœ… PartitionKey ๊ธฐ๋ฐ˜์˜ ์ด๋ฒคํŠธ ์ˆœ์„œ ๋ณด์žฅ

  • EventKafkaProducer.publish() ๋ฉ”์„œ๋“œ์—์„œ aggregateId๋ฅผ ํŒŒํ‹ฐ์…˜ ํ‚ค๋กœ ์‚ฌ์šฉ
  • ๋™์ผํ•œ ์—”ํ‹ฐํ‹ฐ(์˜ˆ: ๊ฐ™์€ ์ƒํ’ˆ)์— ๋Œ€ํ•œ ์ด๋ฒคํŠธ๋“ค์ด ๊ฐ™์€ ํŒŒํ‹ฐ์…˜์œผ๋กœ ์ „์†ก๋˜์–ด ์ˆœ์„œ ๋ณด์žฅ

โœ… ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰ ์‹คํŒจ ์‹œ ๋Œ€์‘

  • Transactional Outbox Pattern ์ ์šฉ
  • ๋น„์ฆˆ๋‹ˆ์Šค ํŠธ๋žœ์žญ์…˜ ๋‚ด์—์„œ event_outbox ํ…Œ์ด๋ธ”์— ์ด๋ฒคํŠธ ์ €์žฅ
  • Kafka ๋ฐœํ–‰ ์„ฑ๊ณต โ†’ Published ์ƒํƒœ๋กœ ์—…๋ฐ์ดํŠธ
  • Kafka ๋ฐœํ–‰ ์‹คํŒจ โ†’ Failed ์ƒํƒœ๋กœ ๋งˆํ‚นํ•˜๊ณ  ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ €์žฅ
  • OutboxEventPublisher๊ฐ€ ์ฃผ๊ธฐ์ ์œผ๋กœ ์žฌ์‹œ๋„ (PENDING ์ด๋ฒคํŠธ ์žฌ๋ฐœํ–‰)

โšพ Consumer

โœ… Consumer๊ฐ€ Metrics ์ง‘๊ณ„ ์ฒ˜๋ฆฌ

  • ProductMetricsService ๊ตฌํ˜„:
    • incrementLikeCount() / decrementLikeCount()
    • incrementViewCount()
    • incrementOrderCount(quantity, amount)
  • CatalogEventConsumer์—์„œ ์ด๋ฒคํŠธ ํƒ€์ž…๋ณ„ ๋ฉ”ํŠธ๋ฆญ ์—…๋ฐ์ดํŠธ
  • OrderEventConsumer์—์„œ ์ฃผ๋ฌธ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ๋ฉ”ํŠธ๋ฆญ ์—…๋ฐ์ดํŠธ
  • product_metrics ํ…Œ์ด๋ธ”์— ์ง‘๊ณ„ ๋ฐ์ดํ„ฐ ์ €์žฅ (optimistic locking with @Version)

โœ… event_handled ํ…Œ์ด๋ธ”์„ ํ†ตํ•œ ๋ฉฑ๋“ฑ ์ฒ˜๋ฆฌ ๊ตฌํ˜„

  • EventInboxService.isDuplicate(eventId) - ์ค‘๋ณต ์ฒดํฌ
  • EventInboxService.save() - ์ด๋ฒคํŠธ ์ €์žฅ
  • event_inbox ํ…Œ์ด๋ธ”์— event_id unique ์ธ๋ฑ์Šค ์„ค์ •
  • ๋‘ Consumer ๋ชจ๋‘์—์„œ ์ค‘๋ณต ์ฒดํฌ ํ›„ ์ฒ˜๋ฆฌ ์Šคํ‚ต ๋กœ์ง ๊ตฌํ˜„
  • ํ…Œ์ŠคํŠธ: EventInboxServiceTest์—์„œ ์ค‘๋ณต ๊ฐ์ง€ ๊ฒ€์ฆ

โœ… ์žฌ๊ณ  ์†Œ์ง„ ์‹œ ์ƒํ’ˆ ์บ์‹œ ๊ฐฑ์‹ 

  • OrderFacade์—์„œ ์ฃผ๋ฌธ ์•„์ดํ…œ ์ฒ˜๋ฆฌ ํ›„ ์žฌ๊ณ ๊ฐ€ 0์ด ๋˜๋ฉด:
    if (product.getStock() == 0) {
        productCacheService.evictCache(product.getId());
    }
  • ProductCacheService.evictCache(productId) ๋ฉ”์„œ๋“œ๋กœ ๊ฐœ๋ณ„ ์ƒํ’ˆ ์บ์‹œ ๋ฌดํšจํ™”
  • @CacheEvict ์–ด๋…ธํ…Œ์ด์…˜ ์‚ฌ์šฉ

โœ… ์ค‘๋ณต ๋ฉ”์‹œ์ง€ ์žฌ์ „์†ก ํ…Œ์ŠคํŠธ

  • CatalogEventConsumerTest.์ค‘๋ณต_์ด๋ฒคํŠธ_๋ฉฑ๋“ฑ์„ฑ_์ฒ˜๋ฆฌ() - LikeCreatedEvent ์ค‘๋ณต ์‹œ ๋ฉ”ํŠธ๋ฆญ ์—…๋ฐ์ดํŠธ ์•ˆ ๋จ ๊ฒ€์ฆ
  • OrderEventConsumerTest.์ค‘๋ณต_์ด๋ฒคํŠธ๋Š”_๋ฌด์‹œ๋œ๋‹ค() - isDuplicate=true ์‹œ ์ฒ˜๋ฆฌ ์Šคํ‚ต ๊ฒ€์ฆ
  • Inbox ์ €์žฅ ๋ฐ ๋ฉ”ํŠธ๋ฆญ ์—…๋ฐ์ดํŠธ๊ฐ€ ํ•œ ๋ฒˆ๋งŒ ๋ฐœ์ƒํ•˜๋Š”์ง€ Mockito verify๋กœ ํ™•์ธ

์ถ”๊ฐ€ ๊ฒ€์ฆ ์‚ฌํ•ญ

โœ… Dead Letter Queue (DLQ) ๊ตฌํ˜„

  • ์ฒ˜๋ฆฌ ์‹คํŒจํ•œ ๋ฉ”์‹œ์ง€๋Š” dead_letter_queue ํ…Œ์ด๋ธ”์— ์ €์žฅ
  • DeadLetterQueueService.save() - topic, eventId, payload, error, retryCount ๊ธฐ๋ก

โœ… Batch ์ฒ˜๋ฆฌ ๋ฐ Manual Commit

  • ๋‘ Consumer ๋ชจ๋‘ List<ConsumerRecord> ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ
  • ์„ฑ๊ณต/์‹คํŒจ/์Šคํ‚ต ์นด์šดํ„ฐ๋กœ ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ ๋กœ๊น…
  • ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ํ›„ acknowledgment.acknowledge() ์ˆ˜๋™ ์ปค๋ฐ‹

โœ… Learning Tests

  • Experiment1_AutoCommitTest - auto vs manual commit
  • Experiment2_OffsetResetTest - earliest vs latest
  • Experiment3_PollIntervalTest - max.poll.interval.ms timeout
  • Experiment4_BatchSizeTest - batch size trade-offs

๋ชจ๋“  ์ฒดํฌ๋ฆฌ์ŠคํŠธ ํ•ญ๋ชฉ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! ๐ŸŽ‰

Kafka ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ํŒŒ์ดํ”„๋ผ์ธ์ด ๊ฒฌ๊ณ ํ•˜๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ๊ณ , Outbox/Inbox ํŒจํ„ด, DLQ, ๋ฉฑ๋“ฑ์„ฑ ์ฒ˜๋ฆฌ, ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๋“ฑ ํ”„๋กœ๋•์…˜ ๋ ˆ๋ฒจ์˜ ๊ณ ๋ ค์‚ฌํ•ญ๋“ค์ด ์ž˜ ๋ฐ˜์˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.


๐Ÿง  Learnings used
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:50.009Z
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.

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:16.424Z
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.

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:08.018Z
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.

Learnt from: kilian-develop
Repo: Loopers-dev-lab/loopers-spring-java-template PR: 190
File: apps/commerce-streamer/src/main/java/com/loopers/applications/streamer/consumer/product/IncreaseProductViewKafkaConsumer.java:25-35
Timestamp: 2025-12-18T13:24:54.339Z
Learning: In this codebase, Kafka consumers delegate error handling and event tracking to the service layer via EventInboxAspect. Service methods annotated with InboxEvent are intercepted by the aspect, which handles failures by saving failed EventInbox entries and logging errors. This centralized approach avoids duplicating error handling logic across multiple consumers.

README ํŒŒ์ผ์—์„œ ํ”„๋กœ์ ํŠธ ๋‚ด๋ถ€ ์ฐธ์กฐ๋ฅผ ์ผ๋ฐ˜์ ์ธ ์„ค๋ช…์œผ๋กœ ์ˆ˜์ •
- performance-test/README.md
- modules/kafka/learning/README.md
@rkdalsdn94 rkdalsdn94 merged commit 9c7961e into Loopers-dev-lab:rkdalsdn94 Dec 22, 2025
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.

2 participants