diff --git a/README.md b/README.md new file mode 100644 index 0000000..e135860 --- /dev/null +++ b/README.md @@ -0,0 +1,221 @@ +# ๐Ÿ“š ์‹ค์‹œ๊ฐ„ ํ™˜์œจ [XCP] ๐Ÿ“š +ํ”Œ๋กœ์šฐ ์ฐจํŠธ + +## โœจ ํ”„๋กœ์ ํŠธ ๊ฐœ์š” +XChangePass ๋ฐฑ์—”๋“œ ์„œ๋น„์Šค๋Š” **์‹ค์ „ ๊ธˆ์œต ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ๊ฒฝํ—˜**์„ ์Œ“๊ธฐ ์œ„ํ•ด +์‹ค์‹œ๊ฐ„ ํ™˜์œจ ์—ฐ๋™, ๋ฉ€ํ‹ฐ ํ†ตํ™” ์ง€๊ฐ‘, ์˜ˆ์•ฝ ์†ก๊ธˆยท์•Œ๋ฆผ ๊ธฐ๋Šฅ์„ ์ง์ ‘ ์„ค๊ณ„ยท๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. + +## โœจ ๊ธฐํš ๋ฐฐ๊ฒฝ +> ๋‹จ์ˆœํ•œ ํ™˜์ „ ๊ธฐ๋Šฅ ์ œ๊ณต์„ ๋„˜์–ด, +> - **์‹ค์ œ ๊ธˆ์œต API ์—ฐ๋™ ๊ณผ์ •**์—์„œ ๋งˆ์ฃผ์น˜๋Š” ๋ณด์•ˆยท์„ฑ๋Šฅ ์ด์Šˆ๋ฅผ ํ•ด๊ฒฐํ•ด ๋ณด๊ณ , +> - **๋Œ€๊ทœ๋ชจ ํŠธ๋ž˜ํ”ฝ** ์ƒํ™ฉ์—์„œ์˜ ์บ์‹œ ์ „๋žตยทํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ ๊ฒฝํ—˜์„ ์Œ“์œผ๋ฉฐ, +> - **์šด์˜ ํ™˜๊ฒฝ**์—์„œ์˜ ๋ชจ๋‹ˆํ„ฐ๋งยท์•Œ๋ฆผยท์ ˆ์ฐจ๋ฅผ ์ฒดํ—˜ํ•˜๋Š” ๊ฒƒ์ด ๋ณธ ํ”„๋กœ์ ํŠธ์˜ ํ•ต์‹ฌ ํ•™์Šต ๋ชฉํ‘œ์ž…๋‹ˆ๋‹ค. +> +> ์ด๋ฅผ ํ†ตํ•ด ๋‹จ์ˆœ ๋ฐ๋ชจ๋ฅผ ๋„˜์–ด, โ€œํ”„๋กœ๋•์…˜ ๋ ˆ๋ฒจโ€์˜ ๋ฐฑ์—”๋“œ ์šด์˜ ์—ญ๋Ÿ‰์„ ํ™•๋ณดํ•˜๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค. + + +
+ +## โœจ ํ”„๋กœ์ ํŠธ ๊ธฐ๊ฐ„ +- **๊ธฐํš ๋ฐ ์„ค๊ณ„ :** 2025.2.17 ~ 2025.2.14 +- **๊ฐœ๋ฐœ :** 2024.2.15 ~ 2024.12.09 + + +
+ +## โœจ ์•„ํ‚คํ…์ฒ˜ ๋ฐ ํ•ต์‹ฌ ๋ชจ๋“ˆ +| ๋ชจ๋“ˆ | ์„ค๋ช… | +|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------| +| ์ธ์ฆยท๊ถŒํ•œ ๊ด€๋ฆฌ | Spring Security + JWT ๊ธฐ๋ฐ˜ ์ธ์ฆยท์ธ๊ฐ€, Token Rotate ์ „๋žต | +| ์นด๋“œ ๊ด€๋ฆฌ | ์นด๋“œ ๋ฐœ๊ธ‰/์กฐํšŒ/์ƒํƒœ ๋ณ€๊ฒฝ, AES ํ‚คยทIV ์•”ํ˜ธํ™” (EncryptionData Embeddable) :contentReference[oaicite:0]{index=0}​:contentReference[oaicite:1]{index=1} | +| ์ง€๊ฐ‘(Wallet) ๊ด€๋ฆฌ | ์‚ฌ์šฉ์ž ์ง€๊ฐ‘ ์ƒ์„ฑ, ํ™”ํ๋ณ„ ์ž”์•ก ์กฐํšŒยท์—ฐ์‚ฐ | +| ํ™˜์œจ ๊ณ„์‚ฐ ์—”์ง„ | ์™ธ๋ถ€ API ์—ฐ๋™(์˜ˆ: Open Exchange Rates), ,๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ, ์‹ค์‹œ๊ฐ„ ํ™˜์œจ ์บ์‹ฑ | +| ๊ฑฐ๋ž˜ ์ฒ˜๋ฆฌ(Transaction) | ACID ํŠธ๋žœ์žญ์…˜ ๋ณด์žฅ, ์žฅ์•  ๋ณต๊ตฌ ๋กœ์ง, ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ | +| ํ™˜์ „ | ์‹ค์‹œ๊ฐ„ ํ™˜์œจ ๊ธฐ๋ฐ˜ ๋‹ค์ค‘ ํ†ตํ™” ๊ฐ„ ํ™˜์ „ ์ฒ˜๋ฆฌ | +| ์•Œ๋ฆผ ์„œ๋น„์Šค | Slack ์•Œ๋ฆผ ๋ฐœ์†ก | + +## โœจ ์ฃผ์š” ๊ธฐ๋Šฅ + +### 1. ํ™˜์œจ ์ •๋ณด ๋ฐ ํ™˜์ „ +- **์„ค๋ช…** + - ์™ธ๋ถ€ ํ™˜์œจ API ์—ฐ๋™(์˜ˆ: Open Exchange Rates), ์‹ค์‹œ๊ฐ„ ์บ์‹ฑ(TTL 5๋ถ„), CompletableFuture ๊ธฐ๋ฐ˜ ๋น„๋™๊ธฐ ๊ฐฑ์‹  ์ฒ˜๋ฆฌ. + - ํ™˜์ „ ๋กœ์ง์€ ํŠธ๋žœ์žญ์…˜ ๊ฒฉ๋ฆฌ ์ˆ˜์ค€์— ๋”ฐ๋ผ ์ตœ์ ํ™” (Read Committed > Repeatable Read > Serializable). +- **์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ** + - ์‘๋‹ต ์‹œ๊ฐ„: ํ‰๊ท  70s โ†’ 11~12s (์“ฐ๋ ˆ๋“œํ’€ + ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ) + - TPS ์ฆ๊ฐ€: 7 โ†’ 24 โ†’ 48 (๊ฒฉ๋ฆฌ ์ˆ˜์ค€: Serializable โ†’ Repeatable Read โ†’ Read Committed) +
+ํ™˜์ „ flow ์ฐจํŠธ ๋ฐ ํŠธ๋žœ์ น์„  ์ฒ˜๋ฆฌ ํ๋ฆ„ โ–ถ๏ธ + +![ํ”Œ๋กœ์šฐ ์ฐจํŠธ](docs/flow/exchangeFlowChart.png) + +- ### **ํ™˜์ „ ํŠธ๋žœ์žญ์…˜ ์ฒ˜๋ฆฌ ํ๋ฆ„ ์ •๋ฆฌ** +| SQL ์ฒ˜๋ฆฌ | ์„ค๋ช… | +| --- | --- | +| TRANSACTION BEGIN | ํŠธ๋žœ์žญ์…˜ ์‹œ์ž‘ | +| SELECT * FROM exchange_transaction WHERE transaction_id = #{transactionId} | ๊ฑฐ๋ž˜ ๋‚ด์—ญ ์กฐํšŒ (์ƒํƒœ ํ™•์ธ: PENDING ์ธ์ง€ ํ™•์ธ) | +| SELECT * FROM wallet WHERE user_id = #{userId} | ์œ ์ € ์ง€๊ฐ‘ ์กฐํšŒ | +| SELECT * FROM wallet_balance WHERE wallet_id = #{walletId} AND currency = #{fromCurrency} | ์ถœ๊ธˆํ•  ํ™”ํ ์ž”์•ก ์กฐํšŒ | +| IF ์ž”์•ก ๋ถ€์กฑ THEN INSERT INTO wallet_balance_history (์ถฉ์ „ ๋‚ด์—ญ) | ์ž”์•ก ๋ถ€์กฑ ์‹œ ์ถฉ์ „ ์ฒ˜๋ฆฌ | +| SELECT * FROM wallet_balance WHERE wallet_id = #{walletId} AND currency = #{toCurrency} | ์ž…๊ธˆํ•  ํ™”ํ ์ž”์•ก ์กฐํšŒ | +| UPDATE wallet_balance SET balance = balance - #{amount} WHERE wallet_id = #{walletId} AND currency = #{fromCurrency} | ์ถœ๊ธˆ ํ™”ํ ์ž”์•ก ์ฐจ๊ฐ | +| UPDATE wallet_balance SET balance = balance + #{receivedAmount} WHERE wallet_id = #{walletId} AND currency = #{toCurrency} | ์ž…๊ธˆ ํ™”ํ ์ž”์•ก ์ฆ๊ฐ€ | +| UPDATE exchange_transaction SET status = 'COMPLETED' WHERE transaction_id = #{transactionId} | ๊ฑฐ๋ž˜ ์ƒํƒœ ๋ณ€๊ฒฝ (์™„๋ฃŒ ์ฒ˜๋ฆฌ) | +| TRANSACTION COMMIT | ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ | +--- + +
+ +--- + +### 2. ์†ก๊ธˆ ๋ฐ ๊ฑฐ๋ž˜ +- **์„ค๋ช…** + ์‚ฌ์šฉ์ž๊ฐ„ ๋‹ค์ค‘ ํ†ตํ™” ์†ก๊ธˆ ์ฒ˜๋ฆฌ, ๊ฑฐ๋ž˜ ๋‚ด์—ญ ๊ธฐ๋ก, ACID ํŠธ๋žœ์žญ์…˜ ๋ณด์žฅ, ๋ชจ๋“ˆํ™”๋œ ๊ฑฐ๋ž˜ ์ฒ˜๋ฆฌ ๋กœ์ง + +
+ + ๊ฑฐ๋ž˜์‹œ์Šคํ…œ ์ฃผ์š” ํ๋ฆ„ โ–ถ๏ธ + + ![๋‹ค์ด์–ด๊ทธ๋žจ](docs/XCP_drawio.png) + +
+ +- **์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ** + - ์‹œ๋‚˜๋ฆฌ์˜ค : ๋กœ๊ทธ์ธ โ†’ ์ถฉ์ „ โ†’ ์ถœ๊ธˆ โ†’ ์†ก๊ธˆ โ†’ ์ž”์•ก ์กฐํšŒ โ†’ ๊ฑฐ๋ž˜๋‚ด์—ญ ์กฐํšŒ + - ๋™์‹œ์„ฑ ํ…Œ์ŠคํŠธ: 50 โ†’ 100 โ†’ 200 ์œ ์ง€ โ†’ 50 โ†’ 10๋ช…๊นŒ์ง€ ์ ์ง„ ์ฆ๊ฐ€/๊ฐ์†Œ ์‹œ๋‚˜๋ฆฌ์˜ค (์ด 5๋ถ„ ํ…Œ์ŠคํŠธ) + - ํ‰๊ท  ์š”์ฒญ ์†๋„ (Avg. Req/sec) : ์•ฝ 139 req/s, ์ตœ๋Œ€ 368 req/s + - Errors per Second = 0 + - ์‘๋‹ต ์‹œ๊ฐ„ (http_req_duration) : ํ‰๊ท : 588ms, ์ตœ๋Œ€: 4.40์ดˆ, P90: 1.36์ดˆ, P95: 1.70์ดˆ, ์ตœ์†Œ: 1.87ms + - ์š”์ฒญ ๋ธ”๋กœํ‚น ์‹œ๊ฐ„ (http_req_blocked) : ๊ฑฐ์˜ ์—†์Œ (ํ‰๊ท  0.03ms) + - ์ฒดํฌ ์„ฑ๊ณต๋ฅ  (Checks Per Second) : ์ด 7015๊ฑด ์ค‘ ๊ฑฐ๋ž˜๋‚ด์—ญ ๋ฐ ๋กœ๊ทธ์ธ ๋“ฑ ๊ฒ€์ฆ ํ•ญ๋ชฉ 99% ์ด์ƒ ์„ฑ๊ณต +- **ํ…Œ์ŠคํŠธ** + - ๋‹จ์œ„ ํ…Œ์ŠคํŠธ (Unit Test) + - **๋Œ€์ƒ** + - ์ง€๊ฐ‘ ๋กœ์ง์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์ธ **์ž”์•ก ์ฒ˜๋ฆฌ ๋กœ์ง** + - ํŠธ๋žœ์žญ์…˜ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ/์†Œ๋น„ ๋กœ์ง, ์Šฌ๋ž™ ์•Œ๋ฆผ ์ „์†ก ๋“ฑ + + - **์ฃผ์š” ๋‚ด์šฉ** + - ์ถฉ์ „(Deposit), ์ถœ๊ธˆ(Withdraw), ์†ก๊ธˆ(Transfer) ๊ธฐ๋Šฅ์˜ ๋‚ด๋ถ€ ๋กœ์ง ๊ฒ€์ฆ + - ์‹คํŒจ ์กฐ๊ฑด ๋ฐœ์ƒ ์‹œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๊ฒ€์ฆ + - `@MockBean` ๋ฐ ๋‚ด๋ถ€ ๊ฐ์ฒด ์ฃผ์ž…์„ ํ†ตํ•œ ๋กœ์ง ๋‹จ์œ„ ๋‹จ๋… ํ…Œ์ŠคํŠธ + - ์Šฌ๋ž™ ์•Œ๋ฆผ ๋“ฑ ์™ธ๋ถ€ ์—ฐ๋™ ์˜์กด์„ฑ์„ ์ œ๊ฑฐํ•œ ๋ฉ”์‹œ์ง€ ์ปจ์Šˆ๋จธ ํ…Œ์ŠคํŠธ + + - โœ… ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ (Integration Test) + + > Testcontainers ๊ธฐ๋ฐ˜ PostgreSQL, RabbitMQ ํ™˜๊ฒฝ์—์„œ ์‹ค์ œ ์„œ๋น„์Šค ํ๋ฆ„์„ ๊ฒ€์ฆํ•˜๋Š” E2E ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์ˆ˜ํ–‰ + + - **ํ™˜๊ฒฝ** + - `PostgreSQL`, `RabbitMQ` ๋„์ปค ์ปจํ…Œ์ด๋„ˆ ๊ธฐ๋ฐ˜ ๊ตฌ์„ฑ + - `SlackNotifier`๋Š” `@MockBean` ์ฒ˜๋ฆฌ + + - **๊ฒ€์ฆ ํ•ญ๋ชฉ** + - ์‚ฌ์šฉ์ž ์ง€๊ฐ‘ ์ƒ์„ฑ โ†’ ์ถฉ์ „ โ†’ ์†ก๊ธˆ โ†’ ์ž”์•ก ํ™•์ธ๊นŒ์ง€์˜ ์ „์ฒด ํ๋ฆ„ + - ๋™์‹œ ์†ก๊ธˆ, ์†ก๊ธˆ ๋„์ค‘ ์ถœ๊ธˆ, ์ถฉ์ „ ๋„์ค‘ ์†ก๊ธˆ ๋“ฑ **๊ฒฝ์Ÿ ์ƒํ™ฉ ์ฒ˜๋ฆฌ** + - ํŠธ๋žœ์žญ์…˜ ๊ธฐ๋ก ์ƒ์„ฑ ๋ฐ ๊ฑฐ๋ž˜๋‚ด์—ญ ์กฐํšŒ ๊ธฐ๋Šฅ + - ์‹คํŒจ ๋ฉ”์‹œ์ง€ โ†’ DLQ โ†’ Slack ์•Œ๋ฆผ ์ „์†ก ์‹œ๋‚˜๋ฆฌ์˜ค ๊ฒ€์ฆ + + - **์ฃผ์š” ์‹œ๋‚˜๋ฆฌ์˜ค** + + | ์‹œ๋‚˜๋ฆฌ์˜ค | ์„ค๋ช… | + |----------|------| + | โœ… ์ •์ƒ ์†ก๊ธˆ ์ฒ˜๋ฆฌ | ์†ก๊ธˆ ํ›„ ์†ก/์ˆ˜์‹ ์ž ์ž”์•ก ๋ฐ˜์˜ ํ™•์ธ | + | โŒ ์ž”์•ก ๋ถ€์กฑ ์˜ˆ์™ธ | ์˜ˆ์™ธ ๋ฐœ์ƒ ๋ฐ ํŠธ๋žœ์žญ์…˜ ์ €์žฅ ์•ˆ๋จ | + | ๐Ÿ”„ ๋Œ€๋Ÿ‰ ๋™์‹œ ์†ก๊ธˆ | 100๋ช… ์†ก๊ธˆ ์‹œ ์ผ๊ด€์„ฑ ์œ ์ง€ | + | โš ๏ธ ์ถฉ๋Œ ์ƒํ™ฉ ํ…Œ์ŠคํŠธ | ์†ก๊ธˆ โ†” ์ถœ๊ธˆ, ์ถฉ์ „ โ†” ์†ก๊ธˆ ๋™์‹œ ๋ฐœ์ƒ ์‹œ ์ฒ˜๋ฆฌ ํ™•์ธ | + | ๐Ÿ“ค DLQ ์ฒ˜๋ฆฌ | ์‹คํŒจ ๋ฉ”์‹œ์ง€ โ†’ Slack ์•Œ๋ฆผ ์ „์†ก๊นŒ์ง€ ํ๋ฆ„ ๊ฒ€์ฆ | + | ๐Ÿ” ๊ฑฐ๋ž˜๋‚ด์—ญ ํ•„ํ„ฐ๋ง | ํŠธ๋žœ์žญ์…˜ ํƒ€์ž…๋ณ„ ์กฐํšŒ ๊ธฐ๋Šฅ ํ™•์ธ | + +--- + +### 3. ์นด๋“œ ๊ด€๋ฆฌ ๋ฐ ์ •๋ณด ์•”ํ˜ธํ™” +- **์„ค๋ช…** + - `CardService.generatePhysicalCard(userId)` + - ๋ฌผ๋ฆฌ(์‹ค๋ฌผ) ์นด๋“œ ๋ฐœ๊ธ‰ + - KMS ๊ธฐ๋ฐ˜ RSAEncryption์œผ๋กœ AES ํ‚ค ์•”๋ณตํ˜ธํ™” โ†’ `EncryptionData` Embeddable์— ์•”ํ˜ธํ™”๋œ AES ํ‚คยทIV ์ €์žฅ + - `CardService.getDetailedCardInfo(cardId)` + - Redis ์บ์‹œ ์กฐํšŒ + - ์บ์‹œ์— ์—†์œผ๋ฉด RSAEncryption์œผ๋กœ AES ํ‚ค ๋ณตํ˜ธํ™” โ†’ AESEncryption์œผ๋กœ ์นด๋“œ๋ฒˆํ˜ธยทCVC ๋ณตํ˜ธํ™” โ†’ Redis์— ์ €์žฅ + - `CardService.changeCardStatus(userId, request)` + - DB ์—…๋ฐ์ดํŠธ + Redis ์บ์‹œ ๋™์‹œ ๋ฐ˜์˜ + +### ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ + +
+์นด๋“œ ๊ด€๋ฆฌ ์ฃผ์š” ํ๋ฆ„ โ–ถ๏ธ + +#### 1) ์‹ค๋ฌผ ์นด๋“œ ๋ฐœ๊ธ‰ ์‹œํ€€์Šค +![์‹ค๋ฌผ ์นด๋“œ ๋ฐœ๊ธ‰ ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ](docs/sequence/card-issuance-sequence.png) + +#### 2) ์นด๋“œ ์ƒ์„ธ ์กฐํšŒ ์‹œํ€€์Šค +![์นด๋“œ ์ƒ์„ธ ์กฐํšŒ ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ](docs/sequence/card-retrieval-sequence.png) +
+ +- **๐Ÿ“์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ** + - **์•”ํ˜ธํ™”/๋ณตํ˜ธํ™” ์ฒ˜๋ฆฌ๋Ÿ‰**: 1,000๊ฑด/sec โ†’ ํ‰๊ท  ์ง€์—ฐ โ‰ค 10ms + - **Redis ์บ์‹œ ์ ์ค‘๋ฅ **: 100์กฐํšŒ ์ค‘ โ‰ฅ 90% (TTL 5๋ถ„) + - **์ปจํŠธ๋กค๋Ÿฌ ์‘๋‹ต ์†๋„** (MockMvc ๊ธฐ์ค€) + - POST `/api/v1/card/physical`, PUT `/api/v1/card/status`: โ‰ค 50ms + - GET `/api/v1/card`, `/api/v1/card/{cardId}`: โ‰ค 30ms + +- **๐Ÿ”จํ…Œ์ŠคํŠธ** + - **โœ…์ปจํŠธ๋กค๋Ÿฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ** (`CardControllerTest`) + - `์‹ค๋ฌผ์นด๋“œ๋ฐœ๊ธ‰_์„ฑ๊ณต` (POST `/api/v1/card/physical` โ†’ 201 Created) + - `์นด๋“œ์ƒํƒœ๋ณ€๊ฒฝ_์„ฑ๊ณต` (PUT `/api/v1/card/status` โ†’ 204 No Content) + - `๋ณด์œ ์นด๋“œ๋ชฉ๋ก์กฐํšŒ_์„ฑ๊ณต` (GET `/api/v1/card` โ†’ 200 OK) + - `์นด๋“œ์ƒ์„ธ์ •๋ณด์กฐํšŒ_์„ฑ๊ณต` (GET `/api/v1/card/{cardId}` โ†’ 200 OK) + + - **โœ…์„œ๋น„์Šค ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** (`CardServiceTest` extends `RedisTestBase`) + - `verifyPhysicalCardIssuance`: DB์— ์‹ค๋ฌผ ์นด๋“œ ์ •์ƒ ๋ฐœ๊ธ‰ ํ™•์ธ + - `verifyKeyDecryptionAndRedisStorage`: + - RSAEncryption์œผ๋กœ AES ํ‚ค ๋ณตํ˜ธํ™” + - AESEncryption์œผ๋กœ ์นด๋“œ๋ฒˆํ˜ธยทCVC ๋ณตํ˜ธํ™” + - Redis ์บ์‹œ ์ €์žฅ ํ™•์ธ + - `changeCardStatus_shouldUpdateBothDatabaseAndRedisCache`: + - DB ์ƒํƒœ ๋ณ€๊ฒฝ + - Redis ์บ์‹œ ์ƒํƒœ ๋™๊ธฐํ™” + + - **โœ…ํ…Œ์ŠคํŠธ ์Šคํƒ** + - JUnit5, Mockito, Spring Boot Test, MockMvc + - Testcontainers Embedded Redis + + + + +## โœจ ๊ธฐ์ˆ  ์Šคํƒ +___ + +
+ + +
+ + + + + + + +## โœจ ๊ฐœ๋ฐœ ๋ฌธ์„œ + +
ERD + +![ERD](/docs/Copy_of_XCP_1.png) +
+ +
์ปจ๋ฒค์…˜ + + +- [ํŒ€ ๊ทœ์น™](https://silky-toothbrush-191.notion.site/ee1575c5d056473f83d9f56f40edaa47) +- [๊ณตํ†ต ์ปค๋ฐ‹ ์ปจ๋ฒค์…˜](https://silky-toothbrush-191.notion.site/3903032f148543b685d3de474249d31f) +- [๋ฒก์—”๋“œ ์ฝ”๋“œ ์ปจ๋ฒค์…˜](https://silky-toothbrush-191.notion.site/70565c77e3b34b38bb8d2d56ca7a6a54) +
+ +
+ +## โœจ ํŒ€ ์†Œ๊ฐœ + +| BE | BE | BE | +|:----------------------------------------------------------:|:----------------------------------------------------------:|:----------------------------------------------------------:| +| ![](https://avatars.githubusercontent.com/u/176664628?v=4) | ![](https://avatars.githubusercontent.com/u/134962465?v=4) | ![](https://avatars.githubusercontent.com/u/97494494?v=4) | +| Team Leader | Developer | Developer | +| [๊ฐ•์‹œ์˜](https://github.com/Si-rauis) | [์ด์‹œํ˜„](https://github.com/CryingPerson) | [์ด์šฉ์ค€](https://github.com/usingjun) | +| ์นด๋“œ ๊ด€๋ฆฌ / ์œ ์ € CRUD /
๊ธˆ์œต ์ •๋ณด ์•”ํ˜ธํ™” / Jira ์—ฐ๋™ | ์‹ค์‹œ๊ฐ„ ํ™˜์œจ ์ •๋ณด/
๋™์‹œ์„ฑ ์ œ์–ด ํ™˜์ „ | ๊ฑฐ๋ž˜ ์‹œ์Šคํ…œ(์†ก๊ธˆ, ์ถฉ์ „, ์ถœ๊ธˆ) / ์‹œํ๋ฆฌํ‹ฐ ๊ตฌ์„ฑ(๋กœ๊ทธ์ธ)
/์ง€๊ฐ‘ ๊ฑฐ๋ž˜๋‚ด์—ญ(์žฅ์•  ๋ณต๊ตฌ, ์•Œ๋ฆผ) | \ No newline at end of file diff --git a/docs/Copy_of_XCP_1.png b/docs/Copy_of_XCP_1.png new file mode 100644 index 0000000..65a48f5 Binary files /dev/null and b/docs/Copy_of_XCP_1.png differ diff --git a/docs/XCP.png b/docs/XCP.png new file mode 100644 index 0000000..791d8f0 Binary files /dev/null and b/docs/XCP.png differ diff --git a/docs/XCP_drawio.png b/docs/XCP_drawio.png new file mode 100644 index 0000000..7dd7058 Binary files /dev/null and b/docs/XCP_drawio.png differ diff --git a/docs/flow/exchangeFlowChart.png b/docs/flow/exchangeFlowChart.png new file mode 100644 index 0000000..7940f51 Binary files /dev/null and b/docs/flow/exchangeFlowChart.png differ diff --git a/docs/sequence/card-issuance-sequence.png b/docs/sequence/card-issuance-sequence.png new file mode 100644 index 0000000..55081d6 Binary files /dev/null and b/docs/sequence/card-issuance-sequence.png differ diff --git a/docs/sequence/card-retrieval-sequence.png b/docs/sequence/card-retrieval-sequence.png new file mode 100644 index 0000000..46953cb Binary files /dev/null and b/docs/sequence/card-retrieval-sequence.png differ diff --git a/src/main/java/bumblebee/xchangepass/domain/card/service/CardService.java b/src/main/java/bumblebee/xchangepass/domain/card/service/CardService.java index 8f28f58..45c467c 100644 --- a/src/main/java/bumblebee/xchangepass/domain/card/service/CardService.java +++ b/src/main/java/bumblebee/xchangepass/domain/card/service/CardService.java @@ -105,6 +105,8 @@ public void generatePhysicalCard(Long userId) { .build(); cardRepository.save(mobileCard); + + existUser.getWallet().getCards().add(mobileCard); }catch (CommonException e) { throw e; }catch (Exception e) { diff --git a/src/test/java/bumblebee/xchangepass/domain/card/controller/CardControllerTest.java b/src/test/java/bumblebee/xchangepass/domain/card/controller/CardControllerTest.java index b3ff9d1..8a96a9f 100644 --- a/src/test/java/bumblebee/xchangepass/domain/card/controller/CardControllerTest.java +++ b/src/test/java/bumblebee/xchangepass/domain/card/controller/CardControllerTest.java @@ -7,6 +7,7 @@ import bumblebee.xchangepass.domain.card.entity.CardStatus; import bumblebee.xchangepass.domain.card.entity.CardType; import bumblebee.xchangepass.domain.card.service.CardService; +import bumblebee.xchangepass.global.security.jwt.CustomUserDetails; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -17,12 +18,16 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import java.time.LocalDateTime; import java.util.Collections; +import java.util.List; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; @@ -33,8 +38,8 @@ * ์ปจํŠธ๋กค๋Ÿฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ */ @SpringBootTest -@AutoConfigureMockMvc @ActiveProfiles("test") +@AutoConfigureMockMvc @Import(TestUserInitializer.class) class CardControllerTest { @@ -47,8 +52,22 @@ class CardControllerTest { @Autowired private ObjectMapper objectMapper; + @TestConfiguration + static class MockSecurityConfig { + @Bean + public UserDetailsService customUserDetailsService() { + // username("1") ์š”์ฒญ์ด ๋“ค์–ด์˜ค๋ฉด CustomUserDetails๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก + return username -> new CustomUserDetails( + 1L, + username, + "", + "ROLE_USER" + ); + } + } + @Test - @WithMockUser(username = "1") + @WithUserDetails(value = "1", userDetailsServiceBeanName = "customUserDetailsService") void ์‹ค๋ฌผ์นด๋“œ๋ฐœ๊ธ‰_์„ฑ๊ณต() throws Exception { doNothing().when(cardService).generatePhysicalCard(1L); @@ -61,7 +80,7 @@ class CardControllerTest { } @Test - @WithMockUser(username = "1") + @WithUserDetails(value = "1", userDetailsServiceBeanName = "customUserDetailsService") void ์นด๋“œ์ƒํƒœ๋ณ€๊ฒฝ_์„ฑ๊ณต() throws Exception { ChangeCardStatusRequest request = ChangeCardStatusRequest.builder() .cardType(CardType.PHYSICAL) @@ -82,7 +101,7 @@ class CardControllerTest { } @Test - @WithMockUser(username = "1") + @WithUserDetails(value = "1", userDetailsServiceBeanName = "customUserDetailsService") void ๋ณด์œ ์นด๋“œ๋ชฉ๋ก์กฐํšŒ_์„ฑ๊ณต() throws Exception { BasicCardInfoResponse cardInfoResponse = BasicCardInfoResponse.builder() .cardId(1L) @@ -102,7 +121,7 @@ class CardControllerTest { } @Test - @WithMockUser(username = "1") + @WithUserDetails(value = "1", userDetailsServiceBeanName = "customUserDetailsService") void ์นด๋“œ์ƒ์„ธ์ •๋ณด์กฐํšŒ_์„ฑ๊ณต() throws Exception { Long cardId = 1L; DetailedCardInfoResponse cardInfoResponse = DetailedCardInfoResponse.builder() diff --git a/src/test/java/bumblebee/xchangepass/domain/card/service/CardServiceTest.java b/src/test/java/bumblebee/xchangepass/domain/card/service/CardServiceTest.java index 7e05941..5209cd2 100644 --- a/src/test/java/bumblebee/xchangepass/domain/card/service/CardServiceTest.java +++ b/src/test/java/bumblebee/xchangepass/domain/card/service/CardServiceTest.java @@ -24,7 +24,6 @@ import org.springframework.transaction.annotation.Transactional; import javax.crypto.SecretKey; - import java.util.List; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; @@ -56,7 +55,11 @@ public class CardServiceTest extends RedisTestBase { void verifyPhysicalCardIssuance(){ Long userId = 1L; - cardService.generatePhysicalCard(userId); + try { + cardService.generatePhysicalCard(userId); + }catch (Exception e){ + System.out.println(e.getMessage()); + } User user = userRepository.findByUserId(userId) .orElseThrow(ErrorCode.USER_NOT_FOUND::commonException); @@ -109,7 +112,7 @@ void verifyKeyDecryptionAndRedisStorage() { @Test @DisplayName("์นด๋“œ ์ƒํƒœ ๋ณ€๊ฒฝ ์‹œ DB์™€ Redis ๋™์‹œ ๋ฐ˜์˜") void changeCardStatus_shouldUpdateBothDatabaseAndRedisCache() { - Long userId = 2L; + Long userId = 3L; cardService.generatePhysicalCard(userId); List cardInfo = cardService.getBasicCardInfo(userId);