diff --git a/README.md b/README.md index 4e47ec6..e5e1b64 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,294 @@ -# Template Backend +๏ปฟ
-[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=ureca-mini2-div4_template-backend&metric=alert_status&token=35bd9a636ce0f7bc836903d9cd4487365306b29c)](https://sonarcloud.io/summary/new_code?id=ureca-mini2-div4_template-backend) -[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=ureca-mini2-div4_template-backend&metric=coverage&token=35bd9a636ce0f7bc836903d9cd4487365306b29c)](https://sonarcloud.io/summary/new_code?id=ureca-mini2-div4_template-backend) +# ๐Ÿš€ dabom-processor-usage + +### Kafka `usage-events` ๊ธฐ๋ฐ˜ +### **์‹ค์‹œ๊ฐ„ ์‚ฌ์šฉ๋Ÿ‰ ํŒ๋‹จ ยท DB ์ง์ ‘ ์ •์‚ฐ ยท Notification ๋ฐœํ–‰/๋ณต๊ตฌ ์—ฐ๊ณ„ ์„œ๋น„์Šค** + +
+ +

+ + + + + +

+ +

+ + + +

+ +
+ +--- + +## โœจ Overview + +`dabom-processor-usage`๋Š” DABOM ์‹œ์Šคํ…œ์—์„œ **์‚ฌ์šฉ๋Ÿ‰ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์ „๋‹ด ๋ ˆํฌ**๋‹ค. + +์ด ๋ ˆํฌ๋Š” ๋‹จ์ˆœ Kafka consumer๊ฐ€ ์•„๋‹ˆ๋ผ, ์•„๋ž˜ ํ๋ฆ„์„ **ํ•˜๋‚˜์˜ ์„œ๋น„์Šค ์•ˆ์—์„œ ์ง์ ‘ ์ฒ˜๋ฆฌ**ํ•œ๋‹ค. + +- `usage-events` ์ˆ˜์‹  +- payload ๋ฐ `familyId-customerId` ๊ฒ€์ฆ +- Redis warmup +- Redis + Lua ๊ธฐ๋ฐ˜ ์‹ค์‹œ๊ฐ„ ์‚ฌ์šฉ๋Ÿ‰ ํŒ๋‹จ +- DB ์ง์ ‘ ์ •์‚ฐ +- notification ๋Œ€์ƒ ํŒ๋‹จ +- notification ์ฆ‰์‹œ ๋น„๋™๊ธฐ ๋ฐœํ–‰ +- ์‹คํŒจ ๊ฑด์— ๋Œ€ํ•œ Outbox ๊ธฐ๋ฐ˜ ๋ณต๊ตฌ ์—ฐ๊ณ„ + +### ๐Ÿ“Œ At a Glance + +
+ +| Area | Responsibility | +|---|---| +| โšก Real-time Decision | Redis + Lua | +| ๐Ÿงพ Direct Settlement | MySQL | +| ๐Ÿ”” Publish & Recovery | `notification-events` + Outbox | + +
+ +์ฆ‰ ์ด ๋ ˆํฌ๋Š” ์•„๋ž˜ ์—ญํ• ์„ ๋™์‹œ์— ๋‹ด๋‹นํ•œ๋‹ค. + +| ์—ญํ•  | ์„ค๋ช… | +|---|---| +| โšก ์‹ค์‹œ๊ฐ„ ํŒ๋‹จ ์„œ๋น„์Šค | Redis + Lua๋กœ ์‚ฌ์šฉ๋Ÿ‰ ์ƒํƒœ๋ฅผ ๋น ๋ฅด๊ฒŒ ๊ณ„์‚ฐ | +| ๐Ÿงพ ์ •์‚ฐ ์„œ๋น„์Šค | DB๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ตœ์ข… ์‚ฌ์šฉ๋Ÿ‰๊ณผ quota๋ฅผ ๋ฐ˜์˜ | +| ๐Ÿ”” ์•Œ๋ฆผ ํŒŒ์ƒ ์„œ๋น„์Šค | notification ๋Œ€์ƒ ์—ฌ๋ถ€๋ฅผ ๊ฒฐ์ •ํ•˜๊ณ  ๋ฐœํ–‰๊ณผ ๋ณต๊ตฌ๋ฅผ ์—ฐ๊ณ„ | + +--- + +## ๐Ÿ—๏ธ Architecture Summary + +ํ˜„์žฌ ๊ตฌ์กฐ๋Š” **`usage-events` ์ค‘์‹ฌ ์ง์ ‘ ์ฒ˜๋ฆฌ ๊ตฌ์กฐ**๋‹ค. + +๊ณผ๊ฑฐ์—๋Š” usage ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ๊ฐ€ `usage-persist`, `usage-realtime`, `notification-events` ๋“ฑ ์—ฌ๋Ÿฌ Kafka ํ† ํ”ฝ์œผ๋กœ ๋ถ„์‚ฐ๋˜๋˜ ๊ตฌ์กฐ๊ฐ€ ์žˆ์—ˆ์ง€๋งŒ, ํ˜„์žฌ๋Š” ๋‹ค์Œ ๋ฐฉํ–ฅ์œผ๋กœ ๋‹จ์ˆœํ™”๋˜์—ˆ๋‹ค. + +- DB ์ •์‚ฐ์€ ์ด ์„œ๋น„์Šค๊ฐ€ ์ง์ ‘ ์ˆ˜ํ–‰ +- notification์€ ๋Œ€์ƒ ์ด๋ฒคํŠธ๋งŒ Outbox์— ์ €์žฅ +- usage ์„œ๋น„์Šค๊ฐ€ ๋จผ์ € ์ฆ‰์‹œ ๋น„๋™๊ธฐ ๋ฐœํ–‰ ์‹œ๋„ +- ์‹คํŒจ ๊ฑด์€ `PUBLISH_PENDING` ์œ ์ง€ + +```mermaid +flowchart LR + A[Kafka: usage-events] --> B[Usage Consumer] + B --> C[Validation] + C --> D[Redis Warmup] + D --> E[Lua Decision] + E --> F[DB Settlement] + F --> G{Should Notify?} + G -- No --> H[Done] + G -- Yes --> I[Save Outbox PUBLISH_PENDING] + I --> J[Async Publish to notification-events] + J --> K{Broker Ack} + K -- Success --> L[Mark SENT] + K -- Fail --> M[Keep PUBLISH_PENDING] + M --> N[External Recovery Process] +``` + +> ํ•ต์‹ฌ์€ **๋‹ค์ค‘ ํ† ํ”ฝ ๋ถ„์‚ฐ ๊ตฌ์กฐ๋ฅผ ์ค„์ด๊ณ **, usage ์ฒ˜๋ฆฌ์˜ ํ•ต์‹ฌ ์ฑ…์ž„์„ ์ด ์„œ๋น„์Šค ์•ˆ์œผ๋กœ ๋ชจ์•˜๋‹ค๋Š” ์ ์ด๋‹ค. + +--- + +## ๐ŸŽฏ Why This Repository Exists + +์‚ฌ์šฉ๋Ÿ‰ ์ฒ˜๋ฆฌ์—์„œ๋Š” ์•„๋ž˜ ์š”๊ตฌ๊ฐ€ ๋™์‹œ์— ์กด์žฌํ•œ๋‹ค. + +- **๋น ๋ฅด๊ฒŒ ํŒ๋‹จํ•ด์•ผ ํ•œ๋‹ค** +- **์ •ํ™•ํ•˜๊ฒŒ ์ •์‚ฐํ•ด์•ผ ํ•œ๋‹ค** +- **์ค‘๋ณต ์ด๋ฒคํŠธ์™€ ์žฌ์ฒ˜๋ฆฌ๋ฅผ ๊ฒฌ๋ŽŒ์•ผ ํ•œ๋‹ค** +- **์•Œ๋ฆผ์€ ์ฆ‰์‹œ ๋ณด๋‚ด๋˜ ์‹คํŒจ ์‹œ ๋ณต๊ตฌ ๊ฐ€๋Šฅํ•ด์•ผ ํ•œ๋‹ค** + +์ด ๋ ˆํฌ๋Š” ์ด ์š”๊ตฌ๋ฅผ ์•„๋ž˜์ฒ˜๋Ÿผ ๋ถ„๋ฆฌํ•ด์„œ ํ•ด๊ฒฐํ•œ๋‹ค. + +### Redis + Lua +- ๋น ๋ฅธ ํŒ๋‹จ +- dedup ์ œ์–ด +- ์‹ค์‹œ๊ฐ„ ์ •์ฑ… ์ฒ˜๋ฆฌ +- ๊ฒฝ๊ณ /์ฐจ๋‹จ ์ƒํƒœ ๊ณ„์‚ฐ + +### DB +- ์ตœ์ข… ์ •์‚ฐ +- ์˜์† ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ +- source of truth ์—ญํ•  + +### Outbox +- notification ๋Œ€์ƒ ์ด๋ฒคํŠธ๋งŒ ์ €์žฅ +- ์ฆ‰์‹œ ๋ฐœํ–‰ ์‹คํŒจ ์‹œ ๋ณต๊ตฌ ๊ธฐ์ค€์  ์œ ์ง€ + +```mermaid +flowchart TD + A[Redis + Lua] -->|๋น ๋ฅธ ํŒ๋‹จ| D[Usage Processing] + B[DB] -->|์ตœ์ข… ์ •์‚ฐ| D + C[Outbox] -->|๋ฐœํ–‰ ์‹คํŒจ ๋ณต๊ตฌ ๊ธฐ์ค€์ | D +``` + +--- + +## ๐Ÿงฉ Responsibilities + +### 1) Validation +- payload ๊ธฐ๋ณธ ๊ฒ€์ฆ +- `familyId-customerId` ๊ด€๊ณ„ ๊ฒ€์ฆ +- invalid input๋Š” ๋’ค ๋‹จ๊ณ„๋กœ ๋ณด๋‚ด์ง€ ์•Š์Œ + +### 2) Redis + Lua Decision +- Redis warmup +- duplicate ํŒ๋‹จ +- ์‚ฌ์šฉ๋Ÿ‰ ๋ฐ˜์˜ +- ๊ฒฝ๊ณ /์ฐจ๋‹จ ์ƒํƒœ ๊ณ„์‚ฐ +- ์•Œ๋ฆผ dedup ํŒ๋‹จ + +### 3) Direct DB Settlement +- `usage_record` ๊ธฐ๋ฐ˜ ๋ฉฑ๋“ฑ ์ •์‚ฐ +- `customer_quota`, `family_quota` ์ง์ ‘ ๋ฐ˜์˜ +- blocked ์ด๋ฒคํŠธ๋Š” ์ฐจ๋‹จ ์ƒํƒœ๋งŒ ๋ฐ˜์˜ + +### 4) Notification Publish & Recovery +- notification ๋Œ€์ƒ ํŒ๋‹จ +- Outbox์— `PUBLISH_PENDING` ์ €์žฅ +- `notification-events` ์ฆ‰์‹œ ๋น„๋™๊ธฐ ๋ฐœํ–‰ +- ์„ฑ๊ณต ์‹œ `SENT`, ์‹คํŒจ ์‹œ `PUBLISH_PENDING` ์œ ์ง€ + +```mermaid +sequenceDiagram + participant K as Kafka usage-events + participant U as Usage Service + participant R as Redis/Lua + participant D as DB + participant O as Outbox + participant N as Kafka notification-events + + K->>U: usage-events 1๊ฑด + U->>U: payload / family-customer ๊ฒ€์ฆ + U->>R: warmup + Lua ์‹คํ–‰ + R-->>U: duplicate, status, shouldNotify + U->>D: DB ์ง์ ‘ ์ •์‚ฐ + alt shouldNotify = true + U->>O: PUBLISH_PENDING ์ €์žฅ + U->>N: ๋น„๋™๊ธฐ ๋ฐœํ–‰ ์‹œ๋„ + alt ack success + U->>O: SENT ๋ฐ˜์˜ + else ack fail + U->>O: PUBLISH_PENDING ์œ ์ง€ + end + else shouldNotify = false + U->>U: Outbox row ์ƒ์„ฑ ์—†์Œ + end +``` + +--- + +## ๐Ÿ“จ Kafka Topics + +
+ +| Type | Topic | +|---|---| +| ๐Ÿ“ฅ Consumed | `usage-events` | +| ๐Ÿ“ค Produced | `notification-events` | +| ๐Ÿ•ฐ๏ธ Historical | `usage-persist`, `usage-realtime` | + +
+ +--- + +## ๐Ÿ—๏ธ Key Design Decisions + +
+1. Redis์™€ DB์˜ ์—ญํ•  ๋ถ„๋ฆฌ + +
+ +- Redis: ๋น ๋ฅธ ํŒ๋‹จ๊ณผ ์‹ค์‹œ๊ฐ„ ์ƒํƒœ ๊ณ„์‚ฐ +- DB: ์ตœ์ข… ์ •์‚ฐ๊ณผ ์˜์† ๊ธฐ์ค€์  + +์ฆ‰, **Redis๋Š” ์†๋„**, **DB๋Š” ์ •ํ™•์„ฑ**์„ ๋‹ด๋‹นํ•˜๋„๋ก ๋ถ„๋ฆฌํ–ˆ๋‹ค. + +
+ +
+2. DB ์ •์‚ฐ์€ ๋ฉฑ๋“ฑํ•˜๊ฒŒ ์žฌ์ง„์ž… ๊ฐ€๋Šฅ + +
+ +- `usage_record`๊ฐ€ ์„ ํ–‰ ๋ฉฑ๋“ฑ ๊ฐ€๋“œ ์—ญํ•  +- quota ๋ฐ˜์˜์€ ์ƒˆ insert ์„ฑ๊ณต ์‹œ์—๋งŒ ์ˆ˜ํ–‰ +- blocked ์ƒํƒœ๋Š” ์žฌ์ ์šฉ๋˜์–ด๋„ ์ตœ์ข… ์ƒํƒœ๊ฐ€ ๊นจ์ง€์ง€ ์•Š์Œ + +์ฆ‰ duplicate์™€ retry๋ฅผ **์˜ˆ์™ธ๊ฐ€ ์•„๋‹ˆ๋ผ ๊ธฐ๋ณธ ์ „์ œ**๋กœ ๋ณด๊ณ  ์„ค๊ณ„ํ–ˆ๋‹ค. + +
+ +
+3. ์ž˜๋ชป๋œ ์ž…๋ ฅ์€ ์ดˆ์ž…์—์„œ ์ฐจ๋‹จ + +
+ +membership ๊ฒ€์ฆ์„ ์ดˆ์ž…์œผ๋กœ ๋Œ์–ด์˜ฌ๋ ค, ๋น„์ •์ƒ ์ž…๋ ฅ์ด Redis, DB, notification ๋‹จ๊ณ„๊นŒ์ง€ ๋‚ด๋ ค๊ฐ€์ง€ ์•Š๋„๋ก ๋ง‰๋Š”๋‹ค. + +
+ +
+4. Notification์€ ์ฆ‰์‹œ์„ฑ๊ณผ ๋ณต๊ตฌ์„ฑ์„ ํ•จ๊ป˜ ๊ฐ€์ ธ๊ฐ + +
+ +- usage ์„œ๋น„์Šค๊ฐ€ ๋จผ์ € ์ฆ‰์‹œ ๋น„๋™๊ธฐ ๋ฐœํ–‰ +- ์‹คํŒจ ๊ฑด๋งŒ Outbox์— ๋‚จ๊ฒจ ํ›„์† ๋ณต๊ตฌ ๊ฐ€๋Šฅ + +์ฆ‰, **์ฆ‰์‹œ์„ฑ**๊ณผ **๋ณต๊ตฌ์„ฑ**์„ ๋™์‹œ์— ๊ณ ๋ คํ–ˆ๋‹ค. + +
+ +
+5. Outbox๋Š” publish-candidate only + +
+ +- ๋ชจ๋“  ์ด๋ฒคํŠธ๋ฅผ ์ €์žฅํ•˜์ง€ ์•Š์Œ +- notification ๋Œ€์ƒ ์ด๋ฒคํŠธ๋งŒ ์ €์žฅ +- ๊ณ TPS ํ™˜๊ฒฝ์—์„œ ๋ถˆํ•„์š”ํ•œ DB hot path write๋ฅผ ์ค„์ด๊ธฐ ์œ„ํ•œ ์„ ํƒ + +
+ +--- + +## ๐Ÿšจ Failure Handling + +| ์œ ํ˜• | ์ฒ˜๋ฆฌ ๋ฐฉ์‹ | +|---|---| +| IGNORE | payload ๊ณ„์•ฝ ์œ„๋ฐ˜, ์ž˜๋ชป๋œ family-customer ์กฐํ•ฉ | +| RETRY | Redis ์‹คํŒจ, Lua ์‹คํŒจ, DB ์ •์‚ฐ ์‹คํŒจ, Outbox ์ €์žฅ ์‹คํŒจ | +| DLQ / Non-Retryable | ์ƒํƒœ ๊ณ„์•ฝ ๋ถˆ์ผ์น˜, ๋ณต๊ตฌ ๋ถˆ๊ฐ€๋Šฅํ•œ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™” ์˜ค๋ฅ˜ | + +--- + +## ๐Ÿ“š Further Reading + +๊ณต์‹ ๋ฌธ์„œ๋Š” `docs/` ์•„๋ž˜ ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•œ๋‹ค. + +- [์„œ๋น„์Šค ๊ฐœ์š” ๋ฐ ๊ตฌ์กฐ ๋ณ€ํ™”](./docs/01_SERVICE_OVERVIEW.md) +- [ํ˜„์žฌ ์ฒ˜๋ฆฌ ํ”Œ๋กœ์šฐ](./docs/02_PROCESSING_FLOW.md) +- [Outbox ๋ฐ ์žฌ์‹œ๋„](./docs/03_OUTBOX_AND_RETRY.md) +- [์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๋ฐ ๋ณต๊ตฌ ์ „๋žต](./docs/04_EXCEPTION_AND_RECOVERY.md) +- [๋ฐ์ดํ„ฐ ๋ชจ๋ธ ๋ฐ Redis ํ‚ค ๊ตฌ์กฐ](./docs/05_DATA_MODEL_AND_KEYS.md) + +--- + +## ๐Ÿ“ Summary + +`dabom-processor-usage`์˜ ํ•ต์‹ฌ์€ ๋‹ค์Œ์œผ๋กœ ์ •๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค. + +- ์ดˆ๊ธฐ์˜ ๋‹ค์ค‘ ํ† ํ”ฝ ๋ถ„์‚ฐ ๊ตฌ์กฐ๋ฅผ ์ค„์˜€๋‹ค. +- `usage-events` ์ค‘์‹ฌ์œผ๋กœ DB ์ •์‚ฐ ์ฑ…์ž„์„ ์ด ์„œ๋น„์Šค์— ๋ชจ์•˜๋‹ค. +- Redis + Lua๋กœ ์‹ค์‹œ๊ฐ„ ํŒ๋‹จ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค. +- notification์€ ์ฆ‰์‹œ ๋น„๋™๊ธฐ ๋ฐœํ–‰๊ณผ Outbox ๊ธฐ๋ฐ˜ ๋ณต๊ตฌ๋ฅผ ๋ถ„๋ฆฌํ•œ๋‹ค. +- Outbox๋Š” ๋ฐœํ–‰ ๋Œ€์ƒ ์ด๋ฒคํŠธ๋งŒ ์ €์žฅํ•ด ๊ณ TPS ๋น„์šฉ์„ ์ค„์ธ๋‹ค. + +> ์ฆ‰ ์ด ๋ ˆํฌ๋Š” ๋‹จ์ˆœ consumer ๊ตฌํ˜„์ด ์•„๋‹ˆ๋ผ, +> **์‹ค์‹œ๊ฐ„์„ฑ ยท ์ •ํ•ฉ์„ฑ ยท ๋ณต๊ตฌ์„ฑ ยท ์šด์˜ ๊ฐ€๋Šฅ์„ฑ**์„ ํ•จ๊ป˜ ๊ณ ๋ คํ•ด์„œ ์ •๋ฆฌ๋œ usage processing ์„œ๋น„์Šค๋‹ค. \ No newline at end of file diff --git a/docs/01_SERVICE_OVERVIEW.md b/docs/01_SERVICE_OVERVIEW.md new file mode 100644 index 0000000..86c470f --- /dev/null +++ b/docs/01_SERVICE_OVERVIEW.md @@ -0,0 +1,331 @@ +๏ปฟ# Service Overview + +## 1. Purpose + +`dabom-processor-usage`๋Š” `usage-events`๋ฅผ ์†Œ๋น„ํ•ด ์‚ฌ์šฉ๋Ÿ‰ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ์„œ๋น„์Šค๋‹ค. + +ํ˜„์žฌ ์ด ์„œ๋น„์Šค๊ฐ€ ์ˆ˜ํ–‰ํ•˜๋Š” ์ผ์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค. + +- payload ๊ธฐ๋ณธ ๊ฒ€์ฆ +- `familyId-customerId` ๊ด€๊ณ„ ๊ฒ€์ฆ +- Redis warmup +- Redis + Lua ๊ธฐ๋ฐ˜ ์ƒํƒœ ๊ณ„์‚ฐ +- DB ์ง์ ‘ ์ •์‚ฐ +- notification ๋Œ€์ƒ ํŒ๋‹จ +- notification ์ฆ‰์‹œ ๋น„๋™๊ธฐ ๋ฐœํ–‰ ์‹œ๋„ +- ๋ฐœํ–‰ ์‹คํŒจ ๊ฑด์— ๋Œ€ํ•œ ๋ณต๊ตฌ ๊ธฐ์ค€์  ์ €์žฅ + +์ฆ‰ ์ด ์„œ๋น„์Šค๋Š” ๋‹จ์ˆœ consumer๊ฐ€ ์•„๋‹ˆ๋ผ, ์‹ค์‹œ๊ฐ„ ํŒ๋‹จ๊ณผ ์ •์‚ฐ, notification ํŒŒ์ƒ์„ ํ•จ๊ป˜ ๋‹ด๋‹นํ•˜๋Š” usage processing service๋‹ค. + +## 2. What the Previous Structure Looked Like + +์ดˆ๊ธฐ์—๋Š” usage ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ๊ฐ€ ์—ฌ๋Ÿฌ Kafka ํ† ํ”ฝ์œผ๋กœ ๋ถ„์‚ฐ๋˜๋Š” ๊ตฌ์กฐ์˜€๋‹ค. + +- `usage-events` +- `usage-persist` +- `usage-realtime` +- `notification-events` + +๋‹น์‹œ ํ๋ฆ„์€ ๋Œ€๋žต ์•„๋ž˜์™€ ๊ฐ™์•˜๋‹ค. + +1. `usage-events` ์ˆ˜์‹  +2. Redis/Lua๋กœ ์ƒํƒœ ๊ณ„์‚ฐ +3. `usage-persist`๋กœ DB ์ •์‚ฐ์šฉ ์ด๋ฒคํŠธ ๋ฐœํ–‰ +4. `usage-realtime`๋กœ ์‹ค์‹œ๊ฐ„ ์‚ฌ์šฉ๋Ÿ‰ ์ด๋ฒคํŠธ ๋ฐœํ–‰ +5. `notification-events`๋กœ ์•Œ๋ฆผ ์ด๋ฒคํŠธ ๋ฐœํ–‰ + +์ฆ‰ usage ์ด๋ฒคํŠธ 1๊ฑด์ด ๋“ค์–ด์˜ค๋ฉด, ๊ทธ ๊ฒฐ๊ณผ๊ฐ€ ์—ฌ๋Ÿฌ ํ† ํ”ฝ์œผ๋กœ ๋‹ค์‹œ ๋ถ„๊ธฐ๋˜๋Š” ๊ตฌ์กฐ์˜€๋‹ค. + +```mermaid +flowchart LR + A[usage-events] --> B[Redis + Lua ํŒ๋‹จ] + B --> C[usage-persist] + B --> D[usage-realtime] + B --> E[notification-events] +``` + +## 3. Why the Structure Changed + +์ด์ „ ๊ตฌ์กฐ๋Š” ๊ธฐ๋Šฅ์„ ํ† ํ”ฝ์œผ๋กœ ๋ถ„๋ฆฌํ–ˆ๋‹ค๋Š” ์ ์—์„œ๋Š” ๋‹จ์ˆœํ•ด ๋ณด์ผ ์ˆ˜ ์žˆ์—ˆ์ง€๋งŒ, ์‹ค์ œ usage ์ฒ˜๋ฆฌ ๊ด€์ ์—์„œ๋Š” ์•„๋ž˜ ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ๋‹ค. + +### 3.1 DB ์ •์‚ฐ ์ฑ…์ž„์ด ๋ฉ€๋ฆฌ ๋–จ์–ด์ ธ ์žˆ์—ˆ๋‹ค + +usage ์ƒํƒœ๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” ๊ณณ๊ณผ DB ์ •์‚ฐ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ณณ์ด `usage-persist` ๊ฒฝ๋กœ๋กœ ๋ถ„๋ฆฌ๋ผ ์žˆ์—ˆ๋‹ค. + +์ด ๋•Œ๋ฌธ์—: +- ์‚ฌ์šฉ๋Ÿ‰ ํŒ๋‹จ๊ณผ ์ตœ์ข… ์ •์‚ฐ์„ ํ•œ ํ๋ฆ„์œผ๋กœ ์„ค๋ช…ํ•˜๊ธฐ ์–ด๋ ค์› ๊ณ  +- ์‹คํŒจ ์‹œ ์–ด๋””๋ถ€ํ„ฐ ๋‹ค์‹œ ๋ด์•ผ ํ•˜๋Š”์ง€๊ฐ€ ๋ถ„์‚ฐ๋˜์—ˆ๊ณ  +- ๋ฉฑ๋“ฑ์„ฑ ๋ณด์žฅ์„ ์–ด๋А ์ง€์ ์ด ์ฑ…์ž„์ง€๋Š”์ง€ ๋ฐ”๋กœ ์ฝํžˆ์ง€ ์•Š์•˜๋‹ค. + +### 3.2 ํ•˜๋‚˜์˜ ์ด๋ฒคํŠธ๊ฐ€ ์—ฌ๋Ÿฌ ํ† ํ”ฝ์œผ๋กœ ๋ถ„๊ธฐ๋๋‹ค + +usage ์ด๋ฒคํŠธ 1๊ฑด์ด ๋“ค์–ด์˜ค๋ฉด `usage-persist`, `usage-realtime`, `notification-events`๋กœ ๋‹ค์‹œ ๋‚˜๊ฐ”๋‹ค. + +์ด ๋•Œ๋ฌธ์—: +- ์šด์˜์ž๊ฐ€ ํ•˜๋‚˜์˜ usage ์ด๋ฒคํŠธ๋ฅผ ์ถ”์ ํ•˜๋ ค๋ฉด ์—ฌ๋Ÿฌ ํ† ํ”ฝ์„ ๊ฐ™์ด ๋ด์•ผ ํ–ˆ๊ณ  +- ์žฅ์•  ์‹œ โ€œ์ด ์ด๋ฒคํŠธ๊ฐ€ ์–ด๋А ๋‹จ๊ณ„๊นŒ์ง€ ์ง„ํ–‰๋๋Š”๊ฐ€โ€๋ฅผ ์„ค๋ช…ํ•˜๊ธฐ ์–ด๋ ค์› ๊ณ  +- ์‹ค์‹œ๊ฐ„ ํŒ๋‹จ๊ณผ ์ •์‚ฐ, ์•Œ๋ฆผ ์‚ฌ์ด์˜ ๊ด€๊ณ„๊ฐ€ ๋ฌธ์„œ ์—†์ด ์ฝ”๋“œ๋งŒ ๋ด์„œ๋Š” ์ž˜ ๋“œ๋Ÿฌ๋‚˜์ง€ ์•Š์•˜๋‹ค. + +### 3.3 notification์˜ ์ฆ‰์‹œ์„ฑ๊ณผ ๋ณต๊ตฌ ์ง€์ ์ด ๋ถ„๋ฆฌ๋ผ ์žˆ์—ˆ๋‹ค + +์•Œ๋ฆผ์€ ๋ฐœํ–‰ ์ž์ฒด๋Š” Kafka ๊ฒฝ๋กœ๋กœ ๋ณด๋‚ด์ง€๋งŒ, ๋ฐœํ–‰ ์‹คํŒจ ํ›„ ์–ด๋–ค ๊ธฐ์ค€์œผ๋กœ ๋‹ค์‹œ ๋ณต๊ตฌํ•  ๊ฒƒ์ธ์ง€๋Š” ๋ช…ํ™•ํ•œ ๊ฒฝ๊ณ„๊ฐ€ ํ•„์š”ํ–ˆ๋‹ค. + +์ด ๋•Œ๋ฌธ์—: +- ์ฆ‰์‹œ ๋ฐœํ–‰๊ณผ ๋ณต๊ตฌ ๋ฐœํ–‰์˜ ์ฑ…์ž„์„ ๋‚˜๋ˆŒ ํ•„์š”๊ฐ€ ์žˆ์—ˆ๊ณ  +- usage ์„œ๋น„์Šค๊ฐ€ ์–ด๋””๊นŒ์ง€ ์ฑ…์ž„์ง€๋Š”์ง€๋ฅผ ๋ถ„๋ช…ํžˆ ํ•ด์•ผ ํ–ˆ๋‹ค. + +### 3.4 ์ž˜๋ชป๋œ ์ž…๋ ฅ์„ ๋” ์•ž์—์„œ ๋Š์–ด์•ผ ํ–ˆ๋‹ค + +usage ์ฒ˜๋ฆฌ์—์„œ `familyId-customerId` ๊ด€๊ณ„๊ฐ€ ํ‹€๋ฆฐ ์ž…๋ ฅ์€ Redis, Lua, DB, notification๊นŒ์ง€ ๋‚ด๋ ค๊ฐ€์ง€ ์•Š๋Š” ๊ฒƒ์ด ๋งž๋‹ค. + +์ด์ „๋ณด๋‹ค ๋” ์•ž๋‹จ์—์„œ: +- payload ๊ณ„์•ฝ ์œ„๋ฐ˜ +- family-customer ๋ถˆ์ผ์น˜ +๋ฅผ ๋Š๋Š” ๊ตฌ์กฐ๊ฐ€ ํ•„์š”ํ–ˆ๋‹ค. + +## 4. How the Current Structure Solves It + +ํ˜„์žฌ ๊ตฌ์กฐ๋Š” usage ์ฒ˜๋ฆฌ์˜ ํ•ต์‹ฌ ์ฑ…์ž„์„ `usage-events` ์ฒ˜๋ฆฌ ์„œ๋น„์Šค ์•ˆ์œผ๋กœ ๋ชจ์œผ๋Š” ๋ฐฉํ–ฅ์œผ๋กœ ์ •๋ฆฌ๋˜์—ˆ๋‹ค. + +### 4.1 DB ์ •์‚ฐ์„ usage ์„œ๋น„์Šค๊ฐ€ ์ง์ ‘ ์ˆ˜ํ–‰ + +์ด์ œ `usage-events`๋ฅผ ์†Œ๋น„ํ•œ ์งํ›„: +- validation +- Redis/Lua ํŒ๋‹จ +- DB ์ •์‚ฐ +์ด ํ•˜๋‚˜์˜ ์„œ๋น„์Šค ํ๋ฆ„ ์•ˆ์—์„œ ์ด์–ด์ง„๋‹ค. + +ํšจ๊ณผ: +- usage ์ด๋ฒคํŠธ 1๊ฑด์ด ์–ด๋–ค ์ˆœ์„œ๋กœ ์ฒ˜๋ฆฌ๋˜๋Š”์ง€ ์„ค๋ช…์ด ์‰ฌ์›Œ์ง +- ์ •์‚ฐ ๋ฉฑ๋“ฑ์„ฑ๊ณผ ๋ณต๊ตฌ ์ง€์ ์ด ์ด ์„œ๋น„์Šค ๊ธฐ์ค€์œผ๋กœ ์ •๋ฆฌ๋จ + +### 4.2 ์‹ค์‹œ๊ฐ„ ํŒ๋‹จ๊ณผ ์˜์† ์ •์‚ฐ์˜ ๊ฒฝ๊ณ„ ๋ช…ํ™•ํ™” + +ํ˜„์žฌ ๊ตฌ์กฐ๋Š” ์—ญํ• ์„ ์•„๋ž˜์ฒ˜๋Ÿผ ๋‚˜๋ˆˆ๋‹ค. + +- Redis + Lua: ๋น ๋ฅธ ํŒ๋‹จ๊ณผ ์ƒํƒœ ๊ณ„์‚ฐ +- DB: ์ตœ์ข… ์ •์‚ฐ๊ณผ ์˜์† ๊ธฐ์ค€์  + +ํšจ๊ณผ: +- Redis๋Š” ์‹ค์‹œ๊ฐ„์„ฑ +- DB๋Š” ์ •ํ•ฉ์„ฑ +์„ ๋‹ด๋‹นํ•œ๋‹ค๋Š” ์ ์ด ๊ตฌ์กฐ์ ์œผ๋กœ ๋ถ„๋ช…ํ•ด์กŒ๋‹ค. + +### 4.3 notification์€ ๋Œ€์ƒ ์ด๋ฒคํŠธ๋งŒ ์ €์žฅํ•˜๊ณ , ์ฆ‰์‹œ ๋ฐœํ–‰๊ณผ ๋ณต๊ตฌ๋ฅผ ๋ถ„๋ฆฌ + +ํ˜„์žฌ notification ํ๋ฆ„์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค. + +- notification ๋Œ€์ƒ์ผ ๋•Œ๋งŒ Outbox row ์ƒ์„ฑ +- usage ์„œ๋น„์Šค๊ฐ€ ์ฆ‰์‹œ ๋น„๋™๊ธฐ ๋ฐœํ–‰ ์‹œ๋„ +- ์‹คํŒจ ์‹œ `PUBLISH_PENDING` ์œ ์ง€ +- ์™ธ๋ถ€ ๋ณต๊ตฌ ํ”„๋กœ์„ธ์Šค๊ฐ€ pending row๋ฅผ ๋‹ค์‹œ ๋ฐœํ–‰ + +ํšจ๊ณผ: +- ์ฆ‰์‹œ์„ฑ์€ usage ์„œ๋น„์Šค๊ฐ€ ๋‹ด๋‹น +- ๋ณต๊ตฌ๋Š” Outbox๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํ›„์† ํ”„๋กœ์„ธ์Šค๊ฐ€ ๋‹ด๋‹น +- notification ๊ด€๋ จ ์ƒํƒœ๊ฐ€ DB row ๊ธฐ์ค€์œผ๋กœ ๊ด€๋ฆฌ๋œ๋‹ค. + +### 4.4 invalid input๋ฅผ ์ดˆ์ž…์—์„œ ์ฐจ๋‹จ + +`familyId-customerId` ๊ด€๊ณ„๋ฅผ Redis membership cache์™€ DB fallback์œผ๋กœ ๊ฒ€์ฆํ•œ๋‹ค. + +ํšจ๊ณผ: +- ์ž˜๋ชป๋œ ์ž…๋ ฅ์ด Redis ์ƒํƒœ๋ฅผ ์˜ค์—ผ์‹œํ‚ค์ง€ ์•Š์Œ +- ์ž˜๋ชป๋œ ์ž…๋ ฅ์ด DB ์ •์‚ฐ์ด๋‚˜ notification์œผ๋กœ ์ด์–ด์ง€์ง€ ์•Š์Œ + +## 5. Why These Technologies + +## 5.1 Why a Messaging Queue Was Needed + +usage ๋„๋ฉ”์ธ์€ ์‚ฌ์šฉ๋Ÿ‰ ์ด๋ฒคํŠธ๊ฐ€ ํ•œ ๋ฒˆ์— ๋ชฐ๋ฆด ์ˆ˜ ์žˆ๊ณ , ์ƒ์‚ฐ ์‹œ์ ๊ณผ ์ฒ˜๋ฆฌ ์‹œ์ ์„ ๋А์Šจํ•˜๊ฒŒ ๋ถ„๋ฆฌํ•  ํ•„์š”๊ฐ€ ์žˆ๋‹ค. + +๋ฉ”์‹œ์ง• ํ๋ฅผ ๋‘๋ฉด ์•„๋ž˜๊ฐ€ ๊ฐ€๋Šฅํ•ด์ง„๋‹ค. + +- usage ๋ฐœ์ƒ ์„œ๋น„์Šค์™€ usage ์ฒ˜๋ฆฌ ์„œ๋น„์Šค๋ฅผ ๋ถ„๋ฆฌ +- burst traffic๋ฅผ ํก์ˆ˜ํ•˜๋ฉด์„œ ์ˆœ์ฐจ ์ฒ˜๋ฆฌ ๊ธฐ์ค€ ์œ ์ง€ +- consumer ์žฌ์ฒ˜๋ฆฌ์™€ ์žฅ์•  ๋ณต๊ตฌ ๊ธฐ์ค€ ํ™•๋ณด +- ํ›„์† ํŒŒ์ƒ ์ฒ˜๋ฆฌ(notification ๋“ฑ)๋ฅผ ์ด๋ฒคํŠธ ์ค‘์‹ฌ์œผ๋กœ ์—ฐ๊ฒฐ + +์ฆ‰ ์ด ์„œ๋น„์Šค์—์„œ ๋ฉ”์‹œ์ง• ํ๋Š” ๋‹จ์ˆœ ์ „์†ก ์ฑ„๋„์ด ์•„๋‹ˆ๋ผ, usage ์ด๋ฒคํŠธ๋ฅผ ์•ˆ์ •์ ์œผ๋กœ ํ˜๋ ค๋ณด๋‚ด๋Š” ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ๊ธฐ๋ฐ˜์ด๋‹ค. + +## 5.2 Why Kafka + +์ด ์„œ๋น„์Šค๊ฐ€ Kafka๋ฅผ ์“ฐ๋Š” ์ด์œ ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค. + +- usage ์ด๋ฒคํŠธ๋Š” ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋งŽ์ด ๋“ค์–ด์˜ค๋Š” ์ŠคํŠธ๋ฆผ ์„ฑ๊ฒฉ์ด ๊ฐ•ํ•˜๋‹ค. +- ์ด๋ฒคํŠธ๋ฅผ ์ˆœ์„œ ์žˆ๊ฒŒ ๊ณ„์† ์†Œ๋น„ํ•ด์•ผ ํ•œ๋‹ค. +- ์žฅ์•  ์‹œ ๊ฐ™์€ ๋ ˆ์ฝ”๋“œ๋ฅผ ๋‹ค์‹œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์žฌ์ฒ˜๋ฆฌ ๋ชจ๋ธ์ด ์ค‘์š”ํ•˜๋‹ค. +- usage, notification์ฒ˜๋Ÿผ ๋‹ค๋ฅธ ํ›„์† ํ† ํ”ฝ๊ณผ๋„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์—ฐ๊ฒฐ๋œ๋‹ค. + +Kafka๋Š” ์ด๋Ÿฐ usage stream ์ฒ˜๋ฆฌ์— ์ž˜ ๋งž๋Š”๋‹ค. + +- ํ† ํ”ฝ ๊ธฐ๋ฐ˜์œผ๋กœ ์ด๋ฒคํŠธ ํ๋ฆ„์„ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค. +- consumer group์œผ๋กœ ์ˆ˜ํ‰ ํ™•์žฅ์ด ๊ฐ€๋Šฅํ•˜๋‹ค. +- offset ๊ธฐ๋ฐ˜์ด๋ผ ์žฌ์ฒ˜๋ฆฌ์™€ ์šด์˜ ์ถ”์ ์ด ๋ช…ํ™•ํ•˜๋‹ค. +- ๊ณ TPS ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ ์ฒ˜๋ฆฌ์— ์ ํ•ฉํ•˜๋‹ค. + +๋˜ํ•œ ์ด ์„œ๋น„์Šค๋Š” ๊ฐ€์กฑ ๋‹จ์œ„ ์ƒํƒœ๋ฅผ ๋งŽ์ด ๋‹ค๋ฃฌ๋‹ค. + +- `family_quota` +- ๊ฐ€์กฑ ์ž”์—ฌ๋Ÿ‰ +- ๊ฐ€์กฑ ๋‹จ์œ„ ๊ฒฝ๊ณ /์ฐจ๋‹จ ํŒ๋‹จ + +์ด๋Ÿฐ ๊ตฌ์กฐ์—์„œ๋Š” ๊ฐ™์€ ๊ฐ€์กฑ์˜ ์ด๋ฒคํŠธ๋ฅผ ๊ฐ™์€ ํ๋ฆ„์—์„œ ์ˆœ์„œ ์žˆ๊ฒŒ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์œ ๋ฆฌํ•˜๋‹ค. Kafka๋Š” key ๊ธฐ๋ฐ˜ partitioning์„ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ง€์›ํ•˜๋ฏ€๋กœ, `familyId`๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๊ฐ™์€ ๊ฐ€์กฑ ์ด๋ฒคํŠธ๋ฅผ ๊ฐ™์€ ํŒŒํ‹ฐ์…˜ ํ๋ฆ„์— ํƒœ์šฐ๋Š” ๋ชจ๋ธ๊ณผ ์ž˜ ๋งž๋Š”๋‹ค. + +์ฆ‰ Kafka๋Š” โ€œ์—…๋ฌด ๋ช…๋ น ์ „๋‹ฌโ€๋ณด๋‹ค โ€œ์ง€์†์ ์œผ๋กœ ์Œ“์ด๋Š” ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ ์ฒ˜๋ฆฌโ€์™€ โ€œ๊ฐ€์กฑ ๊ธฐ์ค€ ์ˆœ์„œ๊ฐ€ ์ค‘์š”ํ•œ usage ์ฒ˜๋ฆฌโ€์— ๋” ์ž˜ ๋งž๋Š” ์„ ํƒ์ด๋‹ค. + +## 5.3 Why Not RabbitMQ + +RabbitMQ ๊ฐ™์€ ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค๋„ ์ถฉ๋ถ„ํžˆ ํ›Œ๋ฅญํ•˜์ง€๋งŒ, ์ด ์„œ๋น„์Šค๊ฐ€ ๋‹ค๋ฃจ๋Š” ๋ฌธ์ œ์™€๋Š” ์„ฑ๊ฒฉ์ด ์กฐ๊ธˆ ๋‹ค๋ฅด๋‹ค. + +RabbitMQ๊ฐ€ ๋” ์ž˜ ๋งž๋Š” ๊ฒฝ์šฐ๋Š” ๋ณดํ†ต ์•„๋ž˜์™€ ๊ฐ™๋‹ค. + +- ์ž‘์—… ํ ๊ธฐ๋ฐ˜์˜ ๋ช…๋ น ์ฒ˜๋ฆฌ +- ๋ณต์žกํ•œ ๋ผ์šฐํŒ… ๊ทœ์น™ +- ๋น ๋ฅธ ๋‹จ๊ฑด ์ „๋‹ฌ๊ณผ ack ์ค‘์‹ฌ์˜ ๋ฉ”์‹œ์ง• + +๋ฐ˜๋ฉด `dabom-processor-usage`๋Š” ์•„๋ž˜ ํŠน์„ฑ์ด ๋” ์ค‘์š”ํ•˜๋‹ค. + +- ์‚ฌ์šฉ๋Ÿ‰ ์ด๋ฒคํŠธ๊ฐ€ ์ง€์†์ ์œผ๋กœ ๋“ค์–ด์˜ค๋Š” stream ์ฒ˜๋ฆฌ +- offset ๊ธฐ์ค€ ์žฌ์ฒ˜๋ฆฌ +- consumer group ํ™•์žฅ +- ๊ฐ™์€ ๊ฐ€์กฑ ์ด๋ฒคํŠธ์˜ ์ˆœ์„œ ์žˆ๋Š” ์ฒ˜๋ฆฌ + +RabbitMQ์—์„œ๋„ ์œ ์‚ฌํ•œ ๊ตฌ์„ฑ์„ ๋งŒ๋“ค ์ˆ˜๋Š” ์žˆ์ง€๋งŒ, ์ผ๋ฐ˜ queue ๋ชจ๋ธ์—์„œ๋Š” Kafka์ฒ˜๋Ÿผ key ๊ธฐ๋ฐ˜ partitioning์ด ๊ธฐ๋ณธ ๋ชจ๋ธ์€ ์•„๋‹ˆ๋‹ค. ๊ฐ™์€ ๊ฐ€์กฑ์˜ ์ด๋ฒคํŠธ๋ฅผ ๊ฐ™์€ ํ๋ฆ„์—์„œ ์•ˆ์ •์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋ ค๋ฉด ์ถ”๊ฐ€ ๋ผ์šฐํŒ… ์„ค๊ณ„๋‚˜ stream ๊ณ„์—ด ๊ตฌ์„ฑ์ด ํ•„์š”ํ•˜๋‹ค. + +์ฆ‰ ์ด ์„œ๋น„์Šค๋Š” โ€œํ•˜๋‚˜์˜ ์ž‘์—…์„ ๋ˆ„๊ฐ€ ์†Œ๋น„ํ• ๊นŒโ€๋ณด๋‹ค โ€œ๊ณ„์† ๋“ค์–ด์˜ค๋Š” usage ์ด๋ฒคํŠธ ํ๋ฆ„์„ ์–ด๋–ป๊ฒŒ ์•ˆ์ •์ ์œผ๋กœ ์ฒ˜๋ฆฌํ• ๊นŒโ€๊ฐ€ ๋” ์ค‘์š”ํ•œ ์„œ๋น„์Šค๋‹ค. ๊ทธ ์ ์—์„œ Kafka๊ฐ€ ๋” ์ž์—ฐ์Šค๋Ÿฝ๋‹ค. + +## 5.4 Why Redis + +usage ์ฒ˜๋ฆฌ์—๋Š” DB๋งŒ์œผ๋กœ๋Š” ๋ถ€์กฑํ•œ ๊ตฌ๊ฐ„์ด ์žˆ๋‹ค. + +์ด ์„œ๋น„์Šค๋Š” ์•„๋ž˜๋ฅผ ๋งค์šฐ ๋น ๋ฅด๊ฒŒ ํŒ๋‹จํ•ด์•ผ ํ•œ๋‹ค. + +- ๊ฐ€์กฑ ์ž”์—ฌ๋Ÿ‰์ด ์–ผ๋งˆ๋‚˜ ๋‚จ์•˜๋Š”์ง€ +- ๊ฐœ์ธ ์›” ์‚ฌ์šฉ๋Ÿ‰์ด ์–ผ๋งˆ์ธ์ง€ +- ํ˜„์žฌ ์ฐจ๋‹จ ์‹œ๊ฐ„๋Œ€์ธ์ง€ +- ํŠน์ • ์•ฑ์ด ์ฐจ๋‹จ ์ƒํƒœ์ธ์ง€ +- ์ง€๊ธˆ ์•Œ๋ฆผ์„ ๋ณด๋‚ด์•ผ ํ•˜๋Š”์ง€ + +์ด๊ฑธ ๋งค ์ด๋ฒคํŠธ๋งˆ๋‹ค DB๋งŒ ๋ณด๊ณ  ์ฒ˜๋ฆฌํ•˜๋ฉด: +- ์ฝ๊ธฐ ๋น„์šฉ์ด ์ปค์ง€๊ณ  +- ๋™์‹œ์— ์—ฌ๋Ÿฌ ์ด๋ฒคํŠธ๊ฐ€ ๋“ค์–ด์˜ฌ ๋•Œ race condition ๊ด€๋ฆฌ๊ฐ€ ์–ด๋ ค์›Œ์ง€๊ณ  +- ์‹ค์‹œ๊ฐ„ ํŒ๋‹จ ์†๋„๊ฐ€ ๋–จ์–ด์ง„๋‹ค. + +Redis๋ฅผ ๋‘๋ฉด: +- ํ•„์š”ํ•œ ์ƒํƒœ๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ๋น ๋ฅด๊ฒŒ ์ฝ๊ณ  +- Lua๋กœ ์›์ž์ ์œผ๋กœ ๊ฐฑ์‹ ๊ณผ ํŒ๋‹จ์„ ๊ฐ™์ด ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค. + +์ฆ‰ Redis๋Š” ๋‹จ์ˆœ ์บ์‹œ๊ฐ€ ์•„๋‹ˆ๋ผ, usage ์‹ค์‹œ๊ฐ„ ํŒ๋‹จ ์—”์ง„์˜ ์ผ๋ถ€ ์—ญํ• ์„ ํ•œ๋‹ค. + +## 5.5 Why Lua + +Redis๋งŒ ์“ฐ๋Š” ๊ฒƒ์œผ๋กœ๋Š” ์ถฉ๋ถ„ํ•˜์ง€ ์•Š๋‹ค. ์‚ฌ์šฉ๋Ÿ‰ ์ฆ๊ฐ€, ์ƒํƒœ ๊ณ„์‚ฐ, ์•Œ๋ฆผ dedup ํŒ๋‹จ์ด ๋ถ„๋ฆฌ๋˜์–ด ์žˆ์œผ๋ฉด race condition์ด ์ƒ๊ธธ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. + +Lua๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด: +- duplicate ํ™•์ธ +- remaining / monthly usage ๋ฐ˜์˜ +- ์ฐจ๋‹จ ํŒ๋‹จ +- ๊ฒฝ๊ณ  ํŒ๋‹จ +- alert dedup ๊ธฐ๋ก +์„ ํ•œ ๋ฒˆ์— ์›์ž์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค. + +์ฆ‰ Lua๋Š” โ€œ์—ฌ๋Ÿฌ Redis ๋ช…๋ น์„ ํ•œ ํŠธ๋žœ์žญ์…˜์ฒ˜๋Ÿผ ๋ฌถ์–ด ์ผ๊ด€๋œ ํŒ๋‹จ ๊ฒฐ๊ณผ๋ฅผ ๋งŒ๋“œ๋Š” ๋„๊ตฌโ€๋‹ค. + +## 5.6 Why Direct DB Settlement + +ํ˜„์žฌ ๊ตฌ์กฐ๋Š” usage ์„œ๋น„์Šค๊ฐ€ DB ์ •์‚ฐ์„ ์ง์ ‘ ์ˆ˜ํ–‰ํ•œ๋‹ค. + +์ด๋ ‡๊ฒŒ ํ•œ ์ด์œ ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค. + +- usage ์ƒํƒœ ๊ณ„์‚ฐ๊ณผ ์ •์‚ฐ์„ ํ•œ ํ๋ฆ„์œผ๋กœ ์„ค๋ช…ํ•˜๊ธฐ ์‰ฝ๋‹ค. +- `usage_record` ๊ธฐ๋ฐ˜ ๋ฉฑ๋“ฑ ์ •์‚ฐ์„ ๋ฐ”๋กœ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค. +- `usage-persist` ๊ฐ™์€ ์ค‘๊ฐ„ ํ† ํ”ฝ์„ ์ค„์—ฌ ์ฒ˜๋ฆฌ ๊ฒฝ๋กœ๋ฅผ ๋‹จ์ˆœํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค. + +์ฆ‰ ์ง์ ‘ ์ •์‚ฐ ๊ตฌ์กฐ๋Š” โ€œ์–ด๋””์„œ ์ตœ์ข… ์ •์‚ฐ์ด ์ผ์–ด๋‚˜๋Š”๊ฐ€โ€๋ฅผ usage ์„œ๋น„์Šค ์•ˆ์œผ๋กœ ๋ช…ํ™•ํžˆ ๋ชจ์œผ๋Š” ์„ ํƒ์ด๋‹ค. + +## 5.7 Why Outbox + +notification์€ ์ฆ‰์‹œ ๋ฐœํ–‰๋งŒ์œผ๋กœ ๋๋‚ด๊ธฐ ์–ด๋ ต๋‹ค. Kafka publish๋Š” ์‹คํŒจํ•  ์ˆ˜ ์žˆ๊ณ , ์‹คํŒจ ํ›„ ๋‹ค์‹œ ๋ณด๋‚ผ ๊ธฐ์ค€์ ์ด ํ•„์š”ํ•˜๋‹ค. + +Outbox๋ฅผ ๋‘๋ฉด: +- notification ๋Œ€์ƒ ์ด๋ฒคํŠธ๋งŒ DB row๋กœ ๋‚จ๊ธฐ๊ณ  +- ์ฆ‰์‹œ ๋ฐœํ–‰ ์‹คํŒจ ์‹œ `PUBLISH_PENDING`์„ ๊ธฐ์ค€์œผ๋กœ ๋‹ค์‹œ ๋ฐœํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค. + +์ฆ‰ Outbox๋Š” notification ์‹คํŒจ ํ›„ ๋ณต๊ตฌ ๊ธฐ์ค€์  ์—ญํ• ์„ ํ•œ๋‹ค. + +## 6. Current Architecture + +ํ˜„์žฌ ๊ตฌ์กฐ์˜ ์‚ฌ์‹ค์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค. + +- Kafka listener๋Š” `usage-events` ํ•˜๋‚˜๋ฅผ ์ฒ˜๋ฆฌํ•œ๋‹ค. +- Redis/Lua๋กœ `duplicate`, `status`, `shouldNotify`๋ฅผ ๊ณ„์‚ฐํ•œ๋‹ค. +- DB ์ •์‚ฐ์€ ์ด ์„œ๋น„์Šค๊ฐ€ ์ง์ ‘ ์ˆ˜ํ–‰ํ•œ๋‹ค. +- notification ๋Œ€์ƒ์ผ ๋•Œ๋งŒ Outbox row๋ฅผ ๋งŒ๋“ ๋‹ค. +- usage ์„œ๋น„์Šค๊ฐ€ notification์„ ์ฆ‰์‹œ ๋น„๋™๊ธฐ๋กœ ๋จผ์ € ๋ฐœํ–‰ํ•œ๋‹ค. +- ๋ฐœํ–‰ ์„ฑ๊ณต ์‹œ `SENT`, ์‹คํŒจ ์‹œ `PUBLISH_PENDING`์„ ์œ ์ง€ํ•œ๋‹ค. + +```mermaid +flowchart LR + A[Kafka: usage-events] --> B[Usage Consumer] + B --> C[Validation] + C --> D[Redis Warmup] + D --> E[Lua Decision] + E --> F[DB Settlement] + F --> G{Should Notify?} + G -- No --> H[Done] + G -- Yes --> I[Save Outbox PUBLISH_PENDING] + I --> J[Async Publish to notification-events] + J --> K{Broker Ack} + K -- Success --> L[Mark SENT] + K -- Fail --> M[Keep PUBLISH_PENDING] + M --> N[External Recovery Process] +``` + +## 7. Core Design Points + +### 7.1 Redis์™€ DB์˜ ์—ญํ•  ๋ถ„๋ฆฌ + +- Redis: ๋น ๋ฅธ ํŒ๋‹จ๊ณผ ์‹ค์‹œ๊ฐ„ ์ƒํƒœ ๊ณ„์‚ฐ +- DB: ์ตœ์ข… ์ •์‚ฐ๊ณผ ์˜์† ๊ธฐ์ค€์  + +### 7.2 DB ์ •์‚ฐ์€ ์žฌ์ง„์ž… ๊ฐ€๋Šฅ + +- `usage_record`๊ฐ€ ์„ ํ–‰ ๋ฉฑ๋“ฑ ๊ฐ€๋“œ ์—ญํ• ์„ ํ•œ๋‹ค. +- allowed ์ด๋ฒคํŠธ๋Š” ์ƒˆ `usage_record` insert ์„ฑ๊ณต ์‹œ์—๋งŒ quota๋ฅผ ๋ฐ˜์˜ํ•œ๋‹ค. +- blocked ์ด๋ฒคํŠธ๋Š” `usage_record` ์—†์ด ์ฐจ๋‹จ ์ƒํƒœ๋งŒ ๋ฐ˜์˜ํ•œ๋‹ค. + +### 7.3 ์ž˜๋ชป๋œ ์ž…๋ ฅ์€ ์ดˆ์ž…์—์„œ ์ฐจ๋‹จ + +- `familyId-customerId` ๊ด€๊ณ„๋ฅผ Redis membership cache์™€ DB fallback์œผ๋กœ ๊ฒ€์ฆํ•œ๋‹ค. +- ์‹ค์ œ๋กœ ์ž˜๋ชป๋œ ์กฐํ•ฉ์ด๋ฉด `IllegalArgumentException`์œผ๋กœ ์ค‘๋‹จํ•œ๋‹ค. +- Redis/DB ์กฐํšŒ ์ž์ฒด๊ฐ€ ์‹คํŒจํ•˜๋ฉด retryable ์˜ˆ์™ธ๋กœ ์ „ํŒŒํ•œ๋‹ค. + +### 7.4 Notification์€ publish-candidate outbox๋ฅผ ์‚ฌ์šฉ + +- ๋ชจ๋“  usage ์ด๋ฒคํŠธ๋ฅผ ์ €์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค. +- notification ๋Œ€์ƒ ์ด๋ฒคํŠธ๋งŒ `PUBLISH_PENDING` row๋ฅผ ๋งŒ๋“ ๋‹ค. +- ์ฆ‰์‹œ ๋ฐœํ–‰ ์‹คํŒจ ์‹œ row๋ฅผ ๋‚จ๊ฒจ ํ›„์† ๋ณต๊ตฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋„๋ก ํ•œ๋‹ค. + +## 8. Kafka Topics + +| Type | Topic | +|---|---| +| Consumed | `usage-events` | +| Produced | `notification-events` | +| Historical | `usage-persist`, `usage-realtime` | + +## 9. Service Boundary + +### This Repository Handles +- `usage-events` ์†Œ๋น„ +- ์‹ค์‹œ๊ฐ„ ์‚ฌ์šฉ๋Ÿ‰ ํŒ๋‹จ +- DB ์ง์ ‘ ์ •์‚ฐ +- notification ๋Œ€์ƒ ํŒ๋‹จ +- notification ์ฆ‰์‹œ ๋ฐœํ–‰ ์‹œ๋„ +- notification ๋ณต๊ตฌ ๊ธฐ์ค€์  ์ €์žฅ + +### External Recovery Process Handles +- `PUBLISH_PENDING` ์กฐํšŒ +- ์žฌ๋ฐœํ–‰ ์‹œ๋„ +- `SENT` ๋˜๋Š” `FAILED` ๋ฐ˜์˜ diff --git a/docs/02_PROCESSING_FLOW.md b/docs/02_PROCESSING_FLOW.md new file mode 100644 index 0000000..5077eba --- /dev/null +++ b/docs/02_PROCESSING_FLOW.md @@ -0,0 +1,210 @@ +๏ปฟ# Processing Flow + +## 1. End-to-End Flow + +ํ˜„์žฌ `dabom-processor-usage`์˜ usage ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ํ๋ฆ„์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค. + +1. Kafka์—์„œ `usage-events`๋ฅผ ์ˆ˜์‹ ํ•œ๋‹ค. +2. payload๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค. +3. `familyId-customerId` ๊ด€๊ณ„๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค. +4. Redis warmup์„ ์ˆ˜ํ–‰ํ•œ๋‹ค. +5. Lua๊ฐ€ `duplicate`, `status`, `shouldNotify`๋ฅผ ๊ณ„์‚ฐํ•œ๋‹ค. +6. Java๊ฐ€ Lua ๊ฒฐ๊ณผ๋ฅผ ํ•ด์„ํ•œ๋‹ค. +7. DB ์ •์‚ฐ์„ ์ง์ ‘ ์ˆ˜ํ–‰ํ•œ๋‹ค. +8. notification ๋Œ€์ƒ์ด๋ฉด Outbox์— `PUBLISH_PENDING`์„ ์ €์žฅํ•œ๋‹ค. +9. notification์„ ์ฆ‰์‹œ ๋น„๋™๊ธฐ ๋ฐœํ–‰ํ•œ๋‹ค. +10. ์„ฑ๊ณตํ•˜๋ฉด `SENT`, ์‹คํŒจํ•˜๋ฉด `PUBLISH_PENDING`์„ ์œ ์ง€ํ•œ๋‹ค. +11. notification ๋น„๋Œ€์ƒ์ด๋”๋ผ๋„, ๊ฐ™์€ `eventId`์— pending row๊ฐ€ ๋‚จ์•„ ์žˆ์œผ๋ฉด ์ฆ‰์‹œ ๋ฐœํ–‰์„ ๋‹ค์‹œ ์‹œ๋„ํ•œ๋‹ค. + +## 2. Validation + +### 2.1 Payload Validation + +์ดˆ์ž…์—์„œ ์•„๋ž˜๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค. + +- payload null ์—ฌ๋ถ€ +- `familyId > 0` +- `customerId > 0` +- `bytesUsed > 0` + +์ด ๊ฒ€์ฆ์€ ๊ฐ€์žฅ ์•ž์—์„œ ์ž˜๋ชป๋œ ๋ฉ”์‹œ์ง€๋ฅผ ์ฐจ๋‹จํ•˜๊ธฐ ์œ„ํ•œ ๋‹จ๊ณ„๋‹ค. + +ํšจ๊ณผ: +- Redis ์ƒํƒœ ์˜ค์—ผ ๋ฐฉ์ง€ +- DB ์ •์‚ฐ ์˜ค์—ผ ๋ฐฉ์ง€ +- ๋ถˆํ•„์š”ํ•œ ํ›„์† ์ฒ˜๋ฆฌ ๋ฐฉ์ง€ + +### 2.2 Family-Customer Validation + +`familyId-customerId` ๊ด€๊ณ„๋ฅผ Redis membership cache์™€ DB fallback์œผ๋กœ ๊ฒ€์ฆํ•œ๋‹ค. + +์ˆœ์„œ: +1. Redis `family:{familyId}:members` ํ™•์ธ +2. cache hit๋ฉด ํ†ต๊ณผ +3. miss ๋˜๋Š” ๋ถˆ์ผ์น˜๋ฉด DB fallback +4. DB์—๋„ ์—†์œผ๋ฉด invalid input +5. Redis์™€ DB ์กฐํšŒ๊ฐ€ ์‹คํŒจํ•˜๋ฉด retryable ์˜ˆ์™ธ๋กœ ์ „ํŒŒ + +์ด ๋‹จ๊ณ„๋Š” usage ์ด๋ฒคํŠธ๊ฐ€ ์‹ค์ œ ๊ฐ€์กฑ ๊ตฌ์„ฑ์› ๊ด€๊ณ„๋ฅผ ๋งŒ์กฑํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๋‹จ๊ณ„๋‹ค. + +ํšจ๊ณผ: +- ์ž˜๋ชป๋œ family-customer ์กฐํ•ฉ ์ฐจ๋‹จ +- Redis, Lua, DB, notification๊นŒ์ง€ ์ž˜๋ชป๋œ ์ž…๋ ฅ์ด ๋‚ด๋ ค๊ฐ€์ง€ ์•Š์Œ + +## 3. Redis + Lua + +### 3.1 Redis Warmup + +Lua ์‹คํ–‰ ์ „์— ํ•„์š”ํ•œ Redis ์ƒํƒœ๋ฅผ ์ฑ„์šด๋‹ค. + +๋Œ€์ƒ: +- ๊ฐ€์กฑ info +- ๊ฐ€์กฑ remaining +- ๊ฐœ์ธ ์›” ์‚ฌ์šฉ๋Ÿ‰ +- policy constraints + +warmup์€ Redis์— ๊ฐ’์ด ์—†์„ ๋•Œ DB๋ฅผ ์ฝ์–ด ํ•„์š”ํ•œ ํ‚ค๋ฅผ ์ฑ„์šฐ๋Š” ์—ญํ• ์„ ํ•œ๋‹ค. + +ํšจ๊ณผ: +- Lua๊ฐ€ ํ•„์š”ํ•œ ๊ธฐ์ค€๊ฐ’์„ ํ•ญ์ƒ Redis์—์„œ ์ฝ์„ ์ˆ˜ ์žˆ์Œ +- ์›”์ดˆ ์ฒซ ์ด๋ฒคํŠธ๋‚˜ ์บ์‹œ miss ์ƒํ™ฉ์—์„œ๋„ ๋™์ผํ•œ ์ฒ˜๋ฆฌ ๊ฒฝ๋กœ ์œ ์ง€ + +### 3.2 Lua Decision + +Lua๋Š” ์•„๋ž˜๋ฅผ ์›์ž์ ์œผ๋กœ ์ˆ˜ํ–‰ํ•œ๋‹ค. + +- duplicate ์—ฌ๋ถ€ ํ™•์ธ +- ์‚ฌ์šฉ๋Ÿ‰ ๋ฐ˜์˜ +- ์ฐจ๋‹จ ์—ฌ๋ถ€ ํŒ๋‹จ +- ๊ฒฝ๊ณ  ์ƒํƒœ ํŒ๋‹จ +- ์•Œ๋ฆผ dedup ํŒ๋‹จ +- ๊ฒฐ๊ณผ ์บ์‹œ ์ €์žฅ + +๋Œ€ํ‘œ ๊ฒฐ๊ณผ: +- `duplicate` +- `status` +- `shouldNotify` +- `totalUsed` +- `remaining` +- `monthlyUsed` +- `userRatio` +- `monthlyLimit` + +์™œ Lua๋ฅผ ์“ฐ๋Š”๊ฐ€: +- Redis read/write์™€ ์ƒํƒœ ๊ณ„์‚ฐ์„ ํ•œ ๋ฒˆ์— ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. +- ์‚ฌ์šฉ๋Ÿ‰ ์ฆ๊ฐ€์™€ ์ƒํƒœ ํŒ๋‹จ์„ ๋ถ„๋ฆฌํ•˜๋ฉด race condition์ด ์ƒ๊ธธ ์ˆ˜ ์žˆ๋‹ค. +- Lua๋Š” Redis ๋‚ด๋ถ€์—์„œ ์›์ž์ ์œผ๋กœ ์‹คํ–‰๋˜๋ฏ€๋กœ ๊ฐ™์€ ์ด๋ฒคํŠธ๊ฐ€ ๋ชฐ๋ ค๋„ ์ผ๊ด€๋œ ๊ฒฐ๊ณผ๋ฅผ ๋งŒ๋“ ๋‹ค. + +## 4. Java Decision + +Java๋Š” Lua status๋ฅผ ์ค‘์•™ ๋งคํผ์—์„œ ํ•ด์„ํ•œ๋‹ค. + +์ง€์›ํ•˜๋Š” status: +- `NORMAL` +- `WARNING_50` +- `WARNING_30` +- `WARNING_10` +- `MANUAL` +- `APP_BLOCK` +- `TIME_BLOCK` +- `MONTHLY_LIMIT_EXCEEDED` +- `FAMILY_QUOTA_EXCEEDED` + +์ด ๋‹จ๊ณ„์—์„œ ์•„๋ž˜๊ฐ€ ์ •ํ•ด์ง„๋‹ค. + +- DB persist result +- notification ๋Œ€์ƒ ์—ฌ๋ถ€ +- notification payload ์˜๋ฏธ + +์ฆ‰ Lua๋Š” ์ƒํƒœ ๋ฌธ์ž์—ด์„ ๋ฐ˜ํ™˜ํ•˜๊ณ , Java๋Š” ๊ทธ ๋ฌธ์ž์—ด์„ ํ›„์† ์ฒ˜๋ฆฌ ๊ทœ์น™์œผ๋กœ ๋ฐ”๊พธ๋Š” ์—ญํ• ์„ ๋งก๋Š”๋‹ค. + +## 5. Direct DB Settlement + +DB ์ •์‚ฐ์€ usage ์„œ๋น„์Šค๊ฐ€ ์ง์ ‘ ์ˆ˜ํ–‰ํ•œ๋‹ค. + +### 5.1 allowed ์ด๋ฒคํŠธ +1. `usage_record` insert ์‹œ๋„ +2. ์ƒˆ insert ์„ฑ๊ณต ์‹œ์—๋งŒ + - `customer_quota` + - `family_quota` + ๋ฐ˜์˜ + +### 5.2 blocked ์ด๋ฒคํŠธ +- `usage_record`๋Š” ๋งŒ๋“ค์ง€ ์•Š์Œ +- `customer_quota` ์ฐจ๋‹จ ์ƒํƒœ ๋ฐ˜์˜ + +allowed์™€ blocked๋ฅผ ๋‚˜๋ˆ„๋Š” ์ด์œ ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค. + +- allowed ์ด๋ฒคํŠธ๋Š” ์‹ค์ œ ์‚ฌ์šฉ๋Ÿ‰ ์ฆ๊ฐ€๋ฅผ ์˜์†ํ™”ํ•ด์•ผ ํ•œ๋‹ค. +- blocked ์ด๋ฒคํŠธ๋Š” ์‚ฌ์šฉ์ด ํ—ˆ์šฉ๋˜์ง€ ์•Š์•˜์œผ๋ฏ€๋กœ ์‚ฌ์šฉ๋Ÿ‰ row๋ฅผ ๋งŒ๋“ค์ง€ ์•Š๊ณ  ์ฐจ๋‹จ ์ƒํƒœ๋งŒ ๋ฐ˜์˜ํ•œ๋‹ค. + +๊ฐ™์€ ์ด๋ฒคํŠธ๊ฐ€ ๋‹ค์‹œ ๋“ค์–ด์™€๋„ `usage_record`๊ฐ€ ๋ฉฑ๋“ฑ ๊ฐ€๋“œ ์—ญํ• ์„ ํ•œ๋‹ค. + +## 6. Notification Flow + +notification์€ ์•„๋ž˜ ์กฐ๊ฑด์„ ๋ชจ๋‘ ๋งŒ์กฑํ•  ๋•Œ๋งŒ ๋Œ€์ƒ์ด๋‹ค. + +- status ๊ธฐ์ค€์œผ๋กœ ์•Œ๋ฆผ ๋Œ€์ƒ +- Lua ๊ฒฐ๊ณผ ๊ธฐ์ค€ `shouldNotify = true` + +notification ๋Œ€์ƒ์ด๋ฉด: +1. Outbox์— `PUBLISH_PENDING` ์ €์žฅ +2. `NotificationPayload`๋ฅผ ์ง๋ ฌํ™”ํ•ด `payload_json`์— ์ €์žฅ +3. usage ์„œ๋น„์Šค๊ฐ€ ์ฆ‰์‹œ ๋น„๋™๊ธฐ๋กœ ๋ฐœํ–‰ ์‹œ๋„ +4. ์„ฑ๊ณตํ•˜๋ฉด `SENT` +5. ์‹คํŒจํ•˜๋ฉด `PUBLISH_PENDING` ์œ ์ง€ + +notification ๋น„๋Œ€์ƒ์ด๋ฉด ์ƒˆ Outbox row๋Š” ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค. + +์ด ๊ตฌ์กฐ์˜ ์˜๋ฏธ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค. + +- ์ฆ‰์‹œ์„ฑ์€ usage ์„œ๋น„์Šค๊ฐ€ ๋‹ด๋‹นํ•œ๋‹ค. +- ๋ฐœํ–‰ ์‹คํŒจ ํ›„ ๋ณต๊ตฌ ๊ธฐ์ค€์ ์€ Outbox๊ฐ€ ๋‹ด๋‹นํ•œ๋‹ค. +- notification ๋Œ€์ƒ์ด ์•„๋‹Œ ์ด๋ฒคํŠธ๋Š” ๋ถˆํ•„์š”ํ•œ DB row๋ฅผ ๋‚จ๊ธฐ์ง€ ์•Š๋Š”๋‹ค. + +## 7. Processing Sequence + +```mermaid +sequenceDiagram + participant K as Kafka usage-events + participant U as Usage Service + participant M as Membership Cache + participant R as Redis/Lua + participant D as DB + participant O as Outbox + participant N as Kafka notification-events + + K->>U: usage-events 1๊ฑด + U->>U: payload ๊ฒ€์ฆ + U->>M: family-customer ๊ฒ€์ฆ + U->>R: warmup + Lua ์‹คํ–‰ + R-->>U: duplicate, status, shouldNotify + U->>D: DB ์ง์ ‘ ์ •์‚ฐ + alt shouldNotify = true + U->>O: PUBLISH_PENDING ์ €์žฅ + U->>N: ๋น„๋™๊ธฐ ๋ฐœํ–‰ ์‹œ๋„ + alt ack success + U->>O: SENT ๋ฐ˜์˜ + else ack fail + U->>O: PUBLISH_PENDING ์œ ์ง€ + end + else shouldNotify = false + U->>U: Outbox row ์ƒ์„ฑ ์—†์Œ + end +``` + +## 8. Why This Flow Matters + +์ด ํ๋ฆ„์—์„œ ์ค‘์š”ํ•œ ์ ์€ ์•„๋ž˜ ์„ธ ๊ฐ€์ง€๋‹ค. + +1. ์‹ค์‹œ๊ฐ„ ํŒ๋‹จ๊ณผ ์˜์† ์ •์‚ฐ์ด ๋ถ„๋ฆฌ๋˜์–ด ์žˆ๋‹ค. +- Redis/Lua๋Š” ๋น ๋ฅธ ํŒ๋‹จ +- DB๋Š” ์ตœ์ข… ์ •์‚ฐ + +2. ๋ฉฑ๋“ฑ์„ฑ์ด ๊ตฌ์กฐ ์•ˆ์— ๋“ค์–ด๊ฐ€ ์žˆ๋‹ค. +- Redis dedup +- `usage_record` ๊ธฐ๋ฐ˜ DB ๋ฉฑ๋“ฑ ์ •์‚ฐ + +3. notification์€ ์ฆ‰์‹œ์„ฑ๊ณผ ๋ณต๊ตฌ์„ฑ์„ ๋™์‹œ์— ๊ฐ€์ง„๋‹ค. +- ์ฆ‰์‹œ ๋น„๋™๊ธฐ ๋ฐœํ–‰ +- Outbox ๊ธฐ๋ฐ˜ ํ›„์† ๋ณต๊ตฌ diff --git a/docs/03_OUTBOX_AND_RETRY.md b/docs/03_OUTBOX_AND_RETRY.md new file mode 100644 index 0000000..4987e47 --- /dev/null +++ b/docs/03_OUTBOX_AND_RETRY.md @@ -0,0 +1,169 @@ +๏ปฟ# Outbox and Retry + +## 1. Outbox Role + +`usage_event_outbox`๋Š” notification ๋ฐœํ–‰ ์ƒํƒœ๋ฅผ ๋‚จ๊ธฐ๊ธฐ ์œ„ํ•œ ํ…Œ์ด๋ธ”์ด๋‹ค. + +ํ˜„์žฌ ๊ตฌ์กฐ์˜ ํ•ต์‹ฌ์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค. + +- ๋ชจ๋“  usage ์ด๋ฒคํŠธ๋ฅผ ์ €์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค. +- notification ๋Œ€์ƒ ์ด๋ฒคํŠธ๋งŒ ์ €์žฅํ•œ๋‹ค. +- ์ €์žฅ๋˜๋Š” payload๋Š” `EventEnvelope`๊ฐ€ ์•„๋‹ˆ๋ผ `NotificationPayload` JSON์ด๋‹ค. +- ์ฆ‰์‹œ ๋ฐœํ–‰ ์‹คํŒจ ์‹œ `PUBLISH_PENDING`์ด ๋‚จ๋Š”๋‹ค. + +์ฆ‰ ์ด ํ…Œ์ด๋ธ”์€ usage ์ด๋ฒคํŠธ ์ „์ฒด ๋กœ๊ทธ๊ฐ€ ์•„๋‹ˆ๋ผ, notification ๋ฐœํ–‰๊ณผ ๋ณต๊ตฌ๋ฅผ ์œ„ํ•œ ์ƒํƒœ ์ €์žฅ์†Œ๋‹ค. + +## 2. Why the Outbox Shape Changed + +์ดˆ๊ธฐ์—๋Š” usage ์ฒ˜๋ฆฌ ์ „์ฒด๋ฅผ ๋” ๋„“๊ฒŒ ์ถ”์ ํ•˜๋Š” ๋ฐฉํ–ฅ๋„ ๊ณ ๋ คํ•  ์ˆ˜ ์žˆ์—ˆ์ง€๋งŒ, ํ˜„์žฌ ๊ตฌ์กฐ๋Š” notification ๋Œ€์ƒ ์ด๋ฒคํŠธ๋งŒ ์ €์žฅํ•˜๋Š” ๋ฐฉํ–ฅ์œผ๋กœ ์ •๋ฆฌ๋˜์—ˆ๋‹ค. + +์ด๋ ‡๊ฒŒ ์ •๋ฆฌํ•œ ์ด์œ ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค. + +- usage ๋„๋ฉ”์ธ์—์„œ๋Š” notification ๋น„๋Œ€์ƒ ์ด๋ฒคํŠธ ๋น„์œจ์ด ๋†’์„ ์ˆ˜ ์žˆ๋‹ค. +- ๊ณ TPS ํ™˜๊ฒฝ์—์„œ๋Š” ๋ชจ๋“  ์ด๋ฒคํŠธ๋ฅผ DB row๋กœ ๋‚จ๊ธฐ๋Š” ๋ฐฉ์‹์ด ๋ถ€๋‹ด์ด ์ปค์ง„๋‹ค. +- ์‹ค์ œ๋กœ ๋ณต๊ตฌ๊ฐ€ ํ•„์š”ํ•œ ์ง€์ ์€ notification ๋ฐœํ–‰ ์‹คํŒจ ๊ตฌ๊ฐ„์ด๋‹ค. + +๊ทธ๋ž˜์„œ ํ˜„์žฌ Outbox๋Š” ์•„๋ž˜ ์—ญํ• ์— ์ง‘์ค‘ํ•œ๋‹ค. + +- notification ๋Œ€์ƒ ํ™•์ • +- ์ฆ‰์‹œ ๋ฐœํ–‰ ํ›„ ์ƒํƒœ ์ถ”์  +- ํ›„์† ๋ณต๊ตฌ ํ”„๋กœ์„ธ์Šค ์ธ๊ณ„ + +## 3. Stored Data + +์ฃผ์š” ์ปฌ๋Ÿผ ์˜๋ฏธ: +- `event_id`: ์›๋ณธ usage ์ด๋ฒคํŠธ ์‹๋ณ„์ž +- `family_id`: ๊ฐ€์กฑ ID +- `customer_id`: trigger ์‚ฌ์šฉ์ž ID +- `status`: `PUBLISH_PENDING`, `SENT`, `FAILED` +- `payload_json`: ์ตœ์ข… `NotificationPayload` JSON +- `retry_count`, `next_retry_at`, `last_error`: ๋ณต๊ตฌ ํ”„๋กœ์„ธ์Šค ์ƒํƒœ ๊ด€๋ฆฌ์šฉ + +์—ฌ๊ธฐ์„œ `event_id`๋Š” notification ์ด๋ฒคํŠธ id๊ฐ€ ์•„๋‹ˆ๋ผ, ์›๋ณธ usage ์ด๋ฒคํŠธ id๋‹ค. +์ฆ‰ ์ด row๊ฐ€ ์–ด๋–ค usage ์ด๋ฒคํŠธ์—์„œ ํŒŒ์ƒ๋๋Š”์ง€๋ฅผ ์ถ”์ ํ•˜๋Š” ํ‚ค๋‹ค. + +## 4. Payload Shape + +Outbox์—๋Š” `EventEnvelope`๊ฐ€ ์•„๋‹ˆ๋ผ ์ตœ์ข… `NotificationPayload` JSON์„ ์ €์žฅํ•œ๋‹ค. + +์ด๋ ‡๊ฒŒ ํ•œ ์ด์œ ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค. + +- ์ฆ‰์‹œ ๋ฐœํ–‰๊ณผ ๋ณต๊ตฌ ๋ฐœํ–‰์ด ๊ฐ™์€ payload๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด์„œ +- ๋ณต๊ตฌ ํ”„๋กœ์„ธ์Šค๊ฐ€ payload๋ฅผ ๋‹ค์‹œ ์กฐ๋ฆฝํ•˜์ง€ ์•Š๊ณ  ๋ฐ”๋กœ ์žฌ๋ฐœํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด์„œ +- notification์˜ business payload์™€ Kafka envelope ์ƒ์„ฑ์„ ๋ถ„๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด์„œ + +์ฆ‰ Outbox๋Š” โ€œ๋ฌด์—‡์„ ๋ณด๋‚ผ ๊ฒƒ์ธ๊ฐ€โ€๋ฅผ ์ €์žฅํ•˜๊ณ , ์‹ค์ œ envelope ์ƒ์„ฑ์€ ๋ฐœํ–‰ ์‹œ์ ์— ์ˆ˜ํ–‰ํ•œ๋‹ค. + +## 5. Outbox Status + +### 5.1 PUBLISH_PENDING +- notification ๋ฐœํ–‰ ๋Œ€์ƒ ํ™•์ • ์™„๋ฃŒ +- ์•„์ง ์„ฑ๊ณต์ ์œผ๋กœ ๋งˆ๊ฐ๋˜์ง€ ์•Š์€ ์ƒํƒœ + +์ด ์ƒํƒœ๋Š” ์•„๋ž˜ ์ƒํ™ฉ์„ ํฌํ•จํ•œ๋‹ค. +- ์ฆ‰์‹œ ๋ฐœํ–‰ ์ „ +- ์ฆ‰์‹œ ๋ฐœํ–‰ ์‹คํŒจ ํ›„ +- ํ›„์† ๋ณต๊ตฌ ํ”„๋กœ์„ธ์Šค ๋Œ€๊ธฐ ์ค‘ + +### 5.2 SENT +- notification Kafka publish ์„ฑ๊ณต์ด ๋ฐ˜์˜๋œ ์ƒํƒœ + +### 5.3 FAILED +- ๋ณต๊ตฌ ํ”„๋กœ์„ธ์Šค๊ฐ€ ์ตœ์ข… ์‹คํŒจ๋กœ ๋งˆ๊ฐํ•œ ์ƒํƒœ + +## 6. Immediate Publish Policy + +usage ์„œ๋น„์Šค๋Š” notification์„ ์ฆ‰์‹œ ๋น„๋™๊ธฐ๋กœ ๋จผ์ € ๋ฐœํ–‰ํ•œ๋‹ค. + +- ์„ฑ๊ณต callback์ด๋ฉด `SENT` +- ์‹คํŒจ callback์ด๋ฉด `PUBLISH_PENDING` ์œ ์ง€ + +์ฆ‰ usage ์„œ๋น„์Šค๋Š” ์ฆ‰์‹œ์„ฑ์„ ๋‹ด๋‹นํ•˜๊ณ , ๋ณต๊ตฌ ํ”„๋กœ์„ธ์Šค๋Š” pending row๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํ›„์† ๋ฐœํ–‰์„ ์ˆ˜ํ–‰ํ•œ๋‹ค. + +์ด ๊ตฌ์กฐ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด: +- ์ •์ƒ ์ผ€์ด์Šค์—์„œ๋Š” ์ฆ‰์‹œ ์•Œ๋ฆผ ์ „ํŒŒ +- ์‹คํŒจ ์ผ€์ด์Šค์—์„œ๋Š” row ๊ธฐ์ค€ ๋ณต๊ตฌ +๊ฐ€ ๊ฐ€๋Šฅํ•ด์ง„๋‹ค. + +## 7. Retry Layers + +### 7.1 Consumer Retry + +lib-kafka ๋ถ„๋ฅ˜ ๊ธฐ์ค€: +- `IllegalArgumentException` -> `IGNORE` +- `KafkaMessageProcessingException` -> `RETRY` +- `NonRetryableKafkaMessageProcessingException` -> `DLQ` + +ํ˜„์žฌ ์ด ๋ ˆํฌ๋Š” consumer retry ์„ค์ •์„ ๋ณ„๋„๋กœ overrideํ•˜์ง€ ์•Š๋Š”๋‹ค. + +์ฆ‰ usage ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋„์ค‘ ๋ฐœ์ƒํ•œ retryable ์˜ˆ์™ธ๋Š” consumer ๋ ˆ๋ฒจ์—์„œ ๋‹ค์‹œ ์ฒ˜๋ฆฌ๋œ๋‹ค. + +### 7.2 Service Re-entry + +๊ฐ™์€ `eventId`๊ฐ€ ๋‹ค์‹œ ๋“ค์–ด์˜ค๋ฉด ์•„๋ž˜๊ฐ€ ๋‹ค์‹œ ์ˆ˜ํ–‰๋  ์ˆ˜ ์žˆ๋‹ค. + +- Redis warmup +- Lua ์‹คํ–‰ +- DB ์ •์‚ฐ +- pending notification ์žฌ๋ฐœํ–‰ ์‹œ๋„ + +์ด ๋ ˆ์ด์–ด๋Š” duplicate์™€ retry๋ฅผ ์—„๊ฒฉํžˆ ๊ตฌ๋ถ„ํ•˜๊ธฐ๋ณด๋‹ค, ๊ฐ™์€ ์ด๋ฒคํŠธ ์žฌ์ง„์ž…์„ ์•ˆ์ „ํ•˜๊ฒŒ ํก์ˆ˜ํ•˜๋Š” ์—ญํ• ์„ ํ•œ๋‹ค. + +### 7.3 External Recovery Process + +๋ณต๊ตฌ ํ”„๋กœ์„ธ์Šค๋Š” `PUBLISH_PENDING`์„ ๋Œ€์ƒ์œผ๋กœ ์•„๋ž˜ ์ˆœ์„œ๋กœ ๋™์ž‘ํ•œ๋‹ค. + +1. `PUBLISH_PENDING` ์กฐํšŒ +2. `payload_json` ์—ญ์ง๋ ฌํ™” +3. `notification-events` ์žฌ๋ฐœํ–‰ +4. ์„ฑ๊ณต ์‹œ `SENT` ๋ฐ˜์˜ +5. ํ•„์š” ์‹œ `retry_count`, `next_retry_at`, `last_error` ๊ฐฑ์‹  +6. ์ตœ์ข… ์‹คํŒจ ์‹œ `FAILED` ๋ฐ˜์˜ + +์ฆ‰ usage ์„œ๋น„์Šค๋Š” ์ฆ‰์‹œ ๋ฐœํ–‰๊นŒ์ง€, ๋ณต๊ตฌ ํ”„๋กœ์„ธ์Šค๋Š” pending row ํ›„์† ๋ฐœํ–‰๊นŒ์ง€ ๋‹ด๋‹นํ•œ๋‹ค. + +## 8. Failure Handling Summary + +| ์œ ํ˜• | ์ฒ˜๋ฆฌ ๋ฐฉ์‹ | +|---|---| +| invalid payload / ์ž˜๋ชป๋œ family-customer | `IGNORE` | +| membership lookup, Redis warmup, Lua, DB ์ •์‚ฐ, Outbox ์ €์žฅ ์‹คํŒจ | `RETRY` | +| ์ƒํƒœ ๊ณ„์•ฝ ๋ถˆ์ผ์น˜ / ๋ณต๊ตฌ ๋ถˆ๊ฐ€ ์ง๋ ฌํ™” ์˜ค๋ฅ˜ | `DLQ` | +| ์ฆ‰์‹œ notification ๋ฐœํ–‰ ์‹คํŒจ | `PUBLISH_PENDING` ์œ ์ง€ | + +## 9. Outbox Sequence + +```mermaid +sequenceDiagram + participant U as Usage Service + participant O as Outbox + participant N as Kafka notification-events + participant B as External Recovery Process + + U->>O: PUBLISH_PENDING ์ €์žฅ + U->>N: ์ฆ‰์‹œ ๋น„๋™๊ธฐ ๋ฐœํ–‰ + alt ack success + U->>O: SENT ๋ฐ˜์˜ + else ack fail + U->>O: PUBLISH_PENDING ์œ ์ง€ + B->>O: PUBLISH_PENDING ์กฐํšŒ + B->>N: ์žฌ๋ฐœํ–‰ + B->>O: SENT ๋˜๋Š” FAILED ๋ฐ˜์˜ + end +``` + +## 10. Why This Design Matters + +ํ˜„์žฌ Outbox ์„ค๊ณ„์˜ ํ•ต์‹ฌ์€ ์•„๋ž˜ ์„ธ ๊ฐ€์ง€๋‹ค. + +1. notification ๋Œ€์ƒ ์ด๋ฒคํŠธ๋งŒ ์ €์žฅํ•œ๋‹ค. +- ๋ถˆํ•„์š”ํ•œ row๋ฅผ ์ค„์ธ๋‹ค. +- ๊ณ TPS ํ™˜๊ฒฝ์—์„œ DB hot path ๋น„์šฉ์„ ์ค„์ธ๋‹ค. + +2. payload๋ฅผ ์ตœ์ข… notification ํ˜•ํƒœ๋กœ ์ €์žฅํ•œ๋‹ค. +- ์ฆ‰์‹œ ๋ฐœํ–‰๊ณผ ๋ณต๊ตฌ ๋ฐœํ–‰์ด ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. +- ๋ณต๊ตฌ ํ”„๋กœ์„ธ์Šค๊ฐ€ ๋‹จ์ˆœํ•ด์ง„๋‹ค. + +3. ์ฆ‰์‹œ์„ฑ๊ณผ ๋ณต๊ตฌ์„ฑ์„ ๋ถ„๋ฆฌํ•œ๋‹ค. +- usage ์„œ๋น„์Šค๋Š” ์ฆ‰์‹œ ๋ฐœํ–‰์„ ๋‹ด๋‹น +- Outbox์™€ ๋ณต๊ตฌ ํ”„๋กœ์„ธ์Šค๋Š” ์‹คํŒจ ํ›„ ํ›„์† ๋ฐœํ–‰์„ ๋‹ด๋‹น diff --git a/docs/04_EXCEPTION_AND_RECOVERY.md b/docs/04_EXCEPTION_AND_RECOVERY.md new file mode 100644 index 0000000..ed34470 --- /dev/null +++ b/docs/04_EXCEPTION_AND_RECOVERY.md @@ -0,0 +1,163 @@ +๏ปฟ# Exception and Recovery Guide + +## 1. Purpose + +์ด ๋ฌธ์„œ๋Š” `usage-events` ์ฒ˜๋ฆฌ ๊ฐ ๋‹จ๊ณ„์—์„œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ํ˜„์žฌ ๊ตฌ์กฐ๊ฐ€ ์–ด๋–ค ๋ฐฉ์‹์œผ๋กœ ๋ณต๊ตฌ์™€ ์ •ํ•ฉ์„ฑ์„ ์œ ์ง€ํ•˜๋Š”์ง€ ์ •๋ฆฌํ•œ๋‹ค. + +ํ•ต์‹ฌ์€ ์•„๋ž˜ ์„ธ ๊ฐ€์ง€๋‹ค. + +- ์ž˜๋ชป๋œ ์ž…๋ ฅ์€ ์ดˆ์ž…์—์„œ ์ฐจ๋‹จํ•œ๋‹ค. +- retryable ์˜ˆ์™ธ๋Š” ๊ฐ™์€ Kafka ๋ ˆ์ฝ”๋“œ๋ฅผ ๋‹ค์‹œ ์ฒ˜๋ฆฌํ•œ๋‹ค. +- duplicate์™€ ์žฌ์ฒ˜๋ฆฌ๋ฅผ ๊ตฌ์กฐ์ ์œผ๋กœ ํก์ˆ˜ํ•œ๋‹ค. + +## 2. Step-by-Step Recovery + +| ๋‹จ๊ณ„ | ์‹คํŒจ ์œ ํ˜• | ์ฒ˜๋ฆฌ ๋ฐฉ์‹ | ๋ณต๊ตฌ ์ „๋žต | +|---|---|---|---| +| Payload Validation | payload null, ์Œ์ˆ˜/0 ๊ฐ’ | `IllegalArgumentException` | ์ž˜๋ชป๋œ ์ž…๋ ฅ์œผ๋กœ ์ข…๋ฃŒ | +| Family-Customer Validation | ์‹ค์ œ ์†Œ์† ๋ถˆ์ผ์น˜ | `IllegalArgumentException` | ์ž˜๋ชป๋œ ์ž…๋ ฅ์œผ๋กœ ์ข…๋ฃŒ | +| Family-Customer Validation | Redis/DB ์กฐํšŒ ์‹คํŒจ | `KafkaMessageProcessingException` | consumer ์žฌ์ฒ˜๋ฆฌ | +| Redis Warmup | info/remaining/monthly usage warmup ์‹คํŒจ | `KafkaMessageProcessingException` | consumer ์žฌ์ฒ˜๋ฆฌ | +| Lua Execution | null ๊ฒฐ๊ณผ, invalid ๋ฐฐ์—ด | `KafkaMessageProcessingException` | consumer ์žฌ์ฒ˜๋ฆฌ | +| Lua Status Mapping | ์ง€์›ํ•˜์ง€ ์•Š๋Š” status | `NonRetryableKafkaMessageProcessingException` | DLQ | +| DB Settlement | `usage_record`, quota ๋ฐ˜์˜ ์‹คํŒจ | ์˜ˆ์™ธ ์ „ํŒŒ | ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ + consumer ์žฌ์ฒ˜๋ฆฌ | +| Outbox Serialization | payload JSON ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™” ์‹คํŒจ | `NonRetryableKafkaMessageProcessingException` | DLQ | +| Outbox Save | `PUBLISH_PENDING` ์ €์žฅ ์‹คํŒจ | ์˜ˆ์™ธ ์ „ํŒŒ | consumer ์žฌ์ฒ˜๋ฆฌ | +| Immediate Publish | Kafka publish ack ์‹คํŒจ | ์˜ˆ์™ธ ์ฝœ๋ฐฑ ์ฒ˜๋ฆฌ | `PUBLISH_PENDING` ์œ ์ง€ + ๋ณต๊ตฌ ํ”„๋กœ์„ธ์Šค ์žฌ๋ฐœํ–‰ | + +## 3. Why Each Recovery Strategy Exists + +### 3.1 Validation ๋‹จ๊ณ„๋Š” ์žฌ์‹œ๋„๋ณด๋‹ค ๊ฒฉ๋ฆฌ๊ฐ€ ์ค‘์š”ํ•˜๋‹ค + +payload ๊ณ„์•ฝ ์œ„๋ฐ˜์ด๋‚˜ family-customer ๋ถˆ์ผ์น˜๋Š” ๋‹ค์‹œ ์ฒ˜๋ฆฌํ•ด๋„ ๋ฐ”๋€Œ์ง€ ์•Š๋Š”๋‹ค. + +๊ทธ๋ž˜์„œ ์ด ๋‹จ๊ณ„์˜ ๋ชฉํ‘œ๋Š”: +- ๋นจ๋ฆฌ ์‹คํŒจ์‹œํ‚ค๊ณ  +- ๋’ค ๋‹จ๊ณ„๋ฅผ ์˜ค์—ผ์‹œํ‚ค์ง€ ์•Š๊ณ  +- ๋ถˆํ•„์š”ํ•œ ์žฌ์‹œ๋„๋ฅผ ๋งŒ๋“ค์ง€ ์•Š๋Š” ๊ฒƒ +์ด๋‹ค. + +### 3.2 Redis/Lua/DB ๋‹จ๊ณ„๋Š” ๋‹ค์‹œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค + +์ด ๋‹จ๊ณ„๋“ค์€ ์ผ์‹œ์ ์ธ ์ธํ”„๋ผ ๋ฌธ์ œ๋‚˜ ํƒ€์ด๋ฐ ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค. + +๊ทธ๋ž˜์„œ ์ด ๋‹จ๊ณ„์˜ ๋ชฉํ‘œ๋Š”: +- retryable ์˜ˆ์™ธ๋กœ ๋ถ„๋ฅ˜ํ•˜๊ณ  +- ๊ฐ™์€ usage ์ด๋ฒคํŠธ๋ฅผ ๋‹ค์‹œ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ํ•˜๊ณ  +- ๊ฐ™์€ ์ด๋ฒคํŠธ ์žฌ์ง„์ž… ์‹œ์—๋„ ๊ฒฐ๊ณผ๊ฐ€ ๊นจ์ง€์ง€ ์•Š๊ฒŒ ํ•˜๋Š” ๊ฒƒ +์ด๋‹ค. + +### 3.3 notification ๋‹จ๊ณ„๋Š” ๋ณต๊ตฌ ๊ธฐ์ค€์ ์ด ํ•„์š”ํ•˜๋‹ค + +์ฆ‰์‹œ ๋ฐœํ–‰์€ ์‹คํŒจํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ž˜์„œ usage ์„œ๋น„์Šค๋Š” ๋ฐœํ–‰ ๋Œ€์ƒ์ผ ๋•Œ Outbox์— `PUBLISH_PENDING`์„ ๋‚จ๊ธด๋‹ค. + +์ด ๋‹จ๊ณ„์˜ ๋ชฉํ‘œ๋Š”: +- ์ •์ƒ ์ผ€์ด์Šค์—์„œ๋Š” ๋ฐ”๋กœ ๋ณด๋‚ด๊ณ  +- ์‹คํŒจ ์ผ€์ด์Šค์—์„œ๋Š” pending row๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋‹ค์‹œ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š” ๊ฒƒ +์ด๋‹ค. + +## 4. Loss Prevention Strategy + +### 4.1 Invalid Input Isolation + +์ž˜๋ชป๋œ payload์™€ ์ž˜๋ชป๋œ family-customer ์กฐํ•ฉ์€ ์ดˆ์ž…์—์„œ ์ฐจ๋‹จํ•œ๋‹ค. + +ํšจ๊ณผ: +- Redis ์˜ค์—ผ ๋ฐฉ์ง€ +- DB ์˜ค์—ผ ๋ฐฉ์ง€ +- ๋ถˆํ•„์š”ํ•œ notification ๋ฐฉ์ง€ + +### 4.2 Redis Re-apply Prevention + +Lua๋Š” `event:dedup:usage:{eventId}` ํ‚ค๋ฅผ ์‚ฌ์šฉํ•ด ๊ฐ™์€ ์ด๋ฒคํŠธ๊ฐ€ ๋‹ค์‹œ ๋“ค์–ด์™€๋„ Redis ์ฆ๊ฐ์„ ๋‹ค์‹œ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š๋Š”๋‹ค. + +ํšจ๊ณผ: +- consumer ์žฌ์ฒ˜๋ฆฌ ์‹œ Redis ์ค‘๋ณต ๋ฐ˜์˜ ๋ฐฉ์ง€ +- duplicate์™€ retry๊ฐ€ ๊ฐ™์€ ๊ฒฝ๋กœ๋กœ ๋“ค์–ด์™€๋„ Redis ์ƒํƒœ ์•ˆ์ •ํ™” + +### 4.3 DB Idempotent Settlement + +allowed ์ด๋ฒคํŠธ๋Š” `usage_record` insert๊ฐ€ ๋จผ์ € ์ˆ˜ํ–‰๋˜๊ณ , ์ƒˆ insert ์„ฑ๊ณต ์‹œ์—๋งŒ quota๋ฅผ ๋ฐ˜์˜ํ•œ๋‹ค. + +ํšจ๊ณผ: +- ๊ฐ™์€ `eventId` ์žฌ์ฒ˜๋ฆฌ ์‹œ `usage_record`๊ฐ€ ๋ฉฑ๋“ฑ ๊ฐ€๋“œ ์—ญํ•  +- `customer_quota`, `family_quota` ์ค‘๋ณต ๋ฐ˜์˜ ๋ฐฉ์ง€ + +### 4.4 Notification Recovery Point + +notification ๋Œ€์ƒ ์ด๋ฒคํŠธ๋Š” Outbox์— `PUBLISH_PENDING`์„ ์ €์žฅํ•œ ๋’ค ์ฆ‰์‹œ ๋น„๋™๊ธฐ ๋ฐœํ–‰์„ ์‹œ๋„ํ•œ๋‹ค. + +ํšจ๊ณผ: +- ์ฆ‰์‹œ ๋ฐœํ–‰ ์„ฑ๊ณต ์‹œ `SENT` ๋ฐ˜์˜ +- ์ฆ‰์‹œ ๋ฐœํ–‰ ์‹คํŒจ ์‹œ `PUBLISH_PENDING`์„ ๊ธฐ์ค€์œผ๋กœ ํ›„์† ๋ณต๊ตฌ ๊ฐ€๋Šฅ + +## 5. Recovery Sequence Examples + +### 5.1 Redis ๋ฐ˜์˜ ํ›„ DB ์ •์‚ฐ ์‹คํŒจ + +```mermaid +sequenceDiagram + participant U as Usage Service + participant R as Redis/Lua + participant D as DB + participant K as Kafka Retry + + U->>R: Lua ์‹คํ–‰ ๋ฐ Redis ๋ฐ˜์˜ + R-->>U: status ๋ฐ˜ํ™˜ + U->>D: DB ์ •์‚ฐ ์‹œ๋„ + D-->>U: ์˜ˆ์™ธ ๋ฐœ์ƒ + U-->>K: retryable ์˜ˆ์™ธ ์ „ํŒŒ + K->>U: ๊ฐ™์€ eventId ์žฌ์ฒ˜๋ฆฌ + U->>R: Lua ์žฌ์‹คํ–‰ + R-->>U: duplicate ๊ฒฐ๊ณผ ์žฌ์‚ฌ์šฉ + U->>D: DB ์ •์‚ฐ ์žฌ์ง„์ž… +``` + +์ด ์‹œ๋‚˜๋ฆฌ์˜ค์˜ ํ•ต์‹ฌ์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค. + +- Redis๋Š” ์ด๋ฏธ ๋ฐ˜์˜๋˜์—ˆ์ง€๋งŒ +- ๊ฐ™์€ ์ด๋ฒคํŠธ ์žฌ์ฒ˜๋ฆฌ ์‹œ Lua dedup cache๊ฐ€ Redis ์žฌ๋ฐ˜์˜์„ ๋ง‰๋Š”๋‹ค. +- DB ์ •์‚ฐ์€ ๋‹ค์‹œ ์ง„์ž…ํ•˜๊ณ , ์ตœ์ข…์ ์œผ๋กœ ์˜์† ์ƒํƒœ๊ฐ€ Redis ํŒ๋‹จ ๊ฒฐ๊ณผ๋ฅผ ๋”ฐ๋ผ๊ฐ„๋‹ค. + +### 5.2 Outbox ์ €์žฅ ํ›„ ์ฆ‰์‹œ ๋ฐœํ–‰ ์‹คํŒจ + +```mermaid +sequenceDiagram + participant U as Usage Service + participant O as Outbox + participant N as Kafka notification-events + participant B as Recovery Process + + U->>O: PUBLISH_PENDING ์ €์žฅ + U->>N: ์ฆ‰์‹œ ๋น„๋™๊ธฐ ๋ฐœํ–‰ + N-->>U: ack ์‹คํŒจ + U->>O: PUBLISH_PENDING ์œ ์ง€ + B->>O: pending row ์กฐํšŒ + B->>N: ์žฌ๋ฐœํ–‰ + B->>O: SENT ๋˜๋Š” FAILED ๋ฐ˜์˜ +``` + +์ด ์‹œ๋‚˜๋ฆฌ์˜ค์˜ ํ•ต์‹ฌ์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค. + +- ์ฆ‰์‹œ ๋ฐœํ–‰์€ ์‹คํŒจํ•  ์ˆ˜ ์žˆ๋‹ค. +- ํ•˜์ง€๋งŒ pending row๊ฐ€ ์ด๋ฏธ ๋‚จ์•„ ์žˆ์œผ๋ฏ€๋กœ ๋ณต๊ตฌ ๊ธฐ์ค€์ ์€ ์œ ์ง€๋œ๋‹ค. +- ํ›„์† ๋ณต๊ตฌ ํ”„๋กœ์„ธ์Šค๋Š” payload๋ฅผ ๋‹ค์‹œ ๋งŒ๋“ค์ง€ ์•Š๊ณ  row ๊ธฐ์ค€์œผ๋กœ ์žฌ๋ฐœํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค. + +## 6. Stage-by-Stage Guarantees + +| ๊ตฌ๊ฐ„ | ์ •ํ•ฉ์„ฑ ํฌ์ธํŠธ | +|---|---| +| Validation | ์ž˜๋ชป๋œ ์ž…๋ ฅ์€ ๋’ค ๋‹จ๊ณ„๋กœ ๋‚ด๋ ค๊ฐ€์ง€ ์•Š์Œ | +| Redis + Lua | duplicate ๊ฒฐ๊ณผ์™€ ์ƒํƒœ ๊ณ„์‚ฐ์„ ์›์ž์ ์œผ๋กœ ์ฒ˜๋ฆฌ | +| DB Settlement | `usage_record` ๊ธฐ๋ฐ˜ ๋ฉฑ๋“ฑ ์ •์‚ฐ | +| Notification | ๋Œ€์ƒ ์ด๋ฒคํŠธ๋งŒ Outbox ์ €์žฅ + ๋ฐœํ–‰ ์ƒํƒœ ๊ด€๋ฆฌ | + +## 7. Reading Guide + +์ด ๋ฌธ์„œ๋ฅผ ๋ณผ ๋•Œ๋Š” ์•„๋ž˜ ์ˆœ์„œ๋กœ ์ฝ์œผ๋ฉด ๋œ๋‹ค. + +1. ์–ด๋–ค ์‹คํŒจ๊ฐ€ retryable์ธ์ง€ ํ™•์ธํ•œ๋‹ค. +2. ๊ทธ ์‹คํŒจ๊ฐ€ Redis, DB, notification ์ค‘ ์–ด๋А ๊ณ„์ธต์—์„œ ์ผ์–ด๋‚˜๋Š”์ง€ ๋ณธ๋‹ค. +3. ์ดํ›„ ์žฌ์ฒ˜๋ฆฌ ๊ธฐ์ค€์ ์ด Redis dedup์ธ์ง€, DB ๋ฉฑ๋“ฑ์„ฑ์ธ์ง€, Outbox์ธ์ง€ ํ™•์ธํ•œ๋‹ค. + +์ฆ‰ ํ˜„์žฌ ๊ตฌ์กฐ๋Š” ์‹คํŒจ ์œ ํ˜•๋งˆ๋‹ค ๋ณต๊ตฌ ๊ธฐ์ค€์ ์„ ๋‹ค๋ฅด๊ฒŒ ๋‘๋Š” ๋ฐฉ์‹์œผ๋กœ ์ •๋ฆฌ๋˜์–ด ์žˆ๋‹ค. diff --git a/docs/05_DATA_MODEL_AND_KEYS.md b/docs/05_DATA_MODEL_AND_KEYS.md new file mode 100644 index 0000000..67eb95d --- /dev/null +++ b/docs/05_DATA_MODEL_AND_KEYS.md @@ -0,0 +1,170 @@ +๏ปฟ# Data Model and Redis Keys + +## 1. Purpose + +์ด ๋ฌธ์„œ๋Š” `dabom-processor-usage`๊ฐ€ ์‚ฌ์šฉํ•˜๋Š” ํ•ต์‹ฌ ์ €์žฅ ๊ตฌ์กฐ๋ฅผ ์ •๋ฆฌํ•œ๋‹ค. + +- MySQL ์˜์† ๋ฐ์ดํ„ฐ +- notification outbox payload +- Redis key ๊ตฌ์กฐ +- Lua dedup/cache ๊ตฌ์กฐ + +ํ•ต์‹ฌ์€ usage ์ด๋ฒคํŠธ 1๊ฑด์ด ์–ด๋–ค ์ €์žฅ ๊ตฌ์กฐ๋ฅผ ํ†ต๊ณผํ•˜๊ณ , ๊ทธ ๊ตฌ์กฐ๊ฐ€ ๊ฐ๊ฐ ์–ด๋–ค ์—ญํ• ์„ ๋งก๋Š”์ง€ ์ดํ•ดํ•˜๋Š” ๊ฒƒ์ด๋‹ค. + +## 2. MySQL Settlement Model + +### 2.1 usage_record + +allowed ์ด๋ฒคํŠธ์˜ ๋ฉฑ๋“ฑ ๊ฐ€๋“œ ์—ญํ• ์„ ํ•œ๋‹ค. + +- ๊ฐ™์€ `eventId`๊ฐ€ ๋‹ค์‹œ ๋“ค์–ด์˜ค๋ฉด ์ค‘๋ณต insert๋ฅผ ๋ง‰๋Š”๋‹ค. +- ์ƒˆ row๊ฐ€ ๋“ค์–ด๊ฐ„ ๊ฒฝ์šฐ์—๋งŒ quota ๋ฐ˜์˜์ด ์ด์–ด์ง„๋‹ค. + +์ฆ‰ `usage_record`๋Š” โ€œ์ด usage ์ด๋ฒคํŠธ๊ฐ€ ์‹ค์ œ ์‚ฌ์šฉ๋Ÿ‰ ์ฆ๊ฐ€๋กœ ์ •์‚ฐ๋˜์—ˆ๋Š”๊ฐ€โ€๋ฅผ ๊ณ ์ •ํ•˜๋Š” ๊ธฐ์ค€์ ์ด๋‹ค. + +### 2.2 customer_quota + +๊ณ ๊ฐ ๊ธฐ์ค€ ์›” ์‚ฌ์šฉ๋Ÿ‰๊ณผ ์ฐจ๋‹จ ์ƒํƒœ๋ฅผ ๋ฐ˜์˜ํ•œ๋‹ค. + +- allowed ์ด๋ฒคํŠธ: ์›” ์‚ฌ์šฉ๋Ÿ‰ ์ฆ๊ฐ€ +- blocked ์ด๋ฒคํŠธ: ์ฐจ๋‹จ ์ƒํƒœ ๋ฐ˜์˜ + +์ฆ‰ customer ๋‹จ์œ„ ์ •์ฑ…๊ณผ ์›” ์‚ฌ์šฉ๋Ÿ‰์„ ์˜์† ๊ธฐ์ค€์œผ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๊ตฌ์กฐ๋‹ค. + +### 2.3 family_quota + +๊ฐ€์กฑ ๊ธฐ์ค€ ์›” ์‚ฌ์šฉ๋Ÿ‰์„ ๋ฐ˜์˜ํ•œ๋‹ค. + +- allowed ์ด๋ฒคํŠธ์—์„œ๋งŒ ์ฆ๊ฐ€ +- ๊ฐ€์กฑ ์ž”์—ฌ๋Ÿ‰๊ณผ ๊ฒฝ๊ณ /์ฐจ๋‹จ ํŒ๋‹จ์˜ ๊ธฐ์ค€์ด ๋œ๋‹ค. + +์ฆ‰ Redis์—์„œ ๋น ๋ฅด๊ฒŒ ํŒ๋‹จํ•˜๋Š” ๊ธฐ์ค€๊ฐ’๋„ ๊ฒฐ๊ตญ DB์˜ `family_quota` ์˜์† ์ƒํƒœ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ warmup ๋œ๋‹ค. + +## 3. Why This Model Matters + +ํ˜„์žฌ ์ •์‚ฐ ๊ตฌ์กฐ๊ฐ€ ์ด๋ ‡๊ฒŒ ๋‚˜๋‰œ ์ด์œ ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค. + +- `usage_record`: ์ด๋ฒคํŠธ ๋ฉฑ๋“ฑ์„ฑ ๊ธฐ์ค€์  +- `customer_quota`: ๊ณ ๊ฐ ๋‹จ์œ„ ์›” ์‚ฌ์šฉ๋Ÿ‰๊ณผ ์ฐจ๋‹จ ์ƒํƒœ ๊ธฐ์ค€์  +- `family_quota`: ๊ฐ€์กฑ ๋‹จ์œ„ ์ด ์‚ฌ์šฉ๋Ÿ‰๊ณผ ์ž”์—ฌ๋Ÿ‰ ๊ธฐ์ค€์  + +์ฆ‰ ํ•˜๋‚˜์˜ ํ…Œ์ด๋ธ”์— ๋ชจ๋“  ์˜๋ฏธ๋ฅผ ๋ชฐ์•„๋„ฃ์ง€ ์•Š๊ณ , usage ์ด๋ฒคํŠธ์˜ ์—ญํ• ์„ ๋‚˜๋ˆ„์–ด ์˜์†ํ™”ํ•œ๋‹ค. + +## 4. Outbox Payload Model + +Outbox์—๋Š” `EventEnvelope`๊ฐ€ ์•„๋‹ˆ๋ผ ์ตœ์ข… `NotificationPayload` JSON์ด ์ €์žฅ๋œ๋‹ค. + +์ฃผ์š” ํ•„๋“œ: +- `familyId` +- `customerId` +- `type` +- `title` +- `message` +- `data` + +`data`์—๋Š” ์•„๋ž˜ ์ถ”์  ์ •๋ณด๊ฐ€ ๋“ค์–ด๊ฐ„๋‹ค. +- `originEventId` +- `eventTime` +- `status` +- `familyId` +- `customerId` +- `appId` +- `bytesUsed` + +์ด๋ ‡๊ฒŒ ์ €์žฅํ•˜๋Š” ์ด์œ ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค. + +- ์ฆ‰์‹œ ๋ฐœํ–‰๊ณผ ๋ณต๊ตฌ ๋ฐœํ–‰์ด ๊ฐ™์€ payload๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด์„œ +- ๋ณต๊ตฌ ํ”„๋กœ์„ธ์Šค๊ฐ€ notification ๋‚ด์šฉ์„ ๋‹ค์‹œ ๊ณ„์‚ฐํ•˜์ง€ ์•Š๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด์„œ +- usage ์ด๋ฒคํŠธ์™€ notification payload์˜ ์—ฐ๊ฒฐ ๊ด€๊ณ„๋ฅผ row ์•ˆ์— ๋ณด์กดํ•˜๊ธฐ ์œ„ํ•ด์„œ + +## 5. Redis Key Structure + +### 5.1 Family State + +- `family:{familyId}:info:{yyyyMM}` +- `family:{familyId}:remaining:{yyyyMM}` +- `family:{familyId}:members` + +๊ฐ ํ‚ค์˜ ์˜๋ฏธ: +- `info`: ๊ฐ€์กฑ ์ด quota์™€ ๋ฉ”ํƒ€ ์ •๋ณด +- `remaining`: ํ˜„์žฌ ๊ฐ€์กฑ ์ž”์—ฌ๋Ÿ‰ +- `members`: family-customer ๊ฒ€์ฆ์šฉ membership set + +### 5.2 Customer State + +- `family:{familyId}:customer:{customerId}:usage:monthly:{yyyyMM}` +- `family:{familyId}:customer:{customerId}:constraints` + +๊ฐ ํ‚ค์˜ ์˜๋ฏธ: +- `usage:monthly`: ๊ฐœ์ธ ์›” ์‚ฌ์šฉ๋Ÿ‰ ์บ์‹œ +- `constraints`: ์ฐจ๋‹จ ์‹œ๊ฐ„, ์•ฑ ์ฐจ๋‹จ, ์›” ํ•œ๋„ ๊ฐ™์€ ์ •์ฑ… ์ œ์•ฝ + +### 5.3 Alert Dedup Keys + +๊ฒฝ๊ณ  ์•Œ๋ฆผ: +- `family:{familyId}:customer:{customerId}:alert:THRESHOLD:50:{yyyyMM}` +- `family:{familyId}:customer:{customerId}:alert:THRESHOLD:30:{yyyyMM}` +- `family:{familyId}:customer:{customerId}:alert:THRESHOLD:10:{yyyyMM}` + +์ฐจ๋‹จ ์•Œ๋ฆผ: +- `family:{familyId}:customer:{customerId}:alert:MANUAL:{yyyyMM}` +- `family:{familyId}:customer:{customerId}:alert:APP_BLOCK:{appId}:{yyyyMM}` +- `family:{familyId}:customer:{customerId}:alert:TIME_BLOCK:{yyyyMM}` +- `family:{familyId}:customer:{customerId}:alert:MONTHLY_LIMIT_EXCEEDED:{yyyyMM}` +- `family:{familyId}:customer:{customerId}:alert:FAMILY_QUOTA_EXCEEDED:{yyyyMM}` + +์ด ํ‚ค๋“ค์€ ์ด๋ฒˆ ๋‹ฌ ๊ฐ™์€ ์œ ํ˜• ์•Œ๋ฆผ์„ ๋‹ค์‹œ ๋ฐœํ–‰ํ•˜์ง€ ์•Š๋„๋ก ์ œ์–ดํ•˜๋Š” ์—ญํ• ์„ ํ•œ๋‹ค. + +### 5.4 Event Dedup Key + +- `event:dedup:usage:{eventId}` + +์ด ํ‚ค์—๋Š” Lua ๊ฒฐ๊ณผ ์บ์‹œ๊ฐ€ ์ €์žฅ๋œ๋‹ค. + +์ฆ‰ ์ด ํ‚ค๋Š” ๊ฐ™์€ usage ์ด๋ฒคํŠธ๊ฐ€ ๋‹ค์‹œ ๋“ค์–ด์™”์„ ๋•Œ Redis ์ฆ๊ฐ์„ ๋‹ค์‹œ ํ•˜์ง€ ์•Š๊ฒŒ ํ•˜๋Š” ๊ธฐ์ค€์ ์ด๋‹ค. + +## 6. Lua Result Cache + +dedup cache์—๋Š” ์•„๋ž˜ ์ •๋ณด๊ฐ€ ํ•จ๊ป˜ ์ €์žฅ๋œ๋‹ค. + +- `totalUsed` +- `remaining` +- `status` +- `monthlyUsed` +- `userRatio` +- `monthlyLimit` +- `shouldNotify` +- `duplicate` + +์ด ์ •๋ณด๊ฐ€ ๊ฐ™์ด ์ €์žฅ๋˜๋Š” ์ด์œ ๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค. + +- ๊ฐ™์€ ์ด๋ฒคํŠธ ์žฌ์ฒ˜๋ฆฌ ์‹œ Redis ์žฌ๋ฐ˜์˜ ๋ฐฉ์ง€ +- ์ด์ „ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ ์žฌ์‚ฌ์šฉ +- Java๊ฐ€ ๋™์ผํ•œ ์ƒํƒœ ํ•ด์„์„ ๋‹ค์‹œ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Œ +- duplicate์™€ retry๊ฐ€ ๊ฐ™์€ ๊ฒฝ๋กœ๋กœ ๋“ค์–ด์™€๋„ ์ผ๊ด€๋œ ๊ฒฐ๊ณผ ์œ ์ง€ + +## 7. Data Flow View + +```mermaid +flowchart TD + A[usage-events] --> B[Validation] + B --> C[Redis warmup] + C --> D[Lua decision] + D --> E[usage_record] + E --> F[customer_quota] + E --> G[family_quota] + D --> H[NotificationPayload] + H --> I[usage_event_outbox] + I --> J[notification-events] +``` + +## 8. Reading the Model as a Whole + +์ด ๋ชจ๋ธ์„ ์ „์ฒด๋กœ ๋ณด๋ฉด ์—ญํ• ์ด ๋‹ค์Œ์ฒ˜๋Ÿผ ๋‚˜๋‰œ๋‹ค. + +- Redis: ๋น ๋ฅธ ์ƒํƒœ ๊ณ„์‚ฐ +- `usage_record`: ์ด๋ฒคํŠธ ๋ฉฑ๋“ฑ์„ฑ ๊ธฐ์ค€์  +- `customer_quota`, `family_quota`: ์ตœ์ข… ์ •์‚ฐ ๊ธฐ์ค€์  +- Outbox: notification ๋ฐœํ–‰ ์ƒํƒœ ๊ธฐ์ค€์  + +์ฆ‰ `dabom-processor-usage`๋Š” ํ•˜๋‚˜์˜ usage ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋ฉด์„œ๋„, ๊ฐ ์ €์žฅ ๊ตฌ์กฐ๋ฅผ ์„œ๋กœ ๋‹ค๋ฅธ ์ฑ…์ž„์— ๋งž๊ฒŒ ์‚ฌ์šฉํ•˜๋„๋ก ์„ค๊ณ„๋œ ์„œ๋น„์Šค๋‹ค.