๊ธ์ต ๋ง์ด๋ฐ์ดํฐ์ ์คํ๋ฑ ํน์ ๊ธฐ๋ฐ์ผ๋ก ํ ๊ฐ์ธ ์์ฐ๊ด๋ฆฌ ์๋น์ค. ์๋์ด ์ฌ์ฉ์์ ๋ณดํธ์๋ฅผ ํจ๊ป ๊ณ ๋ คํ ๊ฐ์กฑ ๋ณดํธ ๊ธฐ๋ฅ์ด ํต์ฌ ์ถ์ด๋ค.
| ์คํ | Java 21 ยท Spring Boot 4 ยท MariaDB ยท Redis ยท Gradle |
| ์ํคํ ์ฒ | ํฅ์ฌ๊ณ ๋ (adapter.in / application.port / application.service / domain / adapter.out) |
| ๊ท๋ชจ | ํ๋ก๋์ ์ฝ 640 ํ์ผ ยท ์๋ํฌ์ธํธ 150+ ยท ์ํฐํฐ 50 |
| ๊ด์ธก์ฑ | Prometheus ยท Loki ยท Grafana ยท OpenTelemetry |
๊ฐ๊ณ๋ถ ์ฑ์ด ์๋๋ผ ๋์ ์ค์ ๋ก ์์ง์ด๋ ์์คํ ์ด๋ค. ๊ทธ๋์ ๊ธฐ๋ฅ ๋ชฉ๋ก๋ณด๋ค ์๋ ์ง๋ฌธ๋ค์ ์ด๋ป๊ฒ ๋ตํ๋์ง๊ฐ ์ค์ํ๋ค.
์ํ์ ์ด์ฒด๋ฅผ ์์ฒญํ๋๋ฐ ์๋ต์ด ์ค์ง ์์ผ๋ฉด, ๊ทธ ๊ฑด์ ์ฑ๊ณต์ธ๊ฐ ์คํจ์ธ๊ฐ?
์ ๋ต์ "๋ ๋ค ์๋๋ค"์ด๋ค. ์ด ๊ตฌ๋ถ์ ์ฝ๋๋ก ํํํ๋ ๊ฒ์ด ์ด ํ๋ก์ ํธ์ ์ค์ฌ ์ฃผ์ ๋ค.
ํ์์์์ด๋ 5xx๋ ์์ฒญ์ด ์ํ์ ๋๋ฌํ๋์ง์กฐ์ฐจ ์ ์ ์๋ ์ํ๋ค. ์ด๊ฑธ ์คํจ๋ก ํ์ ํ๋ฉด, ์ค์ ๋ก๋ ์ถ๊ธ๋ ๊ฑด์ ์ฌ์ฉ์๊ฐ ์ฌ์๋ํด ์ด์ค ์ถ๊ธ์ด ๋ฐ์ํ๋ค.
์ฑ๊ณต โ EXECUTED
๋ช
ํํ ๊ฑฐ์ โ FAILED + ํ๋ ๋ณต๊ตฌ
์๋ต ํ์ธ ๋ถ๊ฐ โ UNKNOWN + ํ๋ ์ ์ง + ์ ์ฐ ๋์ ๋ฑ๋ก
UNKNOWN ๊ฑด์ TransferReconciliationBatch๊ฐ ์ํ์ ์ฌ์กฐํํด ํ์ ํ๋ค. ๋ฏธ์ถ๊ธ์ด ํ์ธ๋์ ๋๋ง ํ๋๋ฅผ ๋๋๋ฆฐ๋ค. ์๋์ผ๋ก ํ๋ฆฌ์ง ์๋ ๊ฑด์ ์ด์ API๋ก ๋ด๋น์๊ฐ ์ํ ์์ฅ๊ณผ ๋์กฐํด ํ์ ํ๋ฉฐ, ๊ทธ ํ์ ์์ฒด๊ฐ ๊ฐ์ฌ ๋ก๊ทธ์ ๋จ๋๋ค.
TransferService์๋ @Transactional์ด ์๋ค. ์๋์ ์ด๋ค. ์ํ ํธ์ถ์ DB ํธ๋์ญ์
์์ ๋ฃ์ผ๋ฉด
- ๋คํธ์ํฌ ์๋ณต ๋ด๋ด ์ปค๋ฅ์ ์ ์ ์ ํ๊ณ ,
- ์คํจ ์ ๋กค๋ฐฑ๋์ด ์คํจ ๊ธฐ๋ก์กฐ์ฐจ ๋จ์ง ์์ผ๋ฉฐ,
- ์ํ ํธ์ถ ์ฑ๊ณต ํ ์ปค๋ฐ์ด ์คํจํ๋ฉด ๋์ ๋๊ฐ๋๋ฐ ๊ธฐ๋ก์ด ์๋ค.
์ํ ์ ์ด๋ TransferOrderWriter๊ฐ REQUIRES_NEW๋ก ๋
๋ฆฝ ์ปค๋ฐํ๊ณ , ์ค์ผ์คํธ๋ ์ดํฐ๋ ์์๋ง ์กฐ์จํ๋ค.
"์กฐํํด์ ์์ผ๋ฉด ์คํ"์ ๋ฉฑ๋ฑ์ด ์๋๋ค. ๋์ ์์ฒญ ๋์ด ๋ชจ๋ ์กฐํ์์ empty๋ฅผ ๋ฐ์ผ๋ฉด ๋ ๋ค ์คํ๋๋ค. ๋์ (user_id, idempotency_key) ์ ๋ํฌ ์ ์ฝ ์๋ฐ์ ์ ํธ๋ก ์ผ์ INSERT๋ก ์ ์ ํ๋ค. ์ ํ๋ฆฌ์ผ์ด์
๋ ๋ฒจ ๊ฒฝ์ ๊ตฌ๊ฐ์ด ์๋ค.
์ํ์ ๋ณด๋ด๋ ๊ฑฐ๋๊ณ ์ ๋ฒํธ๋ ๋ฉฑ๋ฑํค์์ ๊ฒฐ์ ๋ก ์ ์ผ๋ก ๋์ถํ๋ค. ์ฌ์๋๋ง๋ค ๋์๋ฅผ ์๋ก ๋ง๋ค๋ฉด ์ํ ์ชฝ ์ค๋ณต ๋ฐฉ์ง๊ฐ ๋ฌด๋ ฅํด์ง๋ค.
์ผ์ผ ์ด์ฒด ํ๋๋ ์ธ ๋ฒ ๊ฐ์์์๋ค.
| ์๋ | ๊ฒฐ๊ณผ |
|---|---|
์กฐ๊ฑด๋ถ UPDATE ... WHERE total + ? <= limit |
๋์์ฑ ํ ์คํธ์์ ํ๋ 10๊ฑด์ 14๊ฑด ํต๊ณผ |
SELECT ... FOR UPDATE |
์ฌ์ ํ 12๊ฑด ํต๊ณผ (์์ง MVCC ๊ตฌํ ์์กด) |
@Version ๋๊ด์ ์ ๊ธ + ํธ๋์ญ์
๋ฐ ์ฌ์๋ |
ํต๊ณผ |
JPA์ @Version์ UPDATE์ WHERE์ ๋ฒ์ ์ ๋ฃ๊ณ ์ํฅ ํ ์๋ฅผ ํ์ธํ๋ ๋ฐฉ์์ด๋ผ, DB ์์ง์ ๊ฒฉ๋ฆฌ ์์ค ๊ตฌํ๊ณผ ๋ฌด๊ดํ๊ฒ lost update๋ฅผ ์ก์๋ธ๋ค. ๋์ด ๊ฑธ๋ฆฐ ๊ฒ์ฌ๋ฅผ ์์ง ๋์ ์ฐจ์ด์ ๋งก๊ธธ ์ ์๋ค๋ ๊ฒ์ด ์ด ๊ณผ์ ์์ ์ป์ ๊ฒฐ๋ก ์ด๋ค.
์ด์ฒด ์๋ฃ ์๋ฆผ์ ์ปค๋ฐ ์์ ์ ์ง์ ๋ณด๋ด๋ฉด, ์ปค๋ฐ์ ๋๋๋ฐ ๋ฐ์ก์ด ์คํจํ๊ฑฐ๋(์ ์ค) ๋ฐ์ก์ ๋๋๋ฐ ๋กค๋ฐฑ๋๋(์ ๋ น ์๋ฆผ) dual-write ๋ฌธ์ ๊ฐ ์๊ธด๋ค.
OutboxPublisher๋ MANDATORY ์ ํ๋ก ์ ์ธ๋์ด ํธ๋์ญ์
์์ด ํธ์ถํ๋ฉด ์์ธ๊ฐ ๋๋ค. ์์์ฑ ๋ณด์ฅ์ด ์กฐ์ฉํ ๊นจ์ง๋ ์ผ์ด ์๋ค. OutboxRelay๊ฐ ์ปค๋ฐ ์ดํ ๊บผ๋ด ๋ฐ์กํ๊ณ , ์คํจ๋ ์ง์ ๋ฐฑ์คํ๋ก ์ฌ์๋ํ๋ค๊ฐ ์์ง๋๋ฉด DEAD๋ก ๊ฒฉ๋ฆฌํ๋ค. ์กฐ์ฉํ ACKํ๊ณ ๋ฒ๋ฆฌ์ง ์๋๋ค.
๊ฐ์กฑ ๋ณดํธ ๊ธฐ๋ฅ์์ "์ด์ ์ด์ฒด๋ฅผ ์๋ฆฐ๋ค"๋ ๊ฒ๋ง์ผ๋ก๋ ํผํด๋ฅผ ๋ง์ง ๋ชปํ๋ค. ์๋ฆผ์ด ๊ฐ์ ๋๋ ์ด๋ฏธ ์ถ๊ธ์ด ๋๋ ๋ค๋ค.
VIEW_ALERT_AND_APPROVE ๊ถํ์ ๊ฐ์ง ๋ณดํธ์๊ฐ ์์ผ๋ฉด, ์๊ณ ๊ธ์ก ์ด์์ ์ด์ฒด๋ ์คํ๋์ง ์๊ณ ๋ณด๋ฅ๋๋ค. ๋ณดํธ์๊ฐ ์น์ธํด์ผ ์คํ๋๊ณ , ๊ฑฐ์ ํ๊ฑฐ๋ ๊ธฐํ์ด ์ง๋๋ฉด ์ทจ์๋๋ค. ์น์ธ์ "์คํํด๋ ์ข๋ค"๋ ํ๊ฐ์ผ ๋ฟ ํผ๋ณดํธ์์ ์์ฒด์ธ์ฆ์ ๋์ฒดํ์ง ์๋๋ค.
ํํด ์ "์ ๋ถ ์ฆ์ ์ญ์ "๋ ์ ์๊ธ์ต๊ฑฐ๋๋ฒ ์๋ฐ์ด๊ณ , "์๋ฌด๊ฒ๋ ์ ์ง์"์ ๊ฐ์ธ์ ๋ณด๋ณดํธ๋ฒ ์๋ฐ์ด๋ค. ์ด๋ ์ชฝ์ธ์ง ์ฝ๋๊ฐ ๋ช ์์ ์ผ๋ก ๋งํด์ผ ํ๋ค.
| ์ฆ์ | ๋ณด์กด ํ ํ๊ธฐ |
|---|---|
| CIยท์ ํ๋ฒํธยท์ด๋ฆยท์๋ ์์ผ ์ต๋ช ํ | ์ ์๊ธ์ต๊ฑฐ๋ ๊ธฐ๋ก (๊ธฐ๋ณธ 5๋ ) |
| ์ธ์ ยท๊ธฐ๊ธฐยท์์ฒด์ธ์ฆยท์์ ์ฐ๋ ํ๊ธฐ | ๊ฐ์ฌ ๋ก๊ทธ (ํด์ ์ฒด์ธ ๋ฌด๊ฒฐ์ฑ ์ ์ง) |
| ๋ง์ด๋ฐ์ดํฐ ์์ง ๊ณ์ข ๋ฐ์ดํฐ ํ๊ธฐ |
tbl_data_erasure_request๊ฐ "์ธ์ ๋ฌด์์ ์ง์ ๊ณ ๋ฌด์์ด ์ธ์ ๊น์ง ๋จ๋์ง"๋ฅผ ๊ธฐ๋กํ๋ฉฐ, ์ด ๊ธฐ๋ก ์์ฒด๊ฐ ์ฒ๋ฆฌ ์ฆ๋น์ด ๋๋ค.
๊ฐ ํ์ด ์ง์ ํ์ SHA-256 ํด์๋ฅผ ํ๋๋ค. ์ค๊ฐ ํ์ ๊ณ ์น๊ฑฐ๋ ์ง์ฐ๋ฉด ์ดํ ์ฒด์ธ์ด ์ ๋ถ ์ด๊ธ๋ ์ผ์ผ ๊ฒ์ฆ์์ ๋๋ฌ๋๋ค.
# ๋ก์ปฌ ์ ์ฒด ์คํ (์ฑ + MariaDB + Redis + ๊ด์ธก์ฑ)
docker network create global-nginx 2>/dev/null || true
cp .env.example .env # ๊ฐ ์ฑ์ฐ๊ธฐ
docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build| API | http://localhost:8080/health |
| Swagger | http://localhost:8080/api/v1/swagger-ui.html (dev๋ง) |
| Grafana | http://localhost:3001 |
| Prometheus | http://localhost:9090 |
./gradlew check # Spotless + ํ
์คํธ + ์ปค๋ฒ๋ฆฌ์ง ๊ฒ์ดํธ
./gradlew test # ํ
์คํธ๋งFlyway๋ก ๊ด๋ฆฌํ๋ค. V3__fresh_install_baseline.sql์ด ์ ์ฒด ์คํค๋ง๋ฅผ ๋ง๋ค๊ณ ์ดํ ๋ง์ด๊ทธ๋ ์ด์
์ด ๋ณ๊ฒฝ์ ์๋๋ค. ๋ชจ๋ ๋ฌธ์ฅ์ด IF NOT EXISTS์ด๊ณ ์ ์ฝ์ด CREATE TABLE ์์ ์ธ๋ผ์ธ๋์ด ์์ด, ํ
์ด๋ธ์ด ์ด๋ฏธ ์๋ ๊ธฐ์กด ๋ฐ์ดํฐ๋ฒ ์ด์ค์์๋ ๋ฒ ์ด์ค๋ผ์ธ ์ ์ฒด๊ฐ no-op์ด๋ค.
๋ฒ ์ด์ค๋ผ์ธ์ ์์ผ๋ก ๊ณ ์น์ง ์๋๋ค. ์ํฐํฐ์์ ์ฌ์์ฑํ๋ค.
./gradlew schemaDump && python3 tools/generate_baseline.py์ด์๊ณผ ๊ฐ์ DB ์์ง ์์์ Flyway ๋ง์ด๊ทธ๋ ์ด์
์ ์ ์ฉํ ๋ค ddl-auto=validate๋ก ๊ฒ์ฆํ๋ค. ์ํฐํฐ์ ๋ง์ด๊ทธ๋ ์ด์
์ด ์ด๊ธ๋๋ฉด ํ
์คํธ๊ฐ ์คํจํ๋ค.
Docker ์์ โ MariaDB ์ปจํ
์ด๋ + Flyway + validate (CI)
Docker ์์ โ H2(MariaDB ๋ชจ๋) + create-drop ์ผ๋ก ๊ฐ๋ฑ (๋ก์ปฌ ํธ์)
ํ ์คํธ๊ฐ ์ ์ ๊ฒฝ๋ก๋ณด๋ค ์คํจ ๊ฒฝ๋ก์ ์ง์ค๋์ด ์๋ค. ์ ์ ๊ฒฝ๋ก๋ ์๋๋ ๋์ํ๊ณ , ๋์ด ์๋ ๊ณณ์ ์คํจ ๊ฒฝ๋ก์๊ธฐ ๋๋ฌธ์ด๋ค.
| ๋์ | ๊ฒ์ฆ ๋ด์ฉ |
|---|---|
TransferServiceIdempotencyTests |
์ฌ์์ฒญยท์์ฒด์ธ์ฆ ์คํจยทํ๋ ์ด๊ณผยท์๋ต ๋ถ๋ช ์ ์ํ์ ํ๋ |
TransferLimitGuardConcurrencyTests |
40์ค๋ ๋ ๋์ ์์ฒญ์์ ํ๋ ์ด๊ณผ ๋ถ๊ฐ |
TransferReconciliationTests |
์ ์ฐ ํ์ ยท๋ฏธ์ถ๊ธ ์์๋ง ํ๋ ๋ณต๊ตฌ |
OutboxRelayTests |
์ฌ์๋ยทDEAD ๊ฒฉ๋ฆฌยท๊ฒฉ๋ฆฌ ํ ๋ฆด๋ ์ด ์ฐจ๋จ ์์ |
UserWithdrawalTests |
๋ฌด์์ด ์ง์์ง๊ณ ๋ฌด์์ด ๋จ๋์ง |
FieldEncryptorTests |
ํค ๋ถ์ผ์นยท๋ณ์กฐ ์ ์์ธ, ๋ ๊ฑฐ์ ํฌ๋งท ํธํ |
EnumColumnWidthTests |
enum ์์๊ฐ ์ปฌ๋ผ ๊ธธ์ด๋ฅผ ๋์ง ์๋์ง (DB ์์ด) |
SchemaMigrationDriftTests |
๋ง์ด๊ทธ๋ ์ด์ ์ ์ฉ ์ฌ๋ถยท์ ์ฝ ์กด์ฌ (MariaDB ์ ์ฉ) |
์ปค๋ฒ๋ฆฌ์ง ๊ฒ์ดํธ๋ ์ ์ฒด 33%, ๋ ๊ด๋ จ ํด๋์ค(TransferService, TransferLimitGuard, FieldEncryptor)๋ 70%๋ก CI์์ ๊ฐ์ ํ๋ค.
๋ฐฐ์น๋ ์ ๋ถ ShedLock์ผ๋ก ๋ณดํธ๋์ด ์ธ์คํด์ค๊ฐ ์ฌ๋ฌ ๋์ฌ๋ ํ ๋ฒ๋ง ๋๋ค. ์ค์ผ์ค ์ง์
์ ๊ณผ ์ค์ ๋ก์ง์ ๋ถ๋ฆฌํด ๋์ด(relay() / relayOnce()) ํ
์คํธ์์ ๋ฝ ์ธํ๋ผ ์์ด ๋ก์ง์ ์ง์ ๊ฒ์ฆํ ์ ์๋ค.
| ๋ฐฐ์น | ์ฃผ๊ธฐ | ์ญํ |
|---|---|---|
| ์์๋ฐ์ค ๋ฆด๋ ์ด | 1์ด | ์ด๋ฒคํธ ๋ฐํยท์ฌ์๋ยทDEAD ๊ฒฉ๋ฆฌ |
| ์ด์ฒด ์ ์ฐ | 30์ด | ๋ฏธ๊ฒฐ ์ด์ฒด๋ฅผ ์ํ์ ์ฌ์กฐํํด ํ์ |
| ๋ณดํธ์ ์น์ธ ๋ง๋ฃ | 1๋ถ | ๊ธฐํ ์ง๋ ์น์ธ ์์ฒญยท์ฃผ๋ฌธ ์ทจ์ |
| ๋ง์ด๋ฐ์ดํฐ ํ ํฐ ๊ฐฑ์ | 15๋ถ | ๋ง๋ฃ ์๋ฐ ํ ํฐ ๊ฐฑ์ |
| ๋์ ๋ง๋ฃ ์ฒ๋ฆฌ | 1์๊ฐ | ์์ง ์ค๋จ + ๋ฐ์ดํฐ ํ๊ธฐ, 7์ผ ์ ์๊ณ |
| ๊ฑฐ๋ ๋๊ธฐํ | ๋งค์ผ 04์ | ์คํ๋ฑ ํน ๊ฑฐ๋๋ด์ญ ์ ์ฌ |
| ๊ฐ์ฌ ์ฒด์ธ ๊ฒ์ฆ | ๋งค์ผ 04:30 | ํด์ ์ฒด์ธ ๋ฌด๊ฒฐ์ฑ |
| ๋ณด์กด๊ธฐ๊ฐ ๋ง๋ฃ ํ๊ธฐ | ๋งค์ผ 03์ | ๋ฒ์ ๋ณด์กด๊ธฐ๊ฐ ๊ฒฝ๊ณผ๋ถ ํ๊ธฐ |
์๋์ infra/observability/alert-rules.yml์ ์ ์๋์ด ์๊ณ , ๊ฐ ์๋์ ๋์ํ๋ ์กฐ์น API๊ฐ ์๋ค. ์์ธ๋ ์ด์ ๋ฐ๋ถ.
- ์ด์ ๋ฐ๋ถ โ ๋ฐฐํฌยท์ฅ์ ๋์ยท์๋๋ณ ์กฐ์น
- ๋ง์ด๋ฐ์ดํฐยท์คํ๋ฑ ํน ์ง์ ๋ฑ๋ก ๊ฐ์ด๋
์์งํ๊ฒ ๋จ๊ธด๋ค.
- ๋ถํ ๊ฒ์ฆ์ ํ์ง ์์๋ค. ์ปค๋ฅ์ ํ ํฌ๊ธฐ(20)์ HTTP ํด๋ผ์ด์ธํธ ์ค์ ์ ํฉ๋ฆฌ์ ๊ธฐ๋ณธ๊ฐ์ด์ง ์ค์ธก ๊ฒฐ๊ณผ๊ฐ ์๋๋ค.
- ํค ๋กํ
์ด์
๋ฐฐ์น๊ฐ ์๋ค.
FieldEncryptor๊ฐ ๋ฒ์ ๋ณ ๋ณตํธํ์needsReEncryption()์ ์ง์ํ์ง๋ง, ์ค์ ์ฌ์ํธํ๋ฅผ ์ํํ๋ ๋ฐฐ์น๋ ์์ง ์๋ค. ํ์ฌ๋ ๋กํ ์ด์ ํ ํค๊ฐ ์์ด ์ฐ์ ์์๋ฅผ ๋ฎ์ท๋ค. - ๋ก๊ทธ์ PII ๋ ธ์ถ์ ๊ฐ์ฌํ์ง ์์๋ค. ๊ณ์ข๋ฒํธยท์ ํ๋ฒํธ๊ฐ ๋ก๊ทธ์ ๋จ๋์ง ์ ์ ํ์ธ์ด ํ์ํ๋ค.
- ๋ฐฑ์
๋ณต๊ตฌ๋ฅผ ๋ฆฌํ์คํ์ง ์์๋ค.
infra/scripts/backup-mariadb.sh๋ ์์ผ๋ ๋ณต๊ตฌ ์ ์ฐจ๋ฅผ ์คํํด๋ณธ ์ ์ด ์๋ค.