๊ฒฐ์ ์ฐ๋ ํ๊ฒฝ์์ ๋ฐ์ํ๋ ๋ฌธ์ ๋ค โ ์๋ณ์กฐ ๋ฐฉ์ง ยท ๋ฉฑ๋ฑ์ฑ ๋ณด์ฅ ยท ๋น๋๊ธฐ ๊ฒฐ์ ์ฒ๋ฆฌ ยท ์๋ ๋ณต๊ตฌ ยท ๋ถ์ฐ ํธ๋์ญ์ โ ์ ๋จ๊ณ๋ณ๋ก ์ง์ ์ค๊ณํ๊ณ ๊ตฌํํ ํ๋ก์ ํธ์ด๋ค.
โ Phase 6 ์๋ฃ
MSA ์๋น์ค + Eureka + Gateway ยท Kafka ์๋ฐฉํฅ ์ฝ๋ ์ค๊ทธ๋ํผ + Outbox ๋ชจ๋ธ + ๋ถ์ฐ ํธ๋ ์ด์ฑ ์ด์ ยท ๋จ์ 861 / ํตํฉ 59 PASS
๐ ๋ค์ ยท Phase 7
ํ๋ณต์ฑ ๊ฒ์ฆ (์ฅ์ ์ฃผ์ + k6 ์๋๋ฆฌ์ค ์ฌ์ค๊ณ + ๋ก์ปฌ ์คํ ์ค์ผ์ผ๋ฌ + ์ํท๋ธ๋ ์ด์ปค) โ ์๋ ๊ท์น + Toxiproxy ์ฅ์ ๋๋ฆด ์ธํ๋ผ๋ ์ ํ ๊ตฌ์ถ ์๋ฃ
| ํด๊ฒฐ ์์ญ | ์ฃผ์ ํด๊ฒฐ ๋ฐฉ๋ฒ | ๊ฐ์ ๊ฒฐ๊ณผ |
|---|---|---|
| ๋๊ธฐ โ ๋น๋๊ธฐ ์ ํ | 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
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
| ํ ํฝ | ๋ฐํ์ | ์๋น์ | ์๋ฏธ |
|---|---|---|---|
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: ๋ฉฑ๋ฑ ์๋น โ ์ค๋ณต ์ฐจ๋จ
| ๋ชจ๋ธ | ์์น | ํน์ง |
|---|---|---|
payment_outbox |
payment-service | 4์ํ (PENDING / IN_FLIGHT / DONE / FAILED, FAILED ๋ ํ์ฌ ๋๋ฌ ๋ถ๊ฐ) + ์ ์ ๋ฐฉ์ |
pg_outbox |
pg-service | processedAt + availableAt + self-loop retry |
๊ฐ ๋ง์ดํฌ๋ก์๋น์ค์ ๋๋ฉ์ธ ํน์ฑ์ ๋ง์ถฐ ์๋ก ๋ค๋ฅธ ๋ฉฑ๋ฑ์ฑ ๋ณด์ฅ ๋ฐฉ์์ ์ ์ฉํ๋ค.
| ์๋น์ค | ์ ์ฅ์ | ํจํด |
|---|---|---|
| payment | RDB payment_event_dedupe |
Kafka EOS + RDB ๋ฉฑ๋ฑ INSERT |
| pg | RDB pg_inbox |
order_id UNIQUE + ์ํ ์กฐ๊ฑด๋ถ ์ ์ |
| product | RDB stock_commit_dedupe |
์ฌ๊ณ ์ฐจ๊ฐ๊ณผ ํ TX commit |
์ํ ๋จธ์ ํ๋ฆ, ๋์์ฑ ์ ์ด๋ฅผ ์ํ ๋ฐ์ดํฐ ์ ์ ๋ฐฉ์, ์บ์ TTL ์ค์ ๊ธฐ์ค ๋ฑ ์์ธํ ๊ตฌํ ๊ณผ์ ์ ์๋ ๋ฌธ์์ ์ ๋ฆฌํ๋ค.
๊ฒฐ์ ์์ฒญ ํ ๊ฑด์ด ์ฌ๋ฌ ๋ง์ดํฌ๋ก์๋น์ค์ 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
- ๊ธฐ์กด ์ค๋ ๋์ ThreadLocal์๋ OTel๊ณผ MDC ์ปจํ ์คํธ๊ฐ ์กด์ฌ
- ๋น๋๊ธฐ ์ฒ๋ฆฌ๋ฅผ ์ํด ์๋ก์ด ๊ฐ์ ์ค๋ ๋ (Virtual Thread)๋ฅผ ์์ฑํ๋ฉด ThreadLocal์ด ๋น์ด์๋ ์ํ๋ก ์คํ๋์ด ์ถ์ ํ๋ฆ์ด ๋๊น
- ์ด๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด ๋น๋๊ธฐ ์์
๊ฐ์ฒด (
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
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๋ก ์ ํ๋จ์ ๋ฐ๋ผ ํ์ฌ ์ฝ๋์ ๊ตฌํ ํํ์๋ ๋ค์ ์ฐจ์ด๊ฐ ์์ ์ ์์
๋น๋๊ธฐ ๊ฒฐ์ ํ์ธ ํ๋ก์ฐ โ Outbox ์ฑ๋ ๊ธฐ๋ฐ ๋น๋๊ธฐ ์ํคํ ์ฒ ์ ํ ๋ฐ ๋ฒค์น๋งํฌ
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
| ๋คํธ์ํฌ ์ง์ฐ ํ๊ฒฝ | ์ ๋ต | 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
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: โ
์ค๋ณต ์์ฑ ์์
๋ชจ๋๋ฆฌ์ค ์์ โ
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
Phase 3 โ Micrometer ํจํด์ ์ ์ง
Phase 6 ์์ ํจํค์ง๊ฐ ๋ชจ๋๋ฆฌ์คcore.common.metricsโ ์๋น์ค๋ณ ๋ถ์ฐ
- ์น์ธ ์ง์ฐ, ์ฌ์๋ ๋ฑ ๋ณต์กํ ๊ฒฐ์ ํ๋ฆ ์ถ์ ์ ์ด๋ ค์ ๋ฐ ์ค์๊ฐ ์ฑ๋ฅ/์ด์ ์งํ๋ฅผ ํ์ ํ ํต์ฌ ์งํ ๋ถ์ฌ
- ๊ตฌ์กฐํ๋ ๋ก๊น ์ ์ฉ / ๊ฒฐ์ ์ ๋ณด ๋ณ๋ ์ ์ฅ ๋ฐ ์ด๋๋ฏผ ํ์ด์ง ๊ตฌํ / ์ปค์คํ ๋ฉํธ๋ฆญ ์์ง์ ํตํ ํต์ฌ ์งํ ๋ชจ๋ํฐ๋ง ์ฒด๊ณ ๊ตฌ์ถ
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: ๊ฒฐ์ ์๋ฃ
Phase 2 โ TX ๋ถ๋ฆฌ + ๋ณด์ ํจํด์ ์ ์ง Phase 6 ์์ ์ธ๋ถ ํธ์ถ ๊ฒฝ๊ณ๊ฐ payment-service โ pg-service Kafka ์๋ฐฉํฅ์ผ๋ก ์ด๋ํ์ฌ ํธ๋์ญ์ ์์
- ์ธ๋ถ API ํธ์ถ์ด ํฌํจ๋ ๋จ์ผ ํธ๋์ญ์ ๊ตฌ์กฐ๋ก ์ธํด ์ปค๋ฅ์ ์ ์ ์ ์๋ต ์ง์ฐ ๋ฌธ์ ๊ฐ ๋ฐ์
- ์ธ๋ถ ํธ์ถ์ ํธ๋์ญ์ ์ธ๋ถ๋ก ๋ถ๋ฆฌํ๊ณ ๋ณด์ TX ๋ฅผ ์ ์ฉํด ๋ฐ์ดํฐ ๋ถ์ผ์น ๋ฌธ์ ๋ฐ ์ฑ๋ฅ ๋ฌธ์ ํด๊ฒฐ
- ํฌ์คํ : ํธ๋์ญ์ ๋ฒ์ ์ต์ํ๋ฅผ ํตํ ์ฑ๋ฅ ๋ฐ ์์ ์ฑ ํฅ์
Phase 3 โ Fake ํจํด์ ์ ์ง ์ํค ๋ณธ๋ฌธ ํด๋์ค๋ช (
FakeTossHttpOperator๋ฑ)์ ๋ชจ๋๋ฆฌ์ค ์์ ์ด๋ฉฐ ํ์ฌ๋ pg-serviceFakePgGatewayStrategy๋ฑ์ผ๋ก ์ฌ๊ตฌ์ฑ
- ๊ฒฐ์ ๋ก์ง์ด ์ธ๋ถ PG์ฌ ์์คํ ์ ๊ฐํ๊ฒ ๊ฒฐํฉ๋์ด ์์ด ํ์์์, 500 ๋ด๋ถ ์๋ฌ ๋ฑ ๋ค์ํ ์ฅ์ ์ํฉ์ ๋ํ ํ ์คํธ ์ด๋ ค์ ์กด์ฌ
- ์ค์ ๋คํธ์ํฌ ํต์ ๋์ ๋์์ ์ ์ดํ ์ ์๋ Fake ๊ฐ์ฒด๋ฅผ ๊ตฌํํ์ฌ ํ
์คํธ ํ๊ฒฝ ๊ตฌ์ฑ
- ๊ฒฐ๊ณผ์ ์ผ๋ก ์๋ต ์ง์ฐ, ์น์ธ ๊ฑฐ์ , ์ค๋ณต ๊ฒฐ์ ์๋์ ๊ฐ์ ๋ค์ํ ์ฃ์ง ์ผ์ด์ค ๊ฒ์ฆ
- ํฌ์คํ : ์ธ๋ถ ์์กด์ฑ ์ ์ด๋ฅผ ํตํ ๊ฒฐ์ ํ๋ก์ธ์ค ๋ค์ํ ์๋๋ฆฌ์ค ๊ฒ์ฆ
- 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๋ ๋ฐ๊นฅ ๋ ์ด์ด๋ฅผ ์ฐธ์กฐํ๋ฉด ๋น๋ ์คํจ
| ํฌํธ | ์๋น์ค | ์ค๋ช |
|---|---|---|
| 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 # ๊ธฐ๋ ์งํ ์๋ ์คํ| ์คํฌ๋ฆฝํธ | ๊ฒ์ฆ ํญ๋ชฉ | ๊ฒฐ์ 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 | ๊ฒฐ์ ํ์คํ ๋ฆฌ โ ์ํ ๋ณ๊ฒฝ ์ด๋ ฅ ์กฐํ |