Skip to content

About

A payment platform built with Java and Spring

Topics

Resources

Stars

8 stars

Watchers

1 watching

Forks

Latest commit

ย 

History

2,166 Commits

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

Payments Platform

๊ฒฐ์ œ ์—ฐ๋™ ํ™˜๊ฒฝ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋“ค โ€” ์œ„๋ณ€์กฐ ๋ฐฉ์ง€ ยท ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ ยท ๋น„๋™๊ธฐ ๊ฒฐ์ œ ์ฒ˜๋ฆฌ ยท ์ž๋™ ๋ณต๊ตฌ ยท ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜ โ€” ์„ ๋‹จ๊ณ„๋ณ„๋กœ ์ง์ ‘ ์„ค๊ณ„ํ•˜๊ณ  ๊ตฌํ˜„ํ•œ ํ”„๋กœ์ ํŠธ์ด๋‹ค.

CI

โœ… Phase 6 ์™„๋ฃŒ
MSA ์„œ๋น„์Šค + Eureka + Gateway ยท Kafka ์–‘๋ฐฉํ–ฅ ์ฝ”๋ ˆ์˜ค๊ทธ๋ž˜ํ”ผ + Outbox ๋ชจ๋ธ + ๋ถ„์‚ฐ ํŠธ๋ ˆ์ด์‹ฑ ์šด์˜ ยท ๋‹จ์œ„ 861 / ํ†ตํ•ฉ 59 PASS
๐Ÿ”œ ๋‹ค์Œ ยท Phase 7
ํšŒ๋ณต์„ฑ ๊ฒ€์ฆ (์žฅ์•  ์ฃผ์ž… + k6 ์‹œ๋‚˜๋ฆฌ์˜ค ์žฌ์„ค๊ณ„ + ๋กœ์ปฌ ์˜คํ† ์Šค์ผ€์ผ๋Ÿฌ + ์„œํ‚ท๋ธŒ๋ ˆ์ด์ปค) โ€” ์•Œ๋žŒ ๊ทœ์น™ + Toxiproxy ์žฅ์•  ๋“œ๋ฆด ์ธํ”„๋ผ๋Š” ์„ ํ–‰ ๊ตฌ์ถ• ์™„๋ฃŒ

์ž‘์„ฑ์ž: hyoguoo ยท Wiki ยท Blog


๐Ÿš€ ์ฃผ์š” ํ•ด๊ฒฐ ๊ณผ์ œ

ํ•ด๊ฒฐ ์˜์—ญ ์ฃผ์š” ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• ๊ฐœ์„  ๊ฒฐ๊ณผ
๋™๊ธฐ โ†’ ๋น„๋™๊ธฐ ์ „ํ™˜ Toss API ์ง€์—ฐ์ด HTTP ์Šค๋ ˆ๋“œ๋ฅผ ๋ธ”๋กœํ‚นํ•˜๋˜ ๋™๊ธฐ ๊ตฌ์กฐ๋ฅผ Outbox์™€ ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ Worker๋ฅผ ํ™œ์šฉํ•ด ๋น„๋™๊ธฐ๋กœ ์ „ํ™˜ TPS 47% ํ–ฅ์ƒ / ์š”์ฒญ ์œ ์‹ค ์—†์Œ (k6 ๋ถ€ํ•˜ํ…Œ์ŠคํŠธ ๊ธฐ์ค€)
์ •ํ•ฉ์„ฑ / ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ ํด๋ผ์ด์–ธํŠธยท์„œ๋ฒ„ยทPG์‚ฌ ๊ฐ„ ๊ฒฐ์ œ ๋ฐ์ดํ„ฐ ๊ต์ฐจ ๊ฒ€์ฆ ๋ฐ Checkout API ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ (TOCTOU ๋™์‹œ์„ฑ ๋ฌธ์ œ ํ•ด๊ฒฐ) ์ค‘๋ณต ๊ฒฐ์ œ ๋ฐ ๊ธˆ์•ก ์œ„๋ณ€์กฐ ์›์ฒœ ์ฐจ๋‹จ
์žฅ์•  ๋ณต๊ตฌ ์ž๋™ํ™” ๋ฐฑ์˜คํ”„(Backoff) ๊ธฐ๋ฐ˜ ์žฌ์‹œ๋„, ์‹คํŒจ ๊ฑด DLQ ์ž๋™ ๊ฒฉ๋ฆฌ, ์Šค์ผ€์ค„๋Ÿฌ๋ฅผ ํ†ตํ•œ ์ƒํƒœ ๋ณต๊ตฌ ๋ฐ Redis Lua ์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ™œ์šฉํ•œ ์›์ž์  ์žฌ๊ณ  ๋ณด์ƒ DLQ ๊ฒฉ๋ฆฌ๋กœ ์ˆ˜๋™ ๊ฐœ์ž… ์ตœ์†Œํ™” + ์ด์ค‘ ๋ณต๊ตฌ๋ฅผ ๋ฐฉ์ง€ํ•˜๋Š” ์•ˆ์ „ํ•œ ์žฌ๊ณ  ๋ณด์ƒ
MSA ์ „ํ™˜ ๋ฐ Kafka ๋„์ž… ๋ชจ๋†€๋ฆฌ์Šค ์•„ํ‚คํ…์ฒ˜๋ฅผ 4๊ฐœ์˜ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๋กœ ๋ถ„๋ฆฌ(Eureka, Gateway ํฌํ•จ)ํ•˜๊ณ  ๊ฒฐ์ œ์™€ PG ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹ ์„ Kafka๋ฅผ ํ†ตํ•œ ๋น„๋™๊ธฐ ์ด๋ฒคํŠธ๋กœ ์ „ํ™˜ ๊ฒฐํ•ฉ๋„ ๊ฐ์†Œ ๋ฐ AMOUNT_MISMATCH ๊ฒ€์ฆ์„ ํ†ตํ•œ ์ƒํƒœ ๋ถˆ์ผ์น˜ ๋ฐฉ์–ด
Outbox ํŒจํ„ด ๋ฐ ๋ฉฑ๋“ฑ์„ฑ ์‹œ์Šคํ…œ ํŠน์„ฑ์— ๋งž์ถฐ ๊ฒฐ์ œ/PG ์„œ๋น„์Šค์˜ Outbox ์ •์ฑ…์„ ๋ถ„๋ฆฌํ•˜๊ณ , ๋ฉ”์‹œ์ง€ ์ค‘๋ณต ์†Œ๋น„๋ฅผ ๋ง‰๊ธฐ ์œ„ํ•œ ์„œ๋น„์Šค๋ณ„ ๋ฉฑ๋“ฑ์„ฑ ๊ฒ€์ฆ ๋ฃฐ(Kafka EOS, RDB ๋ฉฑ๋“ฑ INSERT, inbox UNIQUE) ์ ์šฉ At-least-once ์ „๋‹ฌ ๋ณด์žฅ ๋ฐ ์ค‘๋ณต ์ฒ˜๋ฆฌ ๋ฐฉ์ง€
์™ธ๋ถ€ ์—ฐ๋™ ํ๋ฆ„ ๊ฒฉ๋ฆฌ ์ˆ˜์‹ (Inbox ์ €์žฅ) โ†’ ๋ฒค๋” API ํ˜ธ์ถœ ๋ฐ ์ฒ˜๋ฆฌ(Virtual Thread) โ†’ ์™„๋ฃŒ ์ด๋ฒคํŠธ ๋ฐœํ–‰(Kafka) ๋‹จ๊ณ„๋ฅผ ๋ถ„๋ฆฌํ•˜๊ณ  ๊ฐ ๊ตฌ๊ฐ„์— ์ฑ„๋„๊ณผ ํด๋ฐฑ์„ ๋‘ฌ ์žฅ์•  ๊ฒฉ๋ฆฌ ์™ธ๋ถ€ ์—ฐ๋™ ์ง€์—ฐ์ด ๋‚ด๋ถ€ ์‹œ์Šคํ…œ์— ๋ฏธ์น˜๋Š” ์˜ํ–ฅ ์ฐจ๋‹จ ๋ฐ ํ”„๋กœ์„ธ์Šค ์ค‘๋‹จ ์‹œ ์•ˆ์ „ํ•œ ํšŒ์ˆ˜
๋ถ„์‚ฐ ํ™˜๊ฒฝ ์ถ”์  MSA ๊ฐ„ ํ๋ฆ„์„ ์ถ”์ ํ•˜๊ธฐ ์œ„ํ•ด OTel ์ปจํ…์ŠคํŠธ์™€ MDC ๋กœ๊ทธ ์‹๋ณ„์ž๋ฅผ ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ ๋ฐ ๋ฉ”๋ชจ๋ฆฌ ์ฑ„๋„ ๊ฒฝ๊ณ„์—์„œ ์ง์ ‘ ์บก์ฒ˜ํ•˜๊ณ  ๋ณต์›ํ•˜๋„๋ก ๊ตฌํ˜„ 5๊ฐœ ์„œ๋น„์Šค์™€ Kafka ์ „๋ฐ˜์— ๊ฑธ์นœ traceId ์—ฐ์†์„ฑ ํ™•๋ณด
MySQL Index Merge ๋ฐ๋“œ๋ฝ ์„œ๋กœ ๋‹ค๋ฅธ ์ฃผ๋ฌธ์˜ ์Šน์ธ UPDATE๊ฐ€ ์ถฉ๋Œํ•˜๋Š” ๊ฒฝ๋กœ๋ฅผ ์‹คํ–‰ ๊ณ„ํšยท๋ฝ ๋คํ”„๋กœ ์žฌํ˜„ํ•˜๊ณ , ์ƒํƒœ ๋‹จ์ผ ์ธ๋ฑ์Šค๋ฅผ ์ƒํƒœยท์‹œ๊ฐ„ ๋ณตํ•ฉ ์ธ๋ฑ์Šค๋กœ ๊ต์ฒด ๊ฒฐ์ œ ์™„๋ฃŒ p99 73.10์ดˆ โ†’ 9.28์ดˆ / ๊ฒฐ๊ณผ ์ €์žฅ ๋ฐ๋“œ๋ฝ 163๊ฑด โ†’ 0๊ฑด (MSA ๋™์ผ ๊ตฌ์„ฑ, ๊ฐ 1ํšŒ ์ธก์ •)

TOCTOU: Time-Of-Check-Time-Of-Use ๊ฒฝ์Ÿ ์กฐ๊ฑด ยท DLQ: Dead Letter Queue ยท EOS: Exactly-Once Semantics
ยท VT: Virtual Thread (๊ฐ€์ƒ ์Šค๋ ˆ๋“œ) ยท OTel: OpenTelemetry ยท MDC: Mapped Diagnostic Context (๋กœ๊ทธ ์ปจํ…์ŠคํŠธ) ยท TX: Transaction


๐Ÿ—บ๏ธ ๊ฐœ๋ฐœ ๊ณผ์ •

Phase ๋ชฉํ‘œ ๊ตฌํ˜„ ๋‚ด์šฉ
Phase 1 ๊ฒฐ์ œ ๋ฐ์ดํ„ฐ ์œ„๋ณ€์กฐ ๋ฐฉ์–ด ๊ต์ฐจ ๊ฒ€์ฆ ์—ฐ๋™
Phase 2 ๊ฒฐํ•ฉ๋„ ํ•ด์†Œ ๋ฐ ์ž๋™ ๋ณต๊ตฌ ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„ ์ตœ์†Œํ™” ยท ์ƒํƒœ ๊ธฐ๋ฐ˜ ๋ณต๊ตฌ ๋ชจ๋ธ ๋ฐ ์žฌ์‹œ๋„ ๋กœ์ง
Phase 3 ์‹œ์Šคํ…œ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์šด์˜ ์•ˆ์ •ํ™” ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ ยท ๊ตฌ์กฐํ™”๋œ ๋กœ๊น… ยท ๊ฒฐ์ œ ์ด๋ ฅ ์ถ”์  ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง
Phase 4 ์ •ํ•ฉ์„ฑ ๊ฐ•ํ™” ๋ฐ ์ค‘๋ณต ์š”์ฒญ ๋ฐฉ์ง€ ๋ณด์ƒ TX ์‹คํŒจ ๋Œ€์‘ ยท Checkout ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ
Phase 5 ๋น„๋™๊ธฐ ๊ฒฐ์ œ ์•„ํ‚คํ…์ฒ˜ ๋„์ž… ๋น„๋™๊ธฐ Outbox ยท ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ ๊ธฐ๋ฐ˜ ๊ฒฐ์ œ ํ”Œ๋กœ์šฐ ยท ๋„๋ฉ”์ธ ์ƒํƒœ ๋จธ์‹ ๊ณผ ์žฅ์•  ๋‚ด์„ฑ ๋ณต๊ตฌ ์ฒด๊ณ„
Phase 6 MSA ์ „ํ™˜ ๋ฐ ๋ถ„์‚ฐ ํ™˜๊ฒฝ ๊ณ ๋„ํ™” MSA ์ „ํ™˜ ยท ์ด๋ฒคํŠธ ๋“œ๋ฆฌ๋ธ ์ฝ”๋ ˆ์˜ค๊ทธ๋ž˜ํ”ผ ยท ์žฌ๊ณ  ์บ์‹œ ๋ณด์ƒ ํšŒ๋ณต โ€” Lua atomic ยท ์žฌ๊ณ  ์„ ์ฐจ๊ฐ ๊ฒŒ์ดํŠธ ์ƒํ’ˆ ๋‹จ์œ„ ๋ถ„ํ•ด ยท Outbox ํŒจํ„ด ยท ๋ฉ”์‹œ์ง€ ์ „๋‹ฌ ๋ณด์žฅ + dedupe ยท ๊ฒฐ์ œ ๋ณต๊ตฌ ๋ชจ๋ธ ยท PG ๊ฒฐ์ œ ํ™•์ธ ํ๋ฆ„ ยท ๋ถ„์‚ฐ ํŠธ๋ ˆ์ด์‹ฑ
ETC ํ™•์žฅ์„ฑ์„ ๊ณ ๋ คํ•œ ์„ค๊ณ„ ์ „๋žต ํŒจํ„ด ๊ธฐ๋ฐ˜ ๋ฉ€ํ‹ฐ PG ์—ฐ๋™
ETC AI ํŽ˜์–ด ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์›Œํฌํ”Œ๋กœ์šฐ ์„œ๋ธŒ์—์ด์ „ํŠธ ๊ธฐ๋ฐ˜ ์›Œํฌํ”Œ๋กœ์šฐ

๐ŸŒ ํ˜„์žฌ ์‹œ์Šคํ…œ (Phase 6)

4 ์„œ๋น„์Šค + Kafka ์–‘๋ฐฉํ–ฅ ์ฝ”๋ ˆ์˜ค๊ทธ๋ž˜ํ”ผ + Outbox ๋ชจ๋ธ + ๋ถ„์‚ฐ ํŠธ๋ ˆ์ด์‹ฑ ์œ„์—์„œ ์ฒ˜๋ฆฌ
๊ฐ ํ•ญ๋ชฉ ์ œ๋ชฉ์„ ํด๋ฆญํ•˜๋ฉด ์ƒ์„ธ ์„ค๊ณ„๊ฐ€ ๋‹ด๊ธด Wiki ๋กœ ์ด๋™

  • ๋‹จ์ผ ๋ชจ๋†€๋ฆฌ์Šค -> 4 ์„œ๋น„์Šค (payment / pg / product / user) + Eureka + Gateway ๋กœ ๋ถ„ํ•ด + DB per service
  • Redis ๋‘ ์ธ์Šคํ„ด์Šค (dedupe / stock) ์šฉ๋„๋ณ„ ๋ถ„๋ฆฌ
  • payment โ†” pg ๋Š” Kafka ์–‘๋ฐฉํ–ฅ ๋ฉ”์‹œ์ง€๋กœ๋งŒ ํ†ต์‹  โ€” payment-service (๊ฒฐ์ œ ์ƒํƒœ ๊ด€๋ฆฌ), pg-service (PG ํ˜ธ์ถœ ๋‹ด๋‹น)
flowchart LR
    Browser["๋ธŒ๋ผ์šฐ์ €"]

    subgraph Edge
        GW["gateway"]
        E["eureka"]
    end

    subgraph Apps["์„œ๋น„์Šค"]
        Pay["payment-service"]
        Pg["pg-service"]
        Prod["product-service"]
        Usr["user-service"]
    end

    subgraph Stores
        MyP[("mysql-payment")]
        MyG[("mysql-pg")]
        MyPr[("mysql-product")]
        MyU[("mysql-user")]
        RedD[("redis-dedupe")]
        RedS[("redis-stock")]
    end

    K[("Kafka")]
    Browser -->|HTTP| GW
    GW --> Pay & Prod & Usr
    Pay & Pg & Prod & Usr -. heartbeat .-> E
    Pay --> MyP
    Pg --> MyG
    Prod --> MyPr
    Usr --> MyU
    Pay --> RedD
    Pay --> RedS
    Pay <-->|" payment.commands.confirm /\npayment.events.confirmed "| K
    K <--> Pg
    Pay -->|" payment.events.stock-committed "| K
    K --> Prod
    Pg -->|HTTP| Vendor["Toss / NicePay"]
    Pay -->|HTTP product/user ์กฐํšŒ| Prod
    Pay -->|HTTP user ์กฐํšŒ| Usr
Loading

Kafka ํ† ํ”ฝ ์นดํƒˆ๋กœ๊ทธ (์šด์˜ 3 + DLQ 2)

ํ† ํ”ฝ ๋ฐœํ–‰์ž ์†Œ๋น„์ž ์˜๋ฏธ
payment.commands.confirm payment (์ตœ์ดˆ) + pg (self-retry) pg ๊ฒฐ์ œ ํ™•์ • ๋ช…๋ น
payment.events.confirmed pg payment PG ๊ฒฐ๊ณผ ํšŒ์‹  (APPROVED / FAILED / QUARANTINED)
payment.events.stock-committed payment (APPROVED ์‹œ) product ์žฌ๊ณ  ํ™•์ • ํ†ต์ง€
payment.commands.confirm.dlq pg (attempt 4 ๊ฒฉ๋ฆฌ) pg DLQ ์ปจ์Šˆ๋จธ self-loop retry ํ•œ๋„ ์ดˆ๊ณผ
payment.events.confirmed.dlq payment (DefaultErrorHandler retry ํ•œ๋„ ์ดˆ๊ณผ) (์ˆ˜๋™) ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ ์˜๊ตฌ ์‹คํŒจ ๊ฒฉ๋ฆฌ

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒํƒœ ๋ณ€๊ฒฝ๊ณผ Kafka ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰์„ ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋ฌถ์„ ์ˆ˜ ์—†์–ด ๋ฐœ์ƒํ•˜๋Š” ์ด์ค‘ ์“ฐ๊ธฐ (Dual-write) ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ–ˆ๋‹ค.

  • ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ๋™์ผํ•œ ํŠธ๋žœ์žญ์…˜ ๋‚ด์—์„œ Outbox ํ…Œ์ด๋ธ”์— ๋ฉ”์‹œ์ง€๋ฅผ PENDING ์ƒํƒœ๋กœ ์ €์žฅ
    • ์ดํ›„ ๋ณ„๋„์˜ Worker๊ฐ€ PENDING ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ์ฝ์–ด Kafka๋กœ ๋ฐœํ–‰ํ•˜์—ฌ ๋ฐ์ดํ„ฐ ์ €์žฅ๊ณผ ๋ฉ”์‹œ์ง€ ์ „์†ก ๊ตฌ๊ฐ„์„ ๋ถ„๋ฆฌ
  • ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰ ์‹คํŒจ ์‹œ ์žฌ์‹œ๋„ํ•˜๋Š” ๋กœ์ง (At-least-once)๊ณผ ์ˆ˜์‹  ์ธก์—์„œ ์ค‘๋ณต ๋ฉ”์‹œ์ง€๋ฅผ ๊ฑธ๋Ÿฌ๋‚ด๋Š” ๋กœ์ง (๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ)์„ ์กฐํ•ฉ
    • ๊ฒฐ๊ณผ์ ์œผ๋กœ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ๋‹จ ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋˜๋„๋ก (Exactly-once) ๊ตฌ์„ฑ
sequenceDiagram
    participant App as Application
    participant DB as RDB (๋„๋ฉ”์ธ + outbox)
    participant W as ๋ฐœํ–‰ Worker
    participant K as Kafka
    participant Sub as Consumer
    App ->> DB: ๋„๋ฉ”์ธ ๋ณ€๊ฒฝ + outbox INSERT
    Note over App,DB: ํ•œ TX commit โ€” dual-write ๋ฐฉ์–ด
    W ->> DB: PENDING outbox ํ”ฝ์—…
    W ->> K: send (์‹คํŒจ ์‹œ retry)
    Note over W,K: at-least-once
    K ->> Sub: deliver (์ค‘๋ณต ๊ฐ€๋Šฅ)
    Sub ->> Sub: dedupe ๊ฒ€์‚ฌ (RDB)
    Note over Sub: ๋ฉฑ๋“ฑ ์†Œ๋น„ โ€” ์ค‘๋ณต ์ฐจ๋‹จ
Loading

Outbox ๋ชจ๋ธ

๋ชจ๋ธ ์œ„์น˜ ํŠน์ง•
payment_outbox payment-service 4์ƒํƒœ (PENDING / IN_FLIGHT / DONE / FAILED, FAILED ๋Š” ํ˜„์žฌ ๋„๋‹ฌ ๋ถˆ๊ฐ€) + ์„ ์  ๋ฐฉ์‹
pg_outbox pg-service processedAt + availableAt + self-loop retry

๋ฉฑ๋“ฑ ์†Œ๋น„ โ€” ์„œ๋น„์Šค dedupe

๊ฐ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค์˜ ๋„๋ฉ”์ธ ํŠน์„ฑ์— ๋งž์ถฐ ์„œ๋กœ ๋‹ค๋ฅธ ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ ๋ฐฉ์‹์„ ์ ์šฉํ–ˆ๋‹ค.

์„œ๋น„์Šค ์ €์žฅ์†Œ ํŒจํ„ด
payment RDB payment_event_dedupe Kafka EOS + RDB ๋ฉฑ๋“ฑ INSERT
pg RDB pg_inbox order_id UNIQUE + ์ƒํƒœ ์กฐ๊ฑด๋ถ€ ์„ ์ 
product RDB stock_commit_dedupe ์žฌ๊ณ  ์ฐจ๊ฐ๊ณผ ํ•œ TX commit

์ƒํƒœ ๋จธ์‹  ํ๋ฆ„, ๋™์‹œ์„ฑ ์ œ์–ด๋ฅผ ์œ„ํ•œ ๋ฐ์ดํ„ฐ ์„ ์  ๋ฐฉ์‹, ์บ์‹œ TTL ์„ค์ • ๊ธฐ์ค€ ๋“ฑ ์ƒ์„ธํ•œ ๊ตฌํ˜„ ๊ณผ์ •์€ ์•„๋ž˜ ๋ฌธ์„œ์— ์ •๋ฆฌํ–ˆ๋‹ค.

image

๊ฒฐ์ œ ์š”์ฒญ ํ•œ ๊ฑด์ด ์—ฌ๋Ÿฌ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค์™€ Kafka, ์™ธ๋ถ€ PG์‚ฌ๋ฅผ ๊ฑฐ์น˜๋Š” ๋™์•ˆ ์ „์ฒด ํ๋ฆ„์„ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋‹จ์ผ traceId๋ฅผ ์ „ํŒŒํ•œ๋‹ค.

  • ๋‹จ์ผ ์Šค๋ ˆ๋“œ ๋‚ด์—์„œ ๋ถ„์‚ฐ ์ถ”์  (OTel)๊ณผ ๋กœ๊ทธ (MDC) ์‹œ์Šคํ…œ์ด ๊ฐ๊ฐ์˜ traceId๋ฅผ ๋ณ„๋„๋กœ ๊ด€๋ฆฌ
  • Tempo (ํŠธ๋ ˆ์ด์Šค)์™€ Loki (๋กœ๊ทธ) ์ธํ”„๋ผ์—์„œ ๋™์ผํ•œ traceId๋กœ ์ „์ฒด ํ๋ฆ„์„ ์—ฐ๊ฒฐํ•˜์—ฌ ์ถ”์  ๊ฐ€๋Šฅ
flowchart LR
    subgraph Th1["Kafka consumer ์Šค๋ ˆ๋“œ"]
        OT1[OTel Context entry<br/>traceparent: abc...]
        MD1[MDC entry<br/>traceid: abc...]
    end

    subgraph Th2["๊ฐ€์ƒ ์Šค๋ ˆ๋“œ Worker"]
        OT2["OTel Context entry<br/>๋น„์–ด์žˆ์Œ -> ๋ช…์‹œ ๋ณต์›"]
        MD2["MDC entry<br/>๋น„์–ด์žˆ์Œ -> ๋ช…์‹œ ๋ณต์›"]
    end

    Th1 -->|OutboxJob ์— ๋‘ ์ปจํ…์ŠคํŠธ ๋™๋ด‰<br/>Worker๊ฐ€ ์ž๊ธฐ ์Šค๋ ˆ๋“œ์— set ํ›„ ์ž๋™ ์›๋ณต| Th2
Loading
  1. ๊ธฐ์กด ์Šค๋ ˆ๋“œ์˜ ThreadLocal์—๋Š” OTel๊ณผ MDC ์ปจํ…์ŠคํŠธ๊ฐ€ ์กด์žฌ
  2. ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด ์ƒˆ๋กœ์šด ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ (Virtual Thread)๋ฅผ ์ƒ์„ฑํ•˜๋ฉด ThreadLocal์ด ๋น„์–ด์žˆ๋Š” ์ƒํƒœ๋กœ ์‹คํ–‰๋˜์–ด ์ถ”์  ํ๋ฆ„์ด ๋Š๊น€
  3. ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๋น„๋™๊ธฐ ์ž‘์—… ๊ฐ์ฒด (OutboxJob)์— ๊ธฐ์กด ์ปจํ…์ŠคํŠธ๋ฅผ ๋‹ด์•„ ์ „๋‹ฌํ•˜๊ณ , Worker ์Šค๋ ˆ๋“œ๊ฐ€ ์‹คํ–‰๋  ๋•Œ ์ด๋ฅผ ๋‹ค์‹œ ๋ณต์›ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์—ฐ์†์„ฑ์„ ์œ ์ง€
flowchart LR
    classDef auto fill: #D5F5E3,stroke: #28B463,color: #000
    classDef explicit fill: #FEF5E7,stroke: #F39C12,color: #000

    subgraph Auto["์ž๋™ wiring (Spring / Boot)"]
        A1[Servlet ์ง„์ž…]:::auto
        A2[Kafka producer / consumer<br/>observation-enabled]:::auto
        A3[OpenFeign HTTP]:::auto
    end

    subgraph Manual["๋ช…์‹œ ์บก์ฒ˜/๋ณต์› ํ•„์š”"]
        M1[๊ฐ€์ƒ ์Šค๋ ˆ๋“œ<br/>ContextAwareVirtualThreadExecutors<br/>์ด์ค‘ ๋ž˜ํ•‘]:::explicit
        M2[in-memory channel pg-service<br/>OutboxJob ๋‘ ์ปจํ…์ŠคํŠธ ๋™๋ด‰]:::explicit
    end

    A1 --> A2 --> M1
    M1 --> M2
    A2 --> A3
Loading

Spring Boot๊ฐ€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ปจํ…์ŠคํŠธ๋ฅผ ์ „ํŒŒํ•ด์ฃผ์ง€ ์•Š๋Š” ๊ตฌ๊ฐ„ (Kafka ํ”„๋กœ๋“€์„œ, ์ปค์Šคํ…€ ์ธ๋ฉ”๋ชจ๋ฆฌ ํ, ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ ๋“ฑ)์—์„œ๋Š” ์ง์ ‘ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด traceId ์œ ์‹ค์„ ๋ฐฉ์ง€ํ–ˆ๋‹ค.

์ถ”์ ์ด ๋Š๊ธธ ์ˆ˜ ์žˆ๋Š” ์ง€์  ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•
HTTP ํ†ต์‹  ๊ตฌ๊ฐ„ OpenFeign๊ณผ Spring Cloud ์ž๋™ ์„ค์ •์„ ํ†ตํ•ด ํ—ค๋” ์ „ํŒŒ
Kafka ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰ / ์†Œ๋น„ observation-enabled: true ์„ค์ •์„ ์ถ”๊ฐ€ํ•ด Kafka ํ—ค๋”์— ์ถ”์  ์ •๋ณด ์ž๋™ ์ฃผ์ž…
๊ฐ€์ƒ ์Šค๋ ˆ๋“œ ์ „ํ™˜ ์Šค๋ ˆ๋“œ ์ „ํ™˜ ์‹œ ContextAwareVirtualThreadExecutors๋ฅผ ํ†ตํ•ด OTel / MDC ์ปจํ…์ŠคํŠธ ์ž๋™ ๋ณต์›
์ธ๋ฉ”๋ชจ๋ฆฌ ํ (์ฑ„๋„) ๋น„๋™๊ธฐ ๋ฉ”์‹œ์ง€(OutboxJob) ๋‚ด๋ถ€์— ์ถ”์  ์ปจํ…์ŠคํŠธ๋ฅผ ๋™๋ด‰ํ•˜๊ณ , Worker๊ฐ€ ์ž‘์—…์„ ๊บผ๋‚ผ ๋•Œ ์Šค๋ ˆ๋“œ์— ์„ธํŒ…ํ•œ ๋’ค ์ž‘์—…์ด ๋๋‚˜๋ฉด ์ดˆ๊ธฐํ™”ํ•˜๋„๋ก ๊ตฌํ˜„

  • ์›์ธ ํ™•์ธ: UNIQUE ์ฃผ๋ฌธ ๋ฒˆํ˜ธ๋กœ ํ•œ ํ–‰์„ ์ง€์ •ํ•ด๋„ Index Merge๊ฐ€ ์ƒํƒœ ์ธ๋ฑ์Šค๋ฅผ ํ•จ๊ป˜ ์ฝ์œผ๋ฉฐ ๋‹ค๋ฅธ ์ฃผ๋ฌธ์˜ ๋ ˆ์ฝ”๋“œ์—์„œ ์ถฉ๋Œ โ€” ์‹คํ–‰ ๊ณ„ํš๊ณผ ๋ฐ๋“œ๋ฝ ๋คํ”„๋กœ ํ™•์ธ
  • ์ธ๋ฑ์Šค ๊ต์ฒด: (status) โ†’ (status, updated_at) โ€” ์Šน์ธ UPDATE์˜ ์ƒํƒœ ์กฐ๊ฑด์„ ์œ ์ง€ํ•˜๊ณ , ์˜ค๋ž˜๋œ IN_PROGRESS ๋ณต๊ตฌ ์กฐํšŒ๋„ ์ง€์›
  • ์‹คํ–‰ ๊ณ„ํš ๊ฒ€์ฆ: ๋ณ€๊ฒฝ ํ›„ trace์—์„œ ๊ธฐ์กด ๊ต์ง‘ํ•ฉ ๋ณ‘ํ•ฉ ํ›„๋ณด๊ฐ€ ์ œ์™ธ๋˜๊ณ  ์ฃผ๋ฌธ ๋ฒˆํ˜ธ ๋‹จ๋… ๊ฒ€์ƒ‰์ด ์„ ํƒ๋œ ์‚ฌ์‹ค ํ™•์ธ
  • ํฌ์ŠคํŒ…: ์„œ๋กœ ๋‹ค๋ฅธ ์ฃผ๋ฌธ์˜ UPDATE๊ฐ€ ์ถฉ๋Œํ•œ ์ด์œ  โ€” MySQL Index Merge ๋ฐ๋“œ๋ฝ
๊ฒ€์ฆ ๋ฒ”์œ„ ๋ณ€๊ฒฝ ์ „ ๋ณ€๊ฒฝ ํ›„
์ „์ฒด ๊ฒฐ์ œ ยท ํ‘œ๋ณธ ์š”์ฒญ์˜ ์™„๋ฃŒ ์‹œ๊ฐ„ p99 73.10์ดˆ 9.28์ดˆ
์ „์ฒด ๊ฒฐ์ œ ยท ์Šน์ธ ๊ฒฐ๊ณผ ์ €์žฅ ๋ฐ๋“œ๋ฝ 163๊ฑด 0๊ฑด
๋ณ„๋„ DB ์žฌํ˜„ ยท UPDATE ์ฒซ ์‹œ๋„ ๋ฐ๋“œ๋ฝ 28 / 5,000๊ฑด 0 / 5,000๊ฑด

๐Ÿ“œ ์ด์ „ ๋‹จ๊ณ„ ์ž‘์—…

ํ”„๋กœ์ ํŠธ ์ดˆ๊ธฐ (Phase 1~5) ๋ชจ๋†€๋ฆฌ์‹ ํ™˜๊ฒฝ์—์„œ ์ง๋ฉดํ•˜๊ณ  ํ•ด๊ฒฐํ–ˆ๋˜ ๋ฌธ์ œ๋“ค๋กœ, Phase 6์—์„œ MSA๋กœ ์ „ํ™˜๋จ์— ๋”ฐ๋ผ ํ˜„์žฌ ์ฝ”๋“œ์˜ ๊ตฌํ˜„ ํ˜•ํƒœ์™€๋Š” ๋‹ค์†Œ ์ฐจ์ด๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์Œ

Phase 5 โ€” ๋ชจ๋†€๋ฆฌ์Šค ๋‹จ์ผ JVM ์‹œ์  ์ธก์ • (ํ˜„์žฌ ์‹œ์Šคํ…œ์˜ ๋ฉ”์‹œ์ง€ ํ๋ฆ„์€ ์œ„ "MSA + Kafka ์–‘๋ฐฉํ–ฅ" ํ•ญ๋ชฉ ์ฐธ๊ณ )

  • ๊ธฐ์กด ๋™๊ธฐ ์ฒ˜๋ฆฌ ๋ฐฉ์‹์—์„œ๋Š” PG์‚ฌ (Toss) ๊ฒฐ์ œ API ์‘๋‹ต์ด ์ง€์—ฐ๋  ๊ฒฝ์šฐ, HTTP ์Šค๋ ˆ๋“œ๊ฐ€ ๋ธ”๋กœํ‚นํ•ด TPS๊ฐ€ ๊ธ‰๋ฝํ•˜๊ณ  ์Šค๋ ˆ๋“œ ํ’€์ด ๊ณ ๊ฐˆ๋˜๋Š” ๋ฌธ์ œ ๋ฐœ์ƒ
  • ์™ธ๋ถ€ API ํ˜ธ์ถœ์„ ๋‚ด๋ถ€ ๋ฉ”๋ชจ๋ฆฌ ํ์™€ ๊ฐ€์ƒ ์Šค๋ ˆ๋“œ (Virtual Thread)๋ฅผ ํ™œ์šฉํ•œ ๋น„๋™๊ธฐ Worker ๊ตฌ์กฐ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๋„คํŠธ์›Œํฌ ์ง€์—ฐ์œผ๋กœ ์ธํ•œ ๋ณ‘๋ชฉ ํ•ด์†Œ
  • ํฌ์ŠคํŒ…: ๋น„๋™๊ธฐ ๊ฒฐ์ œ ์ฒ˜๋ฆฌ ํ”Œ๋กœ์šฐ ๊ตฌํ˜„ โ€” Outbox ํŒจํ„ด๋ถ€ํ„ฐ LinkedBlockingQueue Worker๊นŒ์ง€
flowchart TD
    classDef client fill: #FFFFFF,stroke: #333,color: #000
    classDef process fill: #E1F5FF,stroke: #0078D4,color: #000
    classDef tx fill: #FFF2CC,stroke: #D79B00,color: #000
    classDef worker fill: #E8F5E9,stroke: #2E7D32,color: #000
    classDef fallback fill: #FADAD8,stroke: #B85450,color: #000
    classDef response fill: #F5F5F5,stroke: #333,color: #000
    Client(["ํด๋ผ์ด์–ธํŠธ"]):::client

    subgraph Sync["๋™๊ธฐ ์ „๋žต"]
        S1["๊ฒฐ์ œ ์Šน์ธ ์š”์ฒญ"]:::process
        S2["์žฌ๊ณ  ์ฐจ๊ฐ + ๊ฒฐ์ œ ๊ธฐ๋ก ์ƒ์„ฑ"]:::tx
        S3["๋Œ€๊ธฐ -> ์ง„ํ–‰ ์ค‘"]:::tx
        S4["PG ์Šน์ธ ์š”์ฒญ (๋™๊ธฐ)\nโณ 100ms ~ 3,500ms ๋ธ”๋กœํ‚น"]:::process
        S5["๊ฒฐ์ œ ์™„๋ฃŒ ์ฒ˜๋ฆฌ"]:::tx
        S6(["200 OK"]):::response
        S1 --> S2 --> S3 --> S4 --> S5 --> S6
    end

    subgraph Outbox["๋น„๋™๊ธฐ ์ „๋žต (๊ธฐ๋ณธ๊ฐ’)"]
        O1["์Šน์ธ ์š”์ฒญ"]:::process
        O2["๋‹จ์ผ TX: ์ƒํƒœ ์ „ํ™˜ + ์žฌ๊ณ  ์ฐจ๊ฐ\n+ ์ฒ˜๋ฆฌ ๋Œ€๊ธฐ์—ด ๋“ฑ๋ก"]:::tx
        O3(["202 Accepted\nโ† HTTP ์Šค๋ ˆ๋“œ ์ฆ‰์‹œ ํ•ด๋ฐฉ"]):::response
        O4["์ปค๋ฐ‹ ํ›„ ์ด๋ฒคํŠธ ๋ฐœํ–‰"]:::process
        O5["์ฒ˜๋ฆฌ ํ์— ๋“ฑ๋ก\n(๋น„๋ธ”๋กœํ‚น)"]:::process

        subgraph Workers["์‹ค์‹œ๊ฐ„ Worker"]
            W1["ํ์—์„œ ๊ฒฐ์ œ ๊ฑด ์ˆ˜์‹ \n(๋Œ€๊ธฐ)"]:::worker
            W2["์ฒ˜๋ฆฌ ์„ ์ \n(์›์ž์ )"]:::tx
            W3["PG ์Šน์ธ ์š”์ฒญ\n(HTTP ์Šค๋ ˆ๋“œ์™€ ๋ถ„๋ฆฌ)"]:::worker
            W4["๊ฒฐ์ œ ์™„๋ฃŒ +\n๋Œ€๊ธฐ์—ด ์ข…๊ฒฐ"]:::tx
        end

        OFB["ํด๋ง ํด๋ฐฑ\nํ ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ /\n์„œ๋ฒ„ ์žฌ์‹œ์ž‘ ๋ณต๊ตฌ"]:::fallback
        O1 --> O2 --> O3
        O2 --> O4 --> O5
        O5 -->|" ๋“ฑ๋ก ์„ฑ๊ณต "| W1
        O5 -->|" ํ ๊ฐ€๋“ ์ฐธ "| OFB
        W1 --> W2 --> W3 --> W4
    end

    Client --> S1
    Client --> O1
    linkStyle 7,12,13 stroke: #333,stroke-width: 2px,color: #000
Loading
k6 ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ (๋ชจ๋†€๋ฆฌ์Šค ์‹œ์  ์ธก์ •)
๋„คํŠธ์›Œํฌ ์ง€์—ฐ ํ™˜๊ฒฝ ์ „๋žต TPS Confirm ์‘๋‹ต (med) E2E Latency (med) ์š”์ฒญ ์œ ์‹ค
๊ณ ์ง€์—ฐ (2.0~3.5s) Sync 54.1 6,157ms 3,190ms 1,945
๊ณ ์ง€์—ฐ (2.0~3.5s) Async 79.8 (+47%) 5.3ms 2,820ms 0 (-100%)
์ €์ง€์—ฐ (0.1~0.3s) Sync 106.4 210ms 211ms 0
์ €์ง€์—ฐ (0.1~0.3s) Async 93.5 6.3ms 305ms 0
  • ๋„คํŠธ์›Œํฌ ์ง€์—ฐ์ด ์‹ฌํ•œ ํ™˜๊ฒฝ์—์„œ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ์ „๋žต์„ ์ ์šฉํ•œ ๊ฒฐ๊ณผ TPS๊ฐ€ 47% ํ–ฅ์ƒ / ์š”์ฒญ ์œ ์‹ค 0%๋กœ ๊ฐœ์„ 
  • ์ž์› ์ตœ์ ํ™”: ํŠธ๋ž˜ํ”ฝ์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ปค๋„ฅ์…˜ ํ’€์„ ๋ฌด์ž‘์ • ๋Š˜๋ฆฌ์ง€ ์•Š๊ณ , ์‹œ์Šคํ…œ์ด ๊ฐ๋‹นํ•  ์ˆ˜ ์žˆ๋Š” ์ตœ์ ์˜ ์ˆ˜์น˜ (์˜ˆ: HikariCP 30)๋ฅผ ์ฐพ์•„ ์•ˆ์ •์ ์ธ ์ฒ˜๋ฆฌ๋Ÿ‰ ํ™•๋ณด
  • ์ƒ์„ธ ๋ณด๊ณ ์„œ: Benchmark-Report

Phase 5 โ€” ๋ณธ๋ฌธ๊ณผ ๋‹ค์ด์–ด๊ทธ๋žจ์€ ๋ณต๊ตฌ ํŒ์ • ๊ฐ์ฒด + ๊ฒฉ๋ฆฌ ์ „ ์ตœ์ข… ํ™•์ธ + ์ด์ค‘ ์กฐ๊ฑด ๋ณด์ƒ ๊ฐ€๋“œ๊ฐ€ ์žˆ๋˜ ์‹œ์ ์˜ ์Šค๋ƒ…์ƒท
Phase 6 ์—์„œ PG ์ƒํƒœ ์กฐํšŒ ๊ฒฝ๊ณ„๊ฐ€ pg-service HTTP ํ˜ธ์ถœ๋กœ ์ด๋™ + ๋ณต๊ตฌ ํŒ์ • ๊ฐ์ฒดยท์ด์ค‘ ์กฐ๊ฑด ๋ณด์ƒ ๊ฐ€๋“œ๋Š” ์‚ญ์ œ๋˜๊ณ  ๋ณ„๋„ ํ•ธ๋“ค๋Ÿฌ์˜ ๋‹จ์ผ ์ข…๊ฒฐ ์ฒดํฌ๋กœ ๋Œ€์ฒด
Phase 6 ์—์„œ ์ƒํƒœ๊ฐ€ ํ•˜๋‚˜ ๋Š˜์—ˆ๋‹ค โ€” ๋ณต๊ตฌ ์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ ๋˜๋Œ๋ฆฌ๋Š” ์ž๋ฆฌ๊ฐ€ ๋Œ€๊ธฐ์—์„œ ๊ฒฐ๊ณผ ๋Œ€๊ธฐ๋กœ ๋ฐ”๋€Œ์–ด ๋’ค๋Šฆ๊ฒŒ ๋„์ฐฉํ•œ ์Šน์ธ์„ ๊ทธ๋Œ€๋กœ ์ ์šฉํ•œ๋‹ค (๊ฒฐ์ œ ๋ณต๊ตฌ ๋ชจ๋ธ)

  • PG ์ƒํƒœ ์กฐํšŒ ํ›„ ๋ณต๊ตฌ ํŒ์ • ๊ฐ์ฒด๊ฐ€ ์ข…๊ฒฐ/์žฌ์‹œ๋„/๊ฒฉ๋ฆฌ๋ฅผ ๊ฒฐ์ •
  • ์žฌ์‹œ๋„ ํ•œ๋„ ์†Œ์ง„ ์‹œ ๊ฒฉ๋ฆฌ ์ „ ์ตœ์ข… ํ™•์ธ (PG ์ƒํƒœ 1ํšŒ ์žฌ์กฐํšŒ)์œผ๋กœ ์„ฑ๊ณต ๊ฑด์˜ ์˜ค๊ฒฉ๋ฆฌ ๋ฐฉ์ง€, ๊ฒฉ๋ฆฌ ์ƒํƒœ๋กœ ๊ด€๋ฆฌ์ž ๊ฐœ์ž… ์œ ๋„
  • ๋ณด์ƒ TX ์‹คํ–‰ ์ง์ „ ์ด์ค‘ ์กฐ๊ฑด ๊ฐ€๋“œ (๋Œ€๊ธฐ์—ด ์„ ์  ์ค‘ + ๊ฒฐ์ œ ๋น„์ข…๊ฒฐ)๋กœ ๋™์‹œ์„ฑ ๊ฒฝํ•ฉ ์‹œ ์žฌ๊ณ  ์ด์ค‘ ๋ณต๊ตฌ ์ฐจ๋‹จ
  • ํฌ์ŠคํŒ…: ๊ฒฐ์ œ ๋ณต๊ตฌ ์ƒํƒœ ์ „์ด ์„ค๊ณ„
flowchart TD
    classDef success fill: #D5F5E3,color: #0E6251,stroke: #28B463
    classDef retryable fill: #FEF5E7,color: #7E5109,stroke: #F39C12
    classDef failure fill: #FADBD8,color: #7B241C,stroke: #E74C3C
    classDef action fill: #EBF5FB,color: #21618C,stroke: #3498DB
    classDef quarantine fill: #F3E5F5,color: #4A148C,stroke: #7B1FA2
    classDef check fill: #FEF9E7,color: #7D6608,stroke: #F1C40F
    classDef skip fill: #F5F5F5,color: #616161,stroke: #9E9E9E
    CL["์ฒ˜๋ฆฌ ์„ ์ \n(์›์ž์ )"]:::action
    CL -->|" ์„ ์  ์„ฑ๊ณต "| GS["PG ์ƒํƒœ ์กฐํšŒ"]:::action
    CL -->|" ์„ ์  ์‹คํŒจ "| SKIP["๋‹ค๋ฅธ Worker๊ฐ€ ์ฒ˜๋ฆฌ ์ค‘\n-> ํฌ๊ธฐ"]:::skip
    GS -->|" ์Šน์ธ ์™„๋ฃŒ "| SUCCESS["๊ฒฐ์ œ ์„ฑ๊ณต ํ™•์ •"]:::success
    GS -->|" PG ์ข…๊ฒฐ ์‹คํŒจ "| FAILURE["๊ฒฐ์ œ ์‹คํŒจ ํ™•์ •"]:::failure
    GS -->|" PG ๊ธฐ๋ก ์—†์Œ "| CONFIRM["PG ์Šน์ธ ์žฌ์‹œ๋„"]:::action
    GS -->|" ์ผ์‹œ ์˜ค๋ฅ˜ + ํ•œ๋„ ๋ฏธ์†Œ์ง„ "| RETRY["์žฌ์‹œ๋„ ๋Œ€๊ธฐ\n(๋ฐฑ์˜คํ”„)"]:::retryable
    GS -->|" ํ•œ๋„ ์†Œ์ง„ "| FINAL["๊ฒฉ๋ฆฌ ์ „ ์ตœ์ข… ํ™•์ธ\n(PG ์ƒํƒœ 1ํšŒ ์žฌ์กฐํšŒ)"]:::action
    FINAL -->|" ์Šน์ธ ์™„๋ฃŒ "| SUCCESS
    FINAL -->|" PG ์ข…๊ฒฐ ์‹คํŒจ "| FAILURE
    FINAL -->|" ํŒ๋‹จ ๋ถˆ๊ฐ€ "| QU["๊ฒฉ๋ฆฌ\n๊ด€๋ฆฌ์ž ๊ฐœ์ž… ๋Œ€๊ธฐ"]:::quarantine
    FAILURE --> GUARD{"์žฌ๊ณ  ๋ณต๊ตฌ ๊ฐ€๋“œ\n๋Œ€๊ธฐ์—ด ์„ ์  ์ค‘?\n๊ฒฐ์ œ ๋น„์ข…๊ฒฐ?"}:::check
    GUARD -->|" ์กฐ๊ฑด ์ถฉ์กฑ "| COMP["์žฌ๊ณ  ๋ณต๊ตฌ ํ›„ ์‹คํŒจ ์ฒ˜๋ฆฌ"]:::failure
    GUARD -->|" ์กฐ๊ฑด ๋ฏธ์ถฉ์กฑ "| GSKIP["์žฌ๊ณ  ๋ณต๊ตฌ ์ƒ๋žต"]:::skip
Loading

Phase 4 โ€” ๋ณธ๋ฌธ์€ Caffeine ๋กœ์ปฌ ์บ์‹œ ์‹œ์ 
Phase 6 ์—์„œ Redis ๋ถ„์‚ฐ store (IdempotencyStoreRedisAdapter) ๋กœ ์–ด๋Œ‘ํ„ฐ ๊ต์ฒด

  • UI ์ค‘๋ณต ํด๋ฆญ, ๋„คํŠธ์›Œํฌ ์žฌ์‹œ๋„ ๋“ฑ์œผ๋กœ ๊ฒฐ์ œ ๊ฑด์ด ๋ณต์ˆ˜ ์ƒ์„ฑ๋˜์–ด DB์— ์œ ํšจํ•˜์ง€ ์•Š์€ ์ฃผ๋ฌธ์ด ๋ˆ„์ ๋˜๋Š” ๋ฌธ์ œ ์กด์žฌ
  • ์ดˆ๊ธฐ ์กฐํšŒ ํ›„ ์ƒ์„ฑ ๋ฐฉ์‹์—์„œ ์ฝ”๋“œ ๋ฆฌ๋ทฐ ์ค‘ TOCTOU ๊ฒฝ์Ÿ ์กฐ๊ฑด ๋ฐœ๊ฒฌ
    • ๋‹จ์ผ ์›์ž์  ์กฐํšŒยท์ƒ์„ฑ ๋ฉ”์„œ๋“œ๋กœ ํฌํŠธ ๊ณ„์•ฝ ์žฌ์„ค๊ณ„
  • ๋ชจ๋†€๋ฆฌ์Šค โ†’ MSA ๋ถ„๋ฆฌ ์‹œ์ ์— Caffeine ๋กœ์ปฌ ์บ์‹œ โ†’ Redis ๋ถ„์‚ฐ ์ €์žฅ์†Œ ๋กœ ์–ด๋Œ‘ํ„ฐ ๊ต์ฒด โ€” ๋‹จ์ผ ์ธ์Šคํ„ด์Šค โ†’ ๋‹ค์ค‘ ์ธ์Šคํ„ด์Šค ๋ฉฑ๋“ฑ ๋ณด์žฅ
  • ํฌ์ŠคํŒ…: Checkout API ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ โ€” Caffeine ์บ์‹œ์™€ TOCTOU ๊ฒฝ์Ÿ ์กฐ๊ฑด ํ•ด๊ฒฐ
sequenceDiagram
    participant A as Thread A
    participant B as Thread B
    participant Cache as Caffeine Cache
    A ->> Cache: ์กฐํšŒ ์š”์ฒญ ("key")
    Cache -->> A: (๋ฝ ํš๋“, ์ƒ์„ฑ ๋กœ์ง ์‹คํ–‰ ์ค‘)
    A ->> A: ๊ฒฐ์ œ ๊ฑด#1 ์ƒ์„ฑ
    B ->> Cache: ์กฐํšŒ ์š”์ฒญ ("key")
    Cache -->> B: (๋™์ผ ํ‚ค -> ๋ฝ ๋Œ€๊ธฐ)
    A ->> Cache: ๊ฒฐ๊ณผ ์ €์žฅ ํ›„ ๋ฝ ํ•ด์ œ
    Cache -->> B: ์บ์‹œ ์ ์ค‘ -> ๊ฒฐ์ œ ๊ฑด#1 ๋ฐ˜ํ™˜ (์ƒ์„ฑ ๋กœ์ง ๋ฏธ์‹คํ–‰)
    Note over Cache: โœ… ์ค‘๋ณต ์ƒ์„ฑ ์—†์Œ
Loading

๋ชจ๋†€๋ฆฌ์Šค ์‹œ์  โ€” PaymentGatewayFactory / InternalReceiver ๋„์‹
Phase 6์—์„œ๋Š” PG-Service์—์„œ ์‚ฌ์šฉ ์ค‘

  • Application ๊ณ„์ธต์€ PaymentGatewayPort ์ธํ„ฐํŽ˜์ด์Šค์—๋งŒ ์˜์กดํ•˜์—ฌ PG ๋…๋ฆฝ์„ฑ ํ™•๋ณด
  • ์ „๋žต ํŒจํ„ด์œผ๋กœ Toss/NicePay ๋‘ PG์‚ฌ๋ฅผ ๋™์‹œ ์ง€์›ํ•˜๋ฉฐ, ๊ฒฐ์ œ๊ฑด๋งˆ๋‹ค gatewayType์œผ๋กœ ์˜ฌ๋ฐ”๋ฅธ PG ๋ผ์šฐํŒ…
  • ๋ฉฑ๋“ฑ์„ฑ ํ‚ค๋ฅผ ๊ธฐ๋ณธ์œผ๋กœ ์ง€์›ํ•˜๋Š” ํ† ์Šค์™€ ๋‹ฌ๋ฆฌ ๋‚˜์ด์ŠคํŽ˜์ด๋จผ์ธ ๋Š” ํ•ด๋‹น ๊ธฐ๋Šฅ์ด ์—†์–ด, ์ค‘๋ณต ๊ฒฐ์ œ ์‹œ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ ์ง์ ‘ ๊ฐ์ง€ ๋ฐ ๋ณด์ƒ ํŒจํ„ด์„ ๊ตฌํ˜„
  • ํฌ์ŠคํŒ…: ์ „๋žต ํŒจํ„ด์„ ํ†ตํ•œ ๊ฒฐ์ œ ๊ฒŒ์ดํŠธ์›จ์ด ์ถ”์ƒํ™” ๋ฐ ํ™•์žฅ์„ฑ ํ™•๋ณด
graph TB
    subgraph "Application Layer"
        UseCase[๊ฒฐ์ œ ์ฒ˜๋ฆฌ ์œ ์Šค์ผ€์ด์Šค]
        Port[PG ์—ฐ๋™ ํฌํŠธ<br/>Interface]
    end

    subgraph "Infrastructure Layer"
        Adapter[PG ์—ฐ๋™ ์–ด๋Œ‘ํ„ฐ<br/>ํฌํŠธ ๊ตฌํ˜„์ฒด]
        Factory[PG ์ „๋žต ์„ ํƒ๊ธฐ]
        Strategy[PG ์ „๋žต<br/>Interface]

        subgraph "Strategy Implementations"
            Toss[Toss ์ „๋žต]
            Nicepay[NicePay ์ „๋žต]
        end
    end

    subgraph "External Systems"
        TossAPI[Toss Payments API]
        NicepayAPI[NicePay API]
    end

    UseCase -->|์˜์กด| Port
    Port -.->|๊ตฌํ˜„| Adapter
    Adapter -->|์œ„์ž„| Factory
    Factory -->|์„ ํƒ| Strategy
    Strategy -.->|๊ตฌํ˜„| Toss
    Strategy -.->|๊ตฌํ˜„| Nicepay
    Toss -->|ํ˜ธ์ถœ| TossAPI
    Nicepay -->|ํ˜ธ์ถœ| NicepayAPI
    style Port fill: #e1f5ff,color: #000
    style Strategy fill: #e1f5ff,color: #000
    style Adapter fill: #fff4e1,color: #000
    style Factory fill: #fff4e1,color: #000
    style Toss fill: #e8f5e9,color: #000
    style Nicepay fill: #e8f5e9,color: #000
Loading

Phase 3 โ€” Micrometer ํŒจํ„ด์€ ์œ ์ง€
Phase 6 ์—์„œ ํŒจํ‚ค์ง€๊ฐ€ ๋ชจ๋†€๋ฆฌ์Šค core.common.metrics โ†’ ์„œ๋น„์Šค๋ณ„ ๋ถ„์‚ฐ

  • ์Šน์ธ ์ง€์—ฐ, ์žฌ์‹œ๋„ ๋“ฑ ๋ณต์žกํ•œ ๊ฒฐ์ œ ํ๋ฆ„ ์ถ”์ ์˜ ์–ด๋ ค์›€ ๋ฐ ์‹ค์‹œ๊ฐ„ ์„ฑ๋Šฅ/์ด์ƒ ์ง•ํ›„๋ฅผ ํŒŒ์•…ํ•  ํ•ต์‹ฌ ์ง€ํ‘œ ๋ถ€์žฌ
  • ๊ตฌ์กฐํ™”๋œ ๋กœ๊น… ์ ์šฉ / ๊ฒฐ์ œ ์ •๋ณด ๋ณ€๋™ ์ €์žฅ ๋ฐ ์–ด๋“œ๋ฏผ ํŽ˜์ด์ง€ ๊ตฌํ˜„ / ์ปค์Šคํ…€ ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘์„ ํ†ตํ•œ ํ•ต์‹ฌ ์ง€ํ‘œ ๋ชจ๋‹ˆํ„ฐ๋ง ์ฒด๊ณ„ ๊ตฌ์ถ•
image image

Phase 1 โ€” ํ๋ฆ„ ์ž์ฒด๋Š” ์œ ์ง€
Phase 6 ์—์„œ ํ˜ธ์ถœ ๊ฒฝ๊ณ„๊ฐ€ ๊ฐ™์€ ์ธ์Šคํ„ด์Šค ์•ˆ ํ˜ธ์ถœ โ†’ HTTP / Kafka ๋กœ ์ด๋™

  • ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ฃผ๋ฌธ ์ƒ์„ฑ๋ถ€ํ„ฐ ์Šน์ธ๊นŒ์ง€ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ, ์ค‘๊ฐ„ ๊ฐ’ ์กฐ์ž‘ ๊ฐ™์€ ์œ„๋ณ€์กฐ ๊ฐ€๋Šฅ์„ฑ ์กด์žฌ
  • ์„œ๋ฒ„ ์ฃผ๋„์˜ ํ๋ฆ„์œผ๋กœ ์ „ํ™˜ํ•˜๊ณ , ํด๋ผ์ด์–ธํŠธยท์„œ๋ฒ„ยทPG ์‘๋‹ต๊ฐ’์„ ๊ต์ฐจ ๊ฒ€์ฆํ•˜์—ฌ ๋ถˆ์ผ์น˜ ์‹œ ๊ฒฐ์ œ๋ฅผ ๊ฑฐ๋ถ€ํ•˜๋„๋ก ์„ค๊ณ„
sequenceDiagram
    autonumber
    participant C as Client
    participant S as Server
    participant T as PG
    Note over C,T: ๊ฒฐ์ œ ์‹œํ€€์Šค ํ๋ฆ„
    C ->> S: ์ฃผ๋ฌธ ๋ฒˆํ˜ธ ์ƒ์„ฑ ์š”์ฒญ
    S ->> S: ๊ตฌ๋งค ์š”์ฒญ ๊ฒ€์ฆ ๋ฐ DB ์ €์žฅ
    S -->> C: ์ฃผ๋ฌธ ๋ฒˆํ˜ธ ๋ฐ˜ํ™˜
    C ->> T: ๊ฒฐ์ œ ์š”์ฒญ
    T ->> T: ์นด๋“œ์‚ฌ ๊ฒฐ์ œ ์ธ์ฆ
    T -->> C: ์„ฑ๊ณต ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
    C ->> S: ๊ฒฐ์ œ ์Šน์ธ ์š”์ฒญ
    S ->> S: ๊ฒฐ์ œ / ์ฃผ๋ฌธ ์ •๋ณด ์–‘๋ฐฉํ–ฅ ๊ฒ€์ฆ
    S ->> T: ๊ฒฐ์ œ ์Šน์ธ
    T -->> S: ์Šน์ธ ์„ฑ๊ณต ๋ฐ˜ํ™˜
    S ->> S: DB ์—…๋ฐ์ดํŠธ
    S -->> C: ์„ฑ๊ณต ๋‚ด์—ญ ๋ฐ˜ํ™˜
    C ->> C: ๊ฒฐ์ œ์™„๋ฃŒ
Loading

Phase 2 โ€” TX ๋ถ„๋ฆฌ + ๋ณด์ƒ ํŒจํ„ด์€ ์œ ์ง€ Phase 6 ์—์„œ ์™ธ๋ถ€ ํ˜ธ์ถœ ๊ฒฝ๊ณ„๊ฐ€ payment-service โ†” pg-service Kafka ์–‘๋ฐฉํ–ฅ์œผ๋กœ ์ด๋™ํ•˜์—ฌ ํŠธ๋žœ์žญ์…˜ ์—†์Œ

image

Phase 3 โ€” Fake ํŒจํ„ด์€ ์œ ์ง€ ์œ„ํ‚ค ๋ณธ๋ฌธ ํด๋ž˜์Šค๋ช… (FakeTossHttpOperator ๋“ฑ)์€ ๋ชจ๋†€๋ฆฌ์Šค ์‹œ์ ์ด๋ฉฐ ํ˜„์žฌ๋Š” pg-service FakePgGatewayStrategy ๋“ฑ์œผ๋กœ ์žฌ๊ตฌ์„ฑ

  • ๊ฒฐ์ œ ๋กœ์ง์ด ์™ธ๋ถ€ PG์‚ฌ ์‹œ์Šคํ…œ์— ๊ฐ•ํ•˜๊ฒŒ ๊ฒฐํ•ฉ๋˜์–ด ์žˆ์–ด ํƒ€์ž„์•„์›ƒ, 500 ๋‚ด๋ถ€ ์—๋Ÿฌ ๋“ฑ ๋‹ค์–‘ํ•œ ์žฅ์•  ์ƒํ™ฉ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ ์–ด๋ ค์›€ ์กด์žฌ
  • ์‹ค์ œ ๋„คํŠธ์›Œํฌ ํ†ต์‹  ๋Œ€์‹  ๋™์ž‘์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋Š” Fake ๊ฐ์ฒด๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๊ตฌ์„ฑ
    • ๊ฒฐ๊ณผ์ ์œผ๋กœ ์‘๋‹ต ์ง€์—ฐ, ์Šน์ธ ๊ฑฐ์ ˆ, ์ค‘๋ณต ๊ฒฐ์ œ ์‹œ๋„์™€ ๊ฐ™์€ ๋‹ค์–‘ํ•œ ์—ฃ์ง€ ์ผ€์ด์Šค ๊ฒ€์ฆ
  • ํฌ์ŠคํŒ…: ์™ธ๋ถ€ ์˜์กด์„ฑ ์ œ์–ด๋ฅผ ํ†ตํ•œ ๊ฒฐ์ œ ํ”„๋กœ์„ธ์Šค ๋‹ค์–‘ํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค ๊ฒ€์ฆ
image

๐Ÿ›  ์‚ฌ์šฉ ๊ธฐ์ˆ  ์Šคํƒ

  • Language / Runtime: Java 21 (Virtual Threads), Spring Boot 3.4.4, Spring Cloud 2024.0.0
  • Persistence: MySQL 8.0 ร— 4 (DB per service), Flyway, JPA + QueryDSL
  • Messaging / Cache: Kafka (KRaft, broker 1๋Œ€), Redis ร— 2 (dedupe + redis-stock ๋ถ„๋ฆฌ)
  • Service Discovery / Routing: Eureka, Spring Cloud Gateway, OpenFeign + Spring Cloud LoadBalancer
  • Observability: Micrometer + Prometheus + Grafana + Tempo + Loki, OpenTelemetry traceparent
  • Test: JUnit 5, AssertJ, Mockito, Testcontainers (MySQL / Redis), MockWebServer, ArchUnit (๋ ˆ์ด์–ด ๊ทœ์น™)
  • Static Analysis: Checkstyle (์ปค์Šคํ…€ Check ํฌํ•จ), SpotBugs, JaCoCo

๊ฐ ์„œ๋น„์Šค๋Š” ๋™์ผํ•œ hexagonal ํŒจํ‚ค์ง€ ๊ตฌ์กฐ (domain / application / presentation / infrastructure / core / exception) ์‚ฌ์šฉํ•œ๋‹ค.

  • ๋„๋ฉ”์ธ์˜ ์™ธ๋ถ€ ์ธํ”„๋ผ๋กœ๋ถ€ํ„ฐ ๊ฒฉ๋ฆฌ
  • HTTP (OpenFeign + LB) ๋˜๋Š” Kafka ๋ฉ”์‹œ์ง€๋กœ ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹ 
  • ๋ ˆ์ด์–ด ์˜์กด ๋ฐฉํ–ฅ์€ ๋ฌธ์„œ์ƒ์˜ ์•ฝ์†์ด ์•„๋‹ˆ๋ผ ArchUnit ํ…Œ์ŠคํŠธ๋กœ ๊ฐ•์ œ โ€” ๋„๋ฉ”์ธ์ด SpringยทJPA๋‚˜ ๋ฐ”๊นฅ ๋ ˆ์ด์–ด๋ฅผ ์ฐธ์กฐํ•˜๋ฉด ๋นŒ๋“œ ์‹คํŒจ

โ–ถ๏ธ Quick Start

์„œ๋น„์Šค ๊ตฌ์„ฑ

ํฌํŠธ ์„œ๋น„์Šค ์„ค๋ช…
8090 Gateway API ๊ฒŒ์ดํŠธ์›จ์ด
8761 Eureka ์„œ๋น„์Šค ๋””์Šค์ปค๋ฒ„๋ฆฌ
8081 payment-service ๊ฒฐ์ œ ์„œ๋น„์Šค
8082 pg-service PG ์Šน์ธ/์ค‘๊ณ„ ์„œ๋น„์Šค
8083 product-service ์ƒํ’ˆ ์„œ๋น„์Šค
8084 user-service ํšŒ์› ์„œ๋น„์Šค
3306 mysql-payment payment DB
3308 mysql-pg pg DB
3309 mysql-product product DB
3310 mysql-user user DB
9092 Kafka ์ด๋ฒคํŠธ ๋ธŒ๋กœ์ปค
6379 redis-dedupe dedupe ์บ์‹œ
6380 redis-stock ์žฌ๊ณ  ์บ์‹œ

์‹œํฌ๋ฆฟ ์„ค์ •

cp .env.secret.example .env.secret
# TOSS_TEST_SECRET_KEY ์ž…๋ ฅ (์˜ˆ์‹œ ํŒŒ์ผ์— ํฌํ•จ) โ€” NICEPAY_CLIENT_KEY, NICEPAY_SECRET_KEY ๋Š” ์˜ˆ์‹œ ํŒŒ์ผ์— ์—†์–ด ์ง์ ‘ ์ถ”๊ฐ€ ํ•„์š”

์‹คํ–‰ ๋ฐฉ๋ฒ•

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰

bash scripts/compose-up.sh
# ๊ธฐ๋™ ํ›„ smoke ๊ฒ€์ฆ
bash scripts/smoke-all.sh                  # ์ธํ”„๋ผ ํ—ฌ์Šค + Kafka ํ† ํ”ฝ ์ •์ฑ…
bash scripts/smoke-all.sh --with-trace     # + ํŠธ๋ ˆ์ด์Šค(๊ฒฐ์ œ 1๊ฑด ๋ฐœ์ƒ ํ›„)
# ๋˜๋Š” stack up ๊ณผ ๋™์‹œ์—:
bash scripts/compose-up.sh --with-smoke    # ๊ธฐ๋™ ์งํ›„ ์ž๋™ ์‹คํ–‰

Smoke ์Šคํฌ๋ฆฝํŠธ ๊ตฌ์„ฑ

์Šคํฌ๋ฆฝํŠธ ๊ฒ€์ฆ ํ•ญ๋ชฉ ๊ฒฐ์ œ 1๊ฑด ์„ ํ–‰ ํ•„์š”
scripts/smoke/infra-healthcheck.sh 13 ์ปจํ…Œ์ด๋„ˆ health + 9 ํ˜ธ์ŠคํŠธ ํฌํŠธ + 5 Eureka ๋“ฑ๋ก (์ด 27 ํ•ญ๋ชฉ) X
scripts/smoke/kafka-topic-config.sh ํ† ํ”ฝ partition ๋™์ผ์„ฑ / replication-factor ์ •์ฑ… / retry ํ† ํ”ฝ ๋ฏธ์กด์žฌ X
scripts/smoke/trace-header-check.sh payment.commands.confirm Kafka record header ์˜ traceparent O
scripts/smoke/trace-continuity-check.sh gateway โ†’ payment โ†’ pg โ†’ product/user ๋‹ค์ค‘ ํ™‰ traceId ์—ฐ์†์„ฑ O

๊ฒฐ์ œ 1๊ฑด ์„ ํ–‰ ํ•„์š” = O ์ธ ์Šคํฌ๋ฆฝํŠธ๋Š” ํ† ํ”ฝ์— ๋ฉ”์‹œ์ง€๊ฐ€ / ์„œ๋น„์Šค ๋กœ๊ทธ์— traceId ๊ฐ€ ๋ฐ•ํ˜€ ์žˆ์–ด์•ผ ๊ฒ€์ฆ ๋Œ€์ƒ ์กด์žฌ

์‹คํ–‰ ํ›„ http://localhost:8090 ์—์„œ ์ „์ฒด ํŽ˜์ด์ง€๋ฅผ ํƒ์ƒ‰ ๊ฐ€๋Šฅ

URL ์„ค๋ช…
/ ํ™ˆ โ€” ๊ฒฐ์ œ ํ๋ฆ„ ยท ์–ด๋“œ๋ฏผ ยท ๋ชจ๋‹ˆํ„ฐ๋ง ๋งํฌ ๋ชจ์Œ
/payment/checkout.html ๊ฒฐ์ œํ•˜๊ธฐ โ€” ํ† ์ŠคํŽ˜์ด๋จผ์ธ  ๊ฒฐ์ œ์ฐฝ ํ˜ธ์ถœ
/payment/checkout-nicepay.html ๊ฒฐ์ œํ•˜๊ธฐ โ€” ๋‚˜์ด์ŠคํŽ˜์ด๋จผ์ธ  ๊ฒฐ์ œ์ฐฝ ํ˜ธ์ถœ
/admin/payments/events ๊ฒฐ์ œ ์ด๋ฒคํŠธ ๋ชฉ๋ก ์กฐํšŒ / ๊ฒ€์ƒ‰
/admin/payments/history ๊ฒฐ์ œ ํžˆ์Šคํ† ๋ฆฌ โ€” ์ƒํƒœ ๋ณ€๊ฒฝ ์ด๋ ฅ ์กฐํšŒ

About

A payment platform built with Java and Spring

Topics

Resources

Stars

8 stars

Watchers

1 watching

Forks

Contributors

Languages