์ค์๊ฐ ํ์จ ์ถ์ ๋ฐ AI ๊ธฐ๋ฐ ๋ถ์ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ฐฑ์๋ ๋ ํฌ.
Exchange Watch๋ ์ ์ธ๊ณ ํ์จ ์ ๋ณด๋ฅผ ์ค์๊ฐ์ผ๋ก ์์งํ๊ณ , ์ฌ์ฉ์์ ๊ด์ฌ ํตํ์์ ์ค์๊ฐ์ผ๋ก ๋ชจ๋ํฐ๋งํ๋ฉฐ, ์ผ๊ฐ ์ด๋ ฅ ์ง๊ณ ๋ฐ ์๋ฆผ๊น์ง ์ ๊ณตํ๋ ํ์คํ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋๋ค (์ด ์ ์ฅ์๋ ๋ฐฑ์๋๋ง).
- ์ ์ธ๊ณ ์ฃผ์ ํตํ์ ์ค์๊ฐ ํ์จ ์์ง ๋ฐ ์ค์๊ฐ ์คํธ๋ฆฌ๋ฐ ์ ๊ณต (WebSocket + SSE)
- ๊ด์ฌ ํตํ์ ๋ฑ๋ก ๋ฐ ์ค์๊ฐ ๋ชจ๋ํฐ๋ง
- ์ธ๋ถ API ํธ์ถ์ ์ต์ํํ๊ธฐ ์ํ Redis ๊ธฐ๋ฐ ์บ์ฑ ๋ฐ ์ญ์ฐ ์ต์ ํ
- ์ผ๊ฐ ํ์จ ๋ฐ์ดํฐ๋ฅผ OHLC ํํ๋ก ์๋ ์ง๊ณํ์ฌ ์ด๋ ฅ ์ ์ฅ
- ๋ชฉํ ๊ฐ๊ฒฉ ๋๋ฌ ์ ์๋ฆผ ์ ์ก ๊ธฐ๋ฅ (FCM ์ฐ๋ ์งํ์ค)
- ํ์จ ์ด๋ ฅ ๋ฐ์ดํฐ ์กฐํ ๋ฐ ์ฐจํธ ์๊ฐํ ์ง์
- ๋จ์/ํตํฉ ํ ์คํธ๋ฅผ ๊ฐ์ถ ์์ ์ ์ธ ์๋น์ค ๋ชฉํ
exchange-watch/
โโโ apps/
โ โโโ backend/ # NestJS ๋ฐฑ์๋ API ์๋ฒ
โ โโโ frontend/ # Next.js ํ๋ก ํธ์๋ ์ ํ๋ฆฌ์ผ์ด์
โโโ packages/
โ โโโ shared/ # ๊ณต์ ํ์
๋ฐ ์ ํธ๋ฆฌํฐ
โโโ package.json # ์ํฌ์คํ์ด์ค ์ค์
| ๋ฒ์ฃผ | ๊ธฐ์ | ์ฌ์ฉ์ฒ |
|---|---|---|
| Language | TypeScript | ์ ์ ํ์ ๊ธฐ๋ฐ์ผ๋ก ์์ ์ฑ๊ณผ ๊ฐ๋ ์ฑ ํ๋ณด |
| Framework | NestJS | ๋ชจ๋ ๊ธฐ๋ฐ ๊ตฌ์กฐ, DI๊ธฐ๋ฐ ์ค๊ณ, ์ด๋ฒคํธ ๊ธฐ๋ฐ ์ํคํ ์ณ ์ ์ฉ |
| Realtime | Websocket | ์ธ๋ถ API๋ก๋ถํฐ (Websocket)์ค์๊ฐ ํ์จ ๋ฐ์ดํฐ ์์ง |
| DB | PostgreSQL | ์ ๊ทํ๋ ์ด๋ ฅ ๋ฐ์ดํฐ ์ ์ฅ ๋ฐ ๊ด์ฌ ํตํ ๋ฆฌ์คํธ, ์ ์ ์ ๋ณด ๋ฐ ๋๋ฐ์ด์ค ํ ํฐ ๊ด๋ฆฌ, ์๋ฆผ |
| ORM | Prisma | type-safeํ DB์ ๊ทผ ๋ฐ ๋ง์ด๊ทธ๋ ์ด์ ์๋ํ ๋ฐ ํ ์คํธ ํ๊ฒฝ ๋ถ๋ฆฌ |
| Cache | Redis | ์์ง๋ ์ค์๊ฐ ํ์จ ์บ์ฑ ๋ฐ ์ธ๋ถ API ํธ์ถ ์ต์ํ |
| Messaging | Redis Pub/Sub | ํด๋ผ์ด์ธํธ ๊ด๋ฆฌ ๋ฐ Redis Pub/Sub์ ํตํ ๋ณ๋๋ ํตํ์๋ง ์ค์๊ฐ ๋ถ๋ถ ์ ์ก |
| AI | OpenAI, Agentica | ํ์จ ๋ถ์ ๋ฐ ์ธ์ฌ์ดํธ ์์ฑ |
| Infra | Docker, Docker Compose | ๋ก์ปฌ, ํ ์คํธ, ๋ฐฐํฌ ํ๊ฒฝ ์ผ๊ด์ฑ ํ๋ณด |
| Test | Jest, Supertest | ๋จ์/ํตํฉ ํ ์คํธ ๋ฐ ์ธ๋ถ ์์กด์ฑ ํ ์คํธ |
- CoinAPI WebSocket์ ํตํด ๊ธฐ์ค ํตํ ํ์จ ๋ฐ์ดํฐ ์์
- ์์ ์ฆ์
ExchangeRateReceivedEvent์ด๋ฒคํธ ๋ฐ์ - Redis์ ์ ์ฅ ์ , ์ง์ ํ์จ๊ณผ ๋น๊ตํ์ฌ ๋ณ๋ ์ฌ๋ถ ํ์ธ
- ๋ณ๋์ด ์๋ ๊ฒฝ์ฐ์๋ง Redis์ ์ ์ฅ + Redis Pub/Sub ์ฑ๋์ publish
- ํด๋ผ์ด์ธํธ๋ REST ๋๋ SSE๋ฅผ ํตํด Redis์์ ์ต์ ํ์จ ์์
- WebSocket โ Redis โ SSE ๊ตฌ์กฐ๋ก ์ค์๊ฐ์ฑ๊ณผ ํ์ฅ์ฑ์ ๋์์ ๊ฐ์ ธ๊ฐ
- ์์ง๊ณผ ์๋ต์ ๋ถ๋ฆฌํ์ฌ API ํธ์ถ ์ ์ธ๋ถ ํต์ ์์ด ์บ์์์ ์๋ต
- ์ด๋ฒคํธ ์ค์ฌ ํ๋ฆ์ผ๋ก ํ ์คํธํ๊ธฐ ์ฌ์ด ๊ตฌ์กฐ
- ์ธ๋ถ API๋ฅผ ์๋ฒ 1ํ ์ฐ๊ฒฐ๋ก๋ง ์์ , ๋ชจ๋ ํด๋ผ์ด์ธํธ๋ Redis๋ง ์กฐํํจ (์ต์ข ๋ชฉํ)
- ์ด์ ๊ฐ๊ณผ์ ๋น๊ต๋ฅผ ํตํด ๋ณ๋์ด ์์ ๋๋ง Pub/Sub ๋ฐ์ โ ๋ถํ์ํ ์คํธ๋ฆฌ๋ฐ ๋ฐฉ์ง
> ํ๋ฃจ์ ํ ๋ฒ, ์ธ๋ถ API๋ฅผ ํธ์ถํด ์ ์ผ ์์ง๋ ํ์จ๋ค์ ์ ์ ๋ OHLC ๋ฐ์ดํฐ๋ก ์ง๊ณํ๊ณ DB์ ์ ์ฅํฉ๋๋ค.
- ๋งค์ผ 00:05 (UTC)์ Cron ์ค์ผ์ค๋ฌ๊ฐ ์ง๊ณ ํ๋ก์ธ์ค ์คํ
- ๊ธฐ์ค ํตํ ๊ธฐ์ค์ผ๋ก Fluctuation API๋ฅผ ํธ์ถํด ์ ์ผ ๋ณ๋ ๋ฐ์ดํฐ ์์ง
exchange_rates_dailyํ ์ด๋ธ์ ์ ์ฅ
- ์ฐจํธ์ฉ ๋ฐ์ดํฐ๋ ์ค์๊ฐ ์์ง์ด ์๋, ์ผ๊ด๋ ์ง๊ณ ๊ธฐ์ค์ผ๋ก ๊ด๋ฆฌ
- Fluctuation API๋ ํ๋ฃจ 1ํ๋ง ํธ์ถ โ ์ธ๋ถ API์์ฒญ ์ต์ํ
- ์ค์๊ฐ ๊ตฌ์กฐ์ ๋ถ๋ฆฌ๋์ด ์์ด ๋ฌธ์ ๋ฐ์ ์ ๋ค๋ฅธ ๊ธฐ๋ฅ์ ์ํฅ ์์
> ํด๋ผ์ด์ธํธ๋ ๊ธฐ์ค ํตํ๋ฅผ ์ง์ ํ์ฌ SSE(Server-Sent Events)๋ก ์ค์๊ฐ ํ์จ์ ๊ตฌ๋
> ์๋ฒ๋ Redis Pub/Sub ์ฑ๋์ ํตํด ํ์จ ๋ณ๋์ด ๊ฐ์ง๋ ๊ฒฝ์ฐ์๋ง ํด๋ผ์ด์ธํธ์ ์ค์๊ฐ ํธ์ํฉ๋๋ค.
- ํด๋ผ์ด์ธํธ๊ฐ ๊ธฐ์ค ํตํ๋ฅผ ์ง์ ํ์ฌ SSE ๊ตฌ๋ ์์ฒญ
- ์๋ฒ๋ Redis Pub/Sub ์ฑ๋์ ๊ตฌ๋ (latest-rate-pub:KRW)
- ์ธ๋ถ WebSocket์ผ๋ก ์์ ๋ ํ์จ์ด ๋ณ๋๋ ๊ฒฝ์ฐ์๋ง Redis์ ์ ์ฅ + Pub ์ฑ๋์ publish
- SSE ์คํธ๋ฆผ์ ํตํด ํด๋น ํด๋ผ์ด์ธํธ์๊ฒ๋ง ์ค์๊ฐ ๋ฐ์ดํฐ ์ ์ก
- WebSocket ์์ด ๋ธ๋ผ์ฐ์ ์นํ์ ์ธ ์ค์๊ฐ ๊ตฌ์กฐ ๊ตฌํ
- ๋ณ๋์ด ๋ฐ์ํ ๊ฒฝ์ฐ์๋ง ์ด๋ฒคํธ ์ ํ โ ํธ๋ํฝ ์ต์ํ
- Redis Pub/Sub์ ํตํด ์๋ฒ ์ธ์คํด์ค ๊ฐ ํ์ฅ์๋ ๋์ ๊ฐ๋ฅ
- ๊ธฐ์ค ํตํ๋ณ ์ฑ๋ ๋ถ๋ฆฌ๋ก ๊ด์ฌ ๋ฐ์ดํฐ๋ง ํจ์จ์ ์ผ๋ก ์ ์ก
- ์ด๋ฏธ ์ ์ฅ๋ OHLC ๋ฐ์ดํฐ๋ ๊ทธ๋๋ก ์ฌ์ฌ์ฉ โ ์๋ต ์๋ ํ๋ณด
- ์ด๊ธฐ์๋ ๋ชจ๋ ํตํ์(31 * 30 = 930)์ Redis์ ์ ์ฅํ๋ ค๊ณ ํ์ง๋ง
- ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋๊ณผ Pub/Sub ํธ๋ํฝ์ด ๋นํจ์จ์ ์ด๋ผ๋ ๋ฌธ์ ๊ฐ ์์๊ณ ,
- ๊ธฐ์ค ํตํ(KRW)๋ฅผ ๊ธฐ์ค์ผ๋ก 30๊ฐ๋ง ์ ์ฅํ๊ณ , ๋๋จธ์ง๋ ์์ฒญ ์ ์ ์ฅ๋ ๋ฐ์ดํฐ๋ฅผ ์ด์ฉํ์ฌ ์ญ์ฐ ์ฒ๋ฆฌํ๋๋ก ๊ตฌ์กฐ๋ฅผ ๋ณ๊ฒฝ
- redis Hash ์๋ฃ๊ตฌ์กฐ๋ฅผ ์ด์ฉํ์ฌ ๋ณ๋๋ฅ ์ด ์กด์ฌํ์ง ์ต์ ํ์จ์
- ์ต์ ํ์จ ์์ง ์, ๋ณ๋๋ฅ ์ด (๊ต์ฅํ)๋ฏธ๋ฏธํ๋ค๋ฉด timestamp๋ง ์
๋ฐ์ดํธ ํด์ฃผ๊ธฐ ์ํด
redis hash์๋ฃ๊ตฌ์กฐ๋ฅผ ์ฑํํด ์ฆ์ ๋ถ๋ถ ์ ๋ฐ์ดํธ์ ๋ํ ์ฑ๋ฅ์ ์ปค๋ฒํ์์ต๋๋ค โ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ๋ํญ ์ ๊ฐ, API ์๋ต ์๋ ๊ธฐ์กด๋๋น 80% ํฅ์
๋ค์ด๊ฐ๊ธฐ ์ ์, ์ญ์ฐ ์ต์ ํ ๊ตฌ์กฐ๋ ์ญ์ฐ์ ์ ํ๋์ ํ๊ณ์ฑ๋๋ฌธ์ ์ฒ ํํ์์ ๊ด๋ จ ์ด์
- ๋ค์ด๊ฐ๊ธฐ ์ ์, ์ฐ๋ฆฌ ์๋ฒ๋ KRW/30๊ฐ์ ํตํ์ = ์ด 30๊ฐ์ ๋จ์ผํตํ์์ ๊ฐ์ง๊ณ ์์ต๋๋ค (N์ด๋ง๋ค ์์ง ๋ฐ ์ ๋ฐ์ดํธ).
- ๋ง์ฝ ์ฌ์ฉ์๊ฐ USD/EUR์ ํตํ์์ ์์ฒญํ๋ค๋ฉด?
# Redis์ ์ ์ฅ๋ ๊ฐ (์ฐ๋ฆฌ๋ ํญ์ ๋ค๊ณ ์์ต๋๋ค)
KRW โ USD = 0.000699
KRW โ EUR = 0.000616
# ์์ฒญ: USD/EUR
rate = 0.000616 / 0.000699 โ 0.8813# ๋ณ๋๋ฅ ์ญ์ฐ
KRW โ EUR ๋ณ๋๋ฅ : +1.20%
KRW โ USD ๋ณ๋๋ฅ : +0.85%
USD โ EUR ๋ณ๋๋ฅ โ 1.20% - 0.85% = 0.35%- ๋ค์ํ ํ์จ API๋ฅผ ๊ธฐ๋ฅ๋ณ๋ก ์ ์ฐํ๊ฒ ์ฌ์ฉํด์ผ ํ๊ณ ,
- API๋ง๋ค ์๋ต ํฌ๋งท๊ณผ ๊ธฐ๋ฅ ๋ฒ์๊ฐ ๋ฌ๋ผ, ์ง์ ์๋น์ค์ ์ฐ๊ฒฐํ๋ฉด ๊ฒฐํฉ๋๊ฐ ๋์์ง๋ ๋ฌธ์ ๊ฐ ์์์
- ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ์๋น์ค์์ ํ์ํ ๋์(์: getCurrentRate, getFluctuationData ๋ฑ)์ ๋ช
์ํ ๊ณตํต ์ธํฐํ์ด์ค(
IExchangeRateExternalAPI)๋ฅผ ์ ์ - ๋ชจ๋ ์ธ๋ถ API๋ ํด๋น ์ธํฐํ์ด์ค๋ฅผ ๋ฐ๋ฅด๊ฒ ๊ตฌํํ๊ณ , ExternalApiModule์์ ๊ฐ์๋ผ์ฐ๋ ์์ผ๋ก(DI) ์ํ๋ API๋ฅผ ์ฃผ์ ํด์ ์ฌ์ฉํ๋๋ก ์ค๊ณ โ ๊ธฐ๋ฅ ํ์ฅ, ํ ์คํธ์ฉ Mock ์ฃผ์ , API ๊ต์ฒด ๋ฑ ๋ชจ๋ ์ํฉ์์ ์ ์ฐํ๊ฒ ๋์ ๊ฐ๋ฅ
- ์ค์๊ฐ ํ์จ ์์ (WebSocket)๊ณผ ๋น์ฆ๋์ค ๋ก์ง(์๋ฆผ ๋น๊ต, ์ ์ฅ)์ด ์๋ก๋ฅผ ์ฐธ์กฐํ๋ฉด์, ExternalWebSocketGateway โ ExchangeRateService ๊ฐ ์ํ ์์กด์ฑ ๋ฐ์
- ๋จ์ํ forwardRef()๋ก ํด๊ฒฐํ ์๋ ์์์ง๋ง, ์ด๋ ๊ตฌ์กฐ์ ํด๊ฒฐ์ด ์๋
- ๋์ WebSocket ๊ฒ์ดํธ์จ์ด๋ ๋จ์ํ ๋ฐ์ดํฐ๋ฅผ ์์ ํ๊ณ ,
๋ด๋ถ์ ์ผ๋ก
ExchangeRateReceivedEvent๋ฅผ ๋ฐํํ๋๋ก ์ค๊ณ ๋ณ๊ฒฝ - ExchangeRateService๋ ์ด ์ด๋ฒคํธ๋ฅผ ๊ตฌ๋ ํ์ฌ, Redis ์ ์ฅ ๋ฐ ์๋ฆผ ๋ก์ง์ ์ฒ๋ฆฌ
โ ๊ฒฐ๊ณผ์ ์ผ๋ก
- WebSocket ๋ชจ๋๊ณผ ๋น์ฆ๋์ค ๋ก์ง ๋ชจ๋ ๊ฐ ๊ฒฐํฉ ์ ๊ฑฐ
- ์ธ๋ถ ๋ฐ์ดํฐ ์์ โ ๋ด๋ถ ๋ก์ง ์ฒ๋ฆฌ ํ๋ฆ์ด ๋จ๋ฐฉํฅ์ด ๋จ
- ํ ์คํธ, ์ ์ง๋ณด์, ํ์ฅ์ฑ์ด ๋ชจ๋ ๊ฐ์ ๋จ
- ์๋น์ค ๋ก์ง์ mock ๊ธฐ๋ฐ ๋จ์ ํ ์คํธ๋ก ์ถฉ๋ถํ ์ปค๋ฒ๋์ง๋ง, Controller, Repository ๋จ์ ํ ์คํธ ์ธํ๋ผ์ ํจ๊ป ๊ฒ์ฆํ ํ์๊ฐ ์์์ต๋๋ค
- ๊ทธ๋ฌ๋ ํ ์คํธ ์ค PostgreSQL/Redis ์์กด์ฑ ์ถฉ๋, ๋ฐ์ดํฐ ์ค์ผ ๋ฌธ์ ๋ฐ์
- ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ํ ์คํธ ํ๊ฒฝ์ ์์ ํ ๊ฒฉ๋ฆฌ์์ผ ๊ตฌ์ฑ:
- ๋จ์ ํ ์คํธ๋ Jest๋ก ์๋น์ค ๋ ์ด์ด๋ง mockํ์ฌ ๋น ๋ฅด๊ฒ ํต์ฌ ๋น์ฆ๋์ค ๋ก์ง๋ง ์คํ
- docker-compose๋ฅผ ์ด์ฉํ์ฌ ์ค์ ํ๋ก๋์ ํ๊ฒฝ๊ณผ ๋์ผํ PostgreSQL/Redis ํ๊ฒฝ์์ ์คํ
- ํตํฉ ํ
์คํธ ์คํ ์ Prisma
migrate reset์ผ๋ก ๋งค ํ ์คํธ๋ง๋ค DB ์คํค๋ง ์ด๊ธฐํ - NestJS ๋ชจ๋์ ํ
์คํธ ์ ์ฉ์ผ๋ก ๋ถ๋ฆฌ ๊ตฌ์ฑ (
test-prisma.module.ts,test-redis.module.ts,test-cls.module.ts,test-config.module.ts)
โ ๊ฒฐ๊ณผ์ ์ผ๋ก
- ๋น ๋ฅด๊ณ ์์ ์ ์ธ ํ ์คํธ ํ๊ฒฝ์ ๊ตฌ์ถ
- ๊ตฌ์กฐ ๋ฆฌํฉํ ๋ง/๋ณ๊ฒฝ์๋ ์์ ์๊ฒ ๋์ํ ์ ์๋ ๊ธฐ๋ฐ์ ๋ง๋ จ