HotDeal์ ์ค์๊ฐ ํ ์ธ ์ด๋ฒคํธ ๊ด๋ฆฌ ๋ฐ ์ฃผ๋ฌธ ์ฒ๋ฆฌ๋ฅผ ์ํ ์์คํ ์ ๋๋ค.
- ๋์์ฑ ์ ์ด๋ฅผ ์ํ Redisson ๊ธฐ๋ฐ ๋ถ์ฐ ๋ฝ
- WebSocket์ ํ์ฉํ ์ค์๊ฐ ์ด๋ฒคํธ ์๋ฆผ
- ๋๋ฉ์ธ ๊ฐ ๋ถ๋ฆฌ์ ํฅํ ํ์ฅ์ฑ์ ๊ณ ๋ คํด ๋ด๋ถ API ํต์ ๋ฐฉ์ ์ ์ฉ
์ด ํ๋ก์ ํธ๋ ๋ณต์กํ ๋น์ฆ๋์ค ๋ก์ง์ ๋ช ํํ๊ฒ ๊ตฌ์กฐํํ๊ธฐ ์ํด **๋๋ฉ์ธ ์ฃผ๋ ์ค๊ณ(DDD)**๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ ์ฒด ์ํคํ ์ฒ๋ฅผ ๊ตฌ์ฑํ์ต๋๋ค.
ํนํ ๊ฐ์ฅ ๋ง์ ์๊ฐ๊ณผ ๊ณ ๋ฏผ์ ํฌ์ํ ๋ถ๋ถ์ ๋๋ฉ์ธ ๊ฐ์ ์ฑ ์์ ๊ตฌ๋ถํ๊ณ ๊ทธ ๊ฒฝ๊ณ๋ฅผ ๋ช ํํ ์ ์ํ๋ ๊ฒ์ด์์ต๋๋ค.
์ด๋ฒคํธ, ์ํ, ์ฃผ๋ฌธ๊ณผ ๊ฐ์ ๋๋ฉ์ธ์ ๊ฐ๊ฐ ๋ ๋ฆฝ์ ์ธ ๋น์ฆ๋์ค ๊ฐ๋ ์ ๊ฐ์ง๊ณ ์์ผ๋ฉฐ ์ด๋ค์ด ๊ต์ฐจํ๋ ์ง์ ์์๋ ๋ฐ์ดํฐ ์์กด์ฑ๊ณผ ๋ณ๊ฒฝ ์ ํ์ ์ํ์ด ํญ์ ์กด์ฌํฉ๋๋ค.
์ด์ ๋ฐ๋ผ ์ฐ๋ฆฌ๋ ๋๋ฉ์ธ ์ฃผ๋ ์ค๊ณ(DDD) ๋ฅผ ์ ํํ์๊ณ ์ด๋ฅผ ํตํด ๋ค์๊ณผ ๊ฐ์ ํจ๊ณผ๋ฅผ ์ป๊ณ ์ ํ์ต๋๋ค.
- ๋ถ๋ฆฌ๋ ๊ฐ๋ฐ ํ๊ฒฝ ๊ตฌ์ถ
- ๋๋ฉ์ธ ๊ฐ ์ฑ ์๊ณผ ์ญํ ๋ช ํํ
- ๋ถํ์ํ ๋ณ๊ฒฝ ์ ํ ์ฐจ๋จ
- ๋น์ฆ๋์ค ๊ท์น๊ณผ ํ๋ฆ์ด ๋ฐ์๋ ๋ชจ๋ธ๋ง
- ํ์ค ์ ๋ฌด์ ์ ์ฌํ ๊ตฌ์กฐ๋ก ํ์ ํจ์จ ํฅ์
์ด ํ๋ก์ ํธ์์๋ ์๋ฒ ๊ฐ ํต์ ์ ์ํด Spring์ RestTemplate์ ์ ํํ์ต๋๋ค.
๋น๋ก RestTemplate์ ๋ ๊ฑฐ์๋ก ๋ถ๋ฅ๋๋ฉฐ WebClient๊ฐ ๊ณต์์ ์ผ๋ก ๊ถ์ฅ๋๋ ์ถ์ธ์ด์ง๋ง ์ด๋ฒ ํ๋ก์ ํธ์์๋ ๋ค์๊ณผ ๊ฐ์ ์ด์ ๋ก RestTemplate์ ์ฐ์ ๋์ ์ ๊ฒฐ์ ํ์ต๋๋ค.
RestTemplate์ ๋๊ธฐ/๋ธ๋กํน ๊ธฐ๋ฐ์ผ๋ก ๋์ํ๊ธฐ ๋๋ฌธ์ ์ ์ฒด ํ๋ฆ์ ์ง๊ด์ ์ผ๋ก ํ์
ํ ์ ์์ด
๋๋ฒ๊น
๋ฐ ๋ฌธ์ ์ถ์ ์ด ์๋์ ์ผ๋ก ์์ํฉ๋๋ค.
WebClient๋ ๋น๋๊ธฐ/๋
ผ๋ธ๋กํน ๊ธฐ๋ฐ์ผ๋ก ๊ณ ์ฑ๋ฅ ์ฒ๋ฆฌ์ ์ ํฉํ์ง๋ง ์ด๊ธฐ ํ์ต ๋น์ฉ์ด ํฌ๊ณ
์์ธ ์ฒ๋ฆฌ๋ ์ฅ์ ์ถ์ ์ ๋ํ ์ง์
์ฅ๋ฒฝ์ด ๋์ ์ ์์ต๋๋ค.
ํ๋ก์ ํธ ์ด๋ฐ์๋ ํต์ฌ ๋๋ฉ์ธ ์ค๊ณ ๋ฐ ํ๋ฆ ํ์
์ ์ง์ค์ ์ํด ํต์ ๋ฐฉ์์ ๋ํ ๋ณต์ก๋๋ ์ต์ํํ๊ณ ์ ํ์ต๋๋ค.
ํฅํ ํ์ฅ ๋๋ ์ฑ๋ฅ ์ต์ ํ๊ฐ ํ์ํ ๊ตฌ๊ฐ์์๋ WebClient๋ก์ ์ ํ์ ๊ณ ๋ คํ ์ ์๋๋ก ์ ์ฐํ ๊ตฌ์กฐ๋ก ์ค๊ณํ์์ต๋๋ค.
์ด ํ๋ก์ ํธ์์๋ ๋๋ ์ฌ์ฉ์์๊ฒ ์์ ์ ์ผ๋ก ์๋ฆผ์ ์ ์กํ ์ ์๋ ๊ตฌ์กฐ๋ฅผ ๋ชฉํ๋ก ์ค์๊ฐ ์๋ฆผ ์์คํ ์ ์ค๊ณํ์ต๋๋ค.
์ด๊ธฐ์๋ ์๋ฒ๊ฐ ์ ํ์ ๊ตฌ๋ ์ค์ธ ๋ชจ๋ ์ฌ์ฉ์์๊ฒ ์น์์ผ์ ํตํด ์ง์ ์๋ฆผ์ ํธ์ํ๋ ๋ฐฉ์์ด์๊ณ ์ด๋ ์๊ท๋ชจ ํธ๋ํฝ ํ๊ฒฝ์์๋ ์ ๋์ํ์ต๋๋ค. ๊ทธ๋ฌ๋ ๊ตฌ๋ ์๊ฐ ๋ง์์ง๋ ์ํฉ์์๋ ๋ค์๊ณผ ๊ฐ์ ๋ฌธ์ ์ ์ด ๋ฐ์ํ ์ ์์์ ๊ณ ๋ คํ์ต๋๋ค.
- ์ฐ๊ฒฐ ํ(pool) ํ๊ณ๋ก ์ธํด ๋์ ์ฐ๊ฒฐ ์ ์ ํ
- ๋๊ธฐ ์ฒ๋ฆฌ ์ง์ฐ์ผ๋ก ์ธํ ์๋ต ์๋ ์ ํ
- ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ์ฆ๊ฐ ๋ฐ GC ๋ถํ
- ๊ตฌ๋ ์ ๋ชฉ๋ก ์กฐํ ์์ ์ ์ฑ๋ฅ ์ ํ
์ด๋ฌํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ ์ ๋ต์ ์ค๊ณํ๊ณ ๋ฐ์ํ์ต๋๋ค.
DB ์ธก๋ฉด์์๋ ์ธ๋ฑ์ฑ๊ณผ ์บ์ฑ์ ํตํด ์กฐํ ์ฑ๋ฅ์ ํ๋ณดํ๊ณ ์น์์ผ ์ฒ๋ฆฌ ๋ก์ง์ ๋ฐฐ์น ์ ์ก ๋ฐ ๋ณ๋ ฌ ์ฒ๋ฆฌ๋ก ๋ถ๋ด์ ๋ถ์ฐํ์ต๋๋ค.
ํ์ง๋ง ์น์์ผ ์ฐ๊ฒฐ ์ ์ ํ์ ์ ํ๋ฆฌ์ผ์ด์ ์ฐจ์์์ ๊ทผ๋ณธ์ ์ผ๋ก ํด๊ฒฐํ ์ ์๋ ๊ตฌ์กฐ์ ์ ์ฝ์ด์์ต๋๋ค. ์ด์ ๋ฐ๋ผ ์ค์๊ฐ ์๋ฆผ ๊ตฌ์กฐ๋ฅผ ํด๋ผ์ด์ธํธ ์ฃผ๋ ๋ฐฉ์์ผ๋ก ์ ํํ๋ ๋ฐฉํฅ์ผ๋ก ์ฌ์ค๊ณํ์ต๋๋ค.
์ต์ข ์ ์ผ๋ก๋ ๋ค์๊ณผ ๊ฐ์ ๋ฐฉ์์ผ๋ก ์๋ฆผ ์์คํ ์ ๊ตฌ์ฑํ์ต๋๋ค.
- ์๋ฒ๋ โ์ ํ์ ์๋ก์ด ์ด๋ฒคํธ๊ฐ ๋ฑ๋ก๋์์์ ์๋ฆฌ๋ ์ ํธโ๋ง ์ ์ก
- ํด๋ผ์ด์ธํธ๋ ํด๋น ์ ํธ๋ฅผ ์์ ํ ๋ค ํ์ํ ์๋ฆผ ๋ฐ์ดํฐ๋ฅผ ์ง์ ์กฐํ
์ด๋ฌํ ๊ตฌ์กฐ๋ ๋ค์๊ณผ ๊ฐ์ ์ฅ์ ์ ๊ฐ์ง๋๋ค.
- ์๋ฒ๊ฐ ๋ชจ๋ ๊ตฌ๋ ์์๊ฒ ์ง์ ๋ฐ์ดํฐ๋ฅผ ์ ์กํ์ง ์๊ธฐ ๋๋ฌธ์ ์น์์ผ ์ฐ๊ฒฐ ์ ์ ํ ๋ฌธ์ ๋ฅผ ํจ๊ณผ์ ์ผ๋ก ํํผ
- ํด๋ผ์ด์ธํธ๊ฐ ํ์ํ ์์ ์ ํ์ํ ๋ฐ์ดํฐ๋ง ์์ฒญํ์ฌ ๋ฆฌ์์ค ํจ์จ์ฑ ์์น
- ์ ์ฒด ์์คํ ์ ํ์ฅ์ฑ๊ณผ ์์ ์ฑ ํฅ์
์ด ํ๋ก์ ํธ๋ ์ธ์ฆ ์ ๋ณด(Auth)์ ์ฌ์ฉ์ ํ๋กํ(User)์ ์๋ก ๋ค๋ฅธ ๋ชฉ์ ๊ณผ ์ฑ ์์ ๊ฐ์ง ํ ์ด๋ธ๋ก ๋ถ๋ฆฌํ์ฌ ๊ด๋ฆฌํฉ๋๋ค. Auth๋ ์ฌ์ฉ์ ์ธ์ฆ์ ํ์ํ ์ ๋ณด๋ฅผ ๊ด๋ฆฌํ๊ณ User๋ ์ฌ์ฉ์ ์กฐํ ์ ์ฉ ๋ฐ์ดํฐ๋ฅผ ์ ๊ณตํ๋ Read Model ์ญํ ์ ์ํํฉ๋๋ค.
๋ ํ
์ด๋ธ์ ๋๋ฉ์ธ ์ด๋ฒคํธ ๊ธฐ๋ฐ์ผ๋ก ์ฐ๋๋๋ฉฐ ์๋ฅผ ๋ค์ด ํ์๊ฐ์
์ Auth ์ ์ฅ ์ดํ UserRegisteredEvent๊ฐ ๋ฐํ์ ํตํด
์ด๋ฅผ ๊ตฌ๋
ํ๋ ๋ฆฌ์ค๋๊ฐ User ๋ฐ์ดํฐ๋ฅผ ์์ฑํฉ๋๋ค.
์ด๋ฒคํธ ๊ธฐ๋ฐ ์ํคํ ์ฒ๋ ๋น๋๊ธฐ์ ์ด๋ฉฐ ์ ์ฐํ์ง๋ง ๋น์ ์์ ์ธ ํ๋ฆ(์: ๋ฆฌ์ค๋ ์คํจ)์ผ๋ก ์ธํด ์ผ๋ถ ๋ฐ์ดํฐ๊ฐ ๋๋ฝ๋๊ฑฐ๋ ์ผ๊ด์ฑ์ด ์ด๊ธ๋ ๊ฐ๋ฅ์ฑ๋ ์กด์ฌํฉ๋๋ค.
์ด๋ฅผ ๋ณด์ํ๊ธฐ ์ํด ๋ณด์ ํธ๋์ญ์ ํจํด์ ์ํคํ ์ฒ์ ํฌํจ์์ผฐ์ต๋๋ค.
User ์ ์ฅ ๋ฆฌ์ค๋์์ ์์ธ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ UserSaveFailedEvent๋ฅผ ๋ฐํํด Auth ๋ฐ์ดํฐ๋ฅผ ๋๋๋ฆฌ๋ ๋ณด์ ๋ก์ง์ ์คํํฉ๋๋ค.
- ํ ์ธ ์ด๋ฒคํธ ์์ฑ ๋ฐ ๊ด๋ฆฌ
- ์ํ๋ณ ์ต์ ํ ์ธ์จ ์๋ ๊ณ์ฐ
- ๋ง๋ฃ๋ ์ด๋ฒคํธ ์๋ ์ญ์ (์ค์ผ์ค๋ฌ)
- ๋ค์ค ์ํ ์ฃผ๋ฌธ ์ง์
- ์ด๋ฒคํธ ํ ์ธ๊ฐ ์๋ ์ ์ฉ
- ์ฃผ๋ฌธ ์ํ ๊ด๋ฆฌ (ORDER_BEFORE, ORDER_PENDING, ORDER_SUCCESS, ORDER_FAILURE)
- Redisson์ ํ์ฉํ ๋ถ์ฐ ๋ฝ ๊ตฌํ
- ๋์์ฑ ์ ์ด๋ฅผ ํตํ ์ฌ๊ณ ์ฐจ๊ฐ
- ์ฌ๊ณ ๋ถ์กฑ ์ ์๋ ์ฃผ๋ฌธ ์คํจ ์ฒ๋ฆฌ
- WebSocket์ ํตํ ์ค์๊ฐ ์ด๋ฒคํธ ์๋ฆผ
- Spring Event๋ฅผ ํ์ฉํ ๋น๋๊ธฐ ์ฒ๋ฆฌ
- Java 17
- Spring Boot 3.5.3
- Spring Data JPA
- Spring Security
- Spring WebSocket
- MySQL
- Redis
- Redisson
- JWT
- Lombok
- TestContainers
ํ๋ก์ ํธ ๊ตฌ์กฐ
com.example.hotdeal
โโโ domain
โ โโโ common
โ โ โโโ client # ๋ด๋ถ API ํด๋ผ์ด์ธํธ
โ โ โ โโโ event
โ โ โ โโโ product
โ โ โ โโโ stock
โ โ โโโ springEvent # Spring Event ์ ์
โ โโโ event # ์ด๋ฒคํธ ๋๋ฉ์ธ
โ โ โโโ api
โ โ โโโ application
โ โ โโโ domain
โ โ โโโ infra
โ โโโ order # ์ฃผ๋ฌธ ๋๋ฉ์ธ
โ โ โโโ api
โ โ โโโ application
โ โ โโโ domain
โ โ โโโ infra
โ โโโ stock # ์ฌ๊ณ ๋๋ฉ์ธ
โ โ โโโ api
โ โ โโโ application
โ โ โโโ domain
โ โ โโโ infra
โ โโโ notification # ์๋ฆผ ๋๋ฉ์ธ
โ โ โโโ application
โ โ โโโ domain
โ โ โโโ infra
โ โโโ user # ์ฌ์ฉ์ ๋๋ฉ์ธ
โ โโโ auth
โโโ global
โโโ config # ์ ์ญ ์ค์
โโโ enums # ๊ณตํต ์ด๊ฑฐํ
โโโ exception # ์์ธ ์ฒ๋ฆฌ
โโโ lock # ๋ถ์ฐ ๋ฝ ๊ตฌํ
โโโ model # ๊ณตํต ๋ชจ๋ธ
์ ์ฒด API ๊ฐ์
| API | Method | Endpoint | ๊ถํ | ์ค๋ช |
|---|---|---|---|---|
| ํ์๊ฐ์ | POST | /api/auth/signup |
PUBLIC | ์๋ก์ด ์ฌ์ฉ์ ํ์๊ฐ์ |
| ๋ก๊ทธ์ธ | POST | /api/auth/login |
PUBLIC | ์ฌ์ฉ์ ๋ก๊ทธ์ธ ๋ฐ ํ ํฐ ๋ฐ๊ธ |
| ํ ํฐ ์ฌ๋ฐ๊ธ | POST | /api/auth/reissue |
PUBLIC | Access Token ์ฌ๋ฐ๊ธ |
| ๋ก๊ทธ์์ | POST | /api/auth/logout |
USER | ์ฌ์ฉ์ ๋ก๊ทธ์์ |
| ํ์ํํด | POST | /api/auth/withdraw |
USER | ์ฌ์ฉ์ ๊ณ์ ๋นํ์ฑํ |
| ๊ณ์ ๋ณต๊ตฌ | POST | /api/auth/{authId}/restore |
ADMIN | ํํดํ ๊ณ์ ๋ณต๊ตฌ |
| API | Method | Endpoint | ๊ถํ | ์ค๋ช |
|---|---|---|---|---|
| ๋ด ์ ๋ณด ์กฐํ | GET | /api/users/me |
USER | ํ์ฌ ๋ก๊ทธ์ธํ ์ฌ์ฉ์ ์ ๋ณด ์กฐํ |
| API | Method | Endpoint | ๊ถํ | ์ค๋ช |
|---|---|---|---|---|
| ์ํ ๋ชฉ๋ก ์กฐํ | POST | /api/products/search-product |
USER | ์ฌ๋ฌ ์ํ ์ ๋ณด๋ฅผ ํ ๋ฒ์ ์กฐํ |
| ๋จ์ผ ์ํ ์กฐํ | GET | /api/products/{productId} |
USER | ํน์ ์ํ์ ์์ธ ์ ๋ณด ์กฐํ |
| ์ํ ์์ฑ | POST | /api/products |
ADMIN | ์๋ก์ด ์ํ ๋ฑ๋ก |
| ์ํ ์์ | PUT | /api/products/{productId} |
ADMIN | ๊ธฐ์กด ์ํ ์ ๋ณด ์์ |
| ์ํ ์ญ์ | DELETE | /api/products/{productId} |
ADMIN | ์ํ ์ํํธ ์ญ์ |
| API | Method | Endpoint | ๊ถํ | ์ค๋ช |
|---|---|---|---|---|
| ์ฌ๊ณ ๋ชฉ๋ก ์กฐํ | POST | /api/stocks/search |
USER | ์ฌ๋ฌ ์ํ์ ์ฌ๊ณ ์กฐํ |
| ๋จ์ผ ์ฌ๊ณ ์กฐํ | GET | /api/stocks/product/{productId} |
USER | ํน์ ์ํ์ ์ฌ๊ณ ์กฐํ |
| ์ฌ๊ณ ์ฆ๊ฐ | POST | /api/stocks/product/{productId}/increase |
ADMIN | ์ํ ์ฌ๊ณ ์ฆ๊ฐ |
| ์ฌ๊ณ ์ด๊ธฐํ | POST | /api/stocks/product/{productId}/reset |
ADMIN | ์ํ ์ฌ๊ณ ์ด๊ธฐํ |
| API | Method | Endpoint | ๊ถํ | ์ค๋ช |
|---|---|---|---|---|
| ์ด๋ฒคํธ ์์ฑ | POST | /api/event/create |
ADMIN | ์๋ก์ด ํซ๋ ์ด๋ฒคํธ ์์ฑ |
| ์ด๋ฒคํธ ์กฐํ | POST | /api/event/search-event |
USER | ์ํ๋ณ ์ด๋ฒคํธ ์ ๋ณด ์กฐํ |
| API | Method | Endpoint | ๊ถํ | ์ค๋ช |
|---|---|---|---|---|
| ๋ค์ค ์ํ ์ฃผ๋ฌธ | POST | /api/orders/products |
USER | ๋ค์ค ์ํ ์ฃผ๋ฌธ (RestTemplate ๋ฐฉ์) |
| ์ฃผ๋ฌธ ์ทจ์ | PUT | /api/orders/{orderId} |
USER | ๊ธฐ์กด ์ฃผ๋ฌธ ์ทจ์ |
| ์ฃผ๋ฌธ ์กฐํ | GET | /api/orders/{orderId} |
USER | ์ฃผ๋ฌธ ์์ธ ์ ๋ณด ์กฐํ |
| API | Method | Endpoint | ๊ถํ | ์ค๋ช |
|---|---|---|---|---|
| ์ํ ๊ตฌ๋ | POST | /api/subscribe/sub-product |
USER | ์ํ ์๋ฆผ ๊ตฌ๋ ๋ฑ๋ก |
| ๊ตฌ๋ ์ ์กฐํ | GET | /api/subscribe/search-sub-user |
USER | ํน์ ์ํ ๊ตฌ๋ ์ ๋ชฉ๋ก ์กฐํ |
| ๊ตฌ๋ ์ทจ์ | DELETE | /api/subscribe/cancel-sub |
USER | ์ํ ๊ตฌ๋ ์ทจ์ |
API ์์ธ ์ ๋ณด
POST /api/auth/signup
Request Body
{
"email": "user@example.com",
"name": "ํ๊ธธ๋",
"password": "password123"
}POST /api/auth/login
Request Body
{
"email": "user@example.com",
"password": "password123"
}POST /api/auth/reissue
Request Body
{
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}POST /api/auth/logout
Authorization: Bearer {token}
Request Body
{
"password": "password123"
}POST /api/auth/withdraw
Authorization: Bearer {token}
Request Body
{
"password": "password123"
}POST /api/auth/{authId}/restore
Authorization: Bearer {token} (ADMIN ๊ถํ ํ์)
Query Parameter
authId: ๋ณต๊ตฌํ ์ฌ์ฉ์ ID
GET /api/users/me
Authorization: Bearer {token}
POST /api/products/search-product
Authorization: Bearer {token}
Request Body
{
"productIds": [1, 2, 3, 4, 5]
}GET /api/products/{productId}
Authorization: Bearer {token}
POST /api/products
Authorization: Bearer {token} (ADMIN ๊ถํ ํ์)
Request Body
{
"productName": "Galaxy S24 Ultra",
"productDescription": "์ผ์ฑ ์ต์ ํ๋๊ทธ์ญ ์ค๋งํธํฐ",
"productPrice": 1600000,
"productImageUrl": "https://example.com/galaxy-s24.jpg",
"productCategory": "ELECTRONICS"
}PUT /api/products/{productId}
Authorization: Bearer {token} (ADMIN ๊ถํ ํ์)
Request Body
{
"productName": "iPhone 15 Pro Max ์
๋ฐ์ดํธ",
"productDescription": "์
๋ฐ์ดํธ๋ ์ํ ์ค๋ช
",
"productPrice": 1700000,
"productImageUrl": "https://example.com/updated-iphone15.jpg",
"productCategory": "ELECTRONICS"
}DELETE /api/products/{productId}
Authorization: Bearer {token} (ADMIN ๊ถํ ํ์)
POST /api/stocks/search
Authorization: Bearer {token}
Request Body
{
"productIds": [1, 2, 3, 4, 5]
}GET /api/stocks/product/{productId}
Authorization: Bearer {token}
POST /api/stocks/product/{productId}/increase?quantity={์๋}
Authorization: Bearer {token} (ADMIN ๊ถํ ํ์)
POST /api/stocks/product/{productId}/reset?quantity={์๋}
Authorization: Bearer {token} (ADMIN ๊ถํ ํ์)
POST /api/event/create
Authorization: Bearer {token} (ADMIN ๊ถํ ํ์)
Request Body
{
"eventType": "HOT_DEAL",
"eventDiscount": 20,
"eventDuration": 7,
"startEventTime": "2025-07-15T00:00:00",
"productIds": [1, 2, 3, 4, 5]
}POST /api/event/search-event
Authorization: Bearer {token}
Request Body
{
"productIds": [1, 2, 3, 4, 5]
}POST /api/orders/v1
Authorization: Bearer {token}
Request Body
{
"productId": 1,
"quantity": 2
}POST /api/orders/v2
Authorization: Bearer {token}
Request Body
{
"orderItems": [
{
"productId": 1,
"quantity": 2
},
{
"productId": 2,
"quantity": 1
}
]
}PUT /api/orders/{orderId}
Authorization: Bearer {token}
GET /api/orders/{orderId}
Authorization: Bearer {token}
POST /api/subscribe/sub-product
Authorization: Bearer {token}
Request Body
{
"productIds": [1, 2, 3, 4, 5]
}GET /api/subscribe/search-sub-user?productId={์ํID}
Authorization: Bearer {token}
DELETE /api/subscribe/cancel-sub?userId={์ฌ์ฉ์ID}&productId={์ํID}
Authorization: Bearer {token}
์ถ๊ฐ ์ ๋ณด
| ์ฝ๋ | ํ๊ธ๋ช | ์ฝ๋ | ํ๊ธ๋ช |
|---|---|---|---|
ELECTRONICS |
์ ์์ ํ | HEALTH |
๊ฑด๊ฐ/์๋ฃ |
FASHION |
ํจ์ /์๋ฅ | BABY |
์ก์/์ถ์ฐ |
BEAUTY |
๋ทฐํฐ/ํ์ฅํ | PET |
๋ฐ๋ ค๋๋ฌผ |
HOME_LIVING |
ํ/๋ฆฌ๋น | CAR |
์๋์ฐจ/์ฉํ |
FOOD |
์ํ | HOBBY |
์ทจ๋ฏธ/์์ง |
SPORTS |
์คํฌ์ธ /๋ ์ | OFFICE |
์ฌ๋ฌด/๋ฌธ๊ตฌ |
BOOKS |
๋์ | OTHER |
๊ธฐํ |
์คํ ๋ฐฉ๋ฒ
- JDK 17 ์ด์
- MySQL 8.0
- Redis 6.0 ์ด์
- Gradle 7.x ์ด์
# MySQL๊ณผ Redis๋ฅผ ๋ก์ปฌ์ ์ง์ ์ค์นํ์ฌ ์ฌ์ฉ
# MySQL: 3306 ํฌํธ
# Redis: 6379 ํฌํธ# 1. ํ๋ก์ ํธ ํด๋ก
git clone https://github.com/your-repo/hotdeal.git
cd hotdeal
# 2. .env ํ์ผ ์์ฑ
cp .env.example .env
# .env ํ์ผ์ ์ด์ด ํ๊ฒฝ ๋ณ์ ๊ฐ ์ค์
# 3. Docker Compose๋ก ์ธํ๋ผ ์คํ
docker-compose up -d# 1. ํ๊ฒฝ ๋ณ์ ์ค์ (ํฐ๋ฏธ๋์์ ์คํํ๋ ๊ฒฝ์ฐ)
export DB_SCHEME=hotdeal
export DB_USERNAME=root
export DB_PASSWORD=your_password
export SECRET_KEY=your_secret_key_at_least_256_bits_long
# 2. Gradle ๋น๋
./gradlew clean build
# 3. ์ ํ๋ฆฌ์ผ์ด์
์คํ
./gradlew bootRun
# ๋๋ JAR ํ์ผ๋ก ์คํ
java -jar build/libs/hotdeal-0.0.1-SNAPSHOT.jar
# IDE(IntelliJ IDEA ๋ฑ)์์ ์คํํ๋ ๊ฒฝ์ฐ
# Run Configuration์์ ํ๊ฒฝ ๋ณ์ ์ค์ ํ ์คํ-
์ธํ๋ผ ํ๊ฒฝ ์ค๋น
- MySQL ์๋ฒ ์์ (3306 ํฌํธ)
- Redis ์๋ฒ ์์ (6379 ํฌํธ)
-
ํ๊ฒฝ ๋ณ์ ์ค์
.envํ์ผ ์์ฑ ๋ฐ ํ๊ฒฝ ๋ณ์ ์ค์ - ๋๋ IDE/ํฐ๋ฏธ๋์์ ํ๊ฒฝ ๋ณ์ export
-
์ ํ๋ฆฌ์ผ์ด์ ์คํ
./gradlew bootRun
-
์ด๊ธฐ ๋ฐ์ดํฐ ์ค์
- ์ํ ๋ฐ์ดํฐ ์ด๊ธฐํ (ProductDataInsertTest ํ์ฉ ๊ฐ๋ฅ)
- ์ฌ๊ณ ๋ฐ์ดํฐ ์ค์ (๊ฐ ์ํ๋ณ ์ฌ๊ณ API ํธ์ถ)
-
์๋น์ค ์ด์ฉ
- ์ด๋ฒคํธ ์์ฑ (๊ด๋ฆฌ์)
- WebSocket ์ฐ๊ฒฐ (์ค์๊ฐ ์๋ฆผ ์์ ์ฉ)
- ์ฃผ๋ฌธ ์ฒ๋ฆฌ
ํ๊ฒฝ ์ค์
.env ํ์ผ ์์ ๋ณด๊ธฐ
# Database
DB_SCHEME=hotdeal
DB_USERNAME=root
DB_PASSWORD=your_password
# JWT
SECRET_KEY=your_secret_key_at_least_256_bits_long
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=application.yml ์ ์ฒด ์ค์ ๋ณด๊ธฐ
spring:
application:
name: HotDeal
datasource:
url: jdbc:mysql://localhost:3306/${DB_SCHEME}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 500
minimum-idle: 50
connection-timeout: 60000
idle-timeout: 600000
max-lifetime: 1800000
leak-detection-threshold: 60000
jpa:
hibernate:
ddl-auto: create-drop # ์ด์ํ๊ฒฝ์์๋ validate ๋๋ none ์ฌ์ฉ
properties:
hibernate:
show_sql: true
format_sql: true
use_sql_comments: true
dialect: org.hibernate.dialect.MySQLDialect
open-in-view: false
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
jwt:
secret:
key: ${SECRET_KEY}
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
redisson:
address: redis://${REDIS_HOST:localhost}:${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
server:
port: 8080application-test.yml ์ ์ฒด ์ค์ ๋ณด๊ธฐ
spring:
datasource:
url: jdbc:mysql://localhost:3306/test_db
username: root
password: 3030
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 30
minimum-idle: 10
data:
redis:
host: localhost
port: 6379
jpa:
hibernate:
ddl-auto: create-drop
open-in-view: false
jwt:
secret:
key: dGVzdFNlY3JldEtleUZvclRlc3RpbmdQdXJwb3NlT25seTEyMzQ1Njc4OTAxMjM0NTY3ODkwDocker๋ฅผ ์ฌ์ฉํ์ฌ ์ธํ๋ผ๋ฅผ ๊ตฌ์ฑํ๋ ๊ฒฝ์ฐ
version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: ${DB_SCHEME}
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:6.2-alpine
ports:
- "6379:6379"
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
volumes:
mysql_data:
redis_data:- PUBLIC: ์ธ์ฆ ์์ด ์ ๊ทผ ๊ฐ๋ฅ
- USER: ์ผ๋ฐ ์ฌ์ฉ์ ๊ถํ ํ์
- ADMIN: ๊ด๋ฆฌ์ ๊ถํ ํ์
- ํ ์คํธ ์คํ ์ TestContainers๊ฐ ์๋์ผ๋ก Redis ์ปจํ ์ด๋๋ฅผ ์์ฑ
- ํตํฉ ํ ์คํธ์์๋ ์ค์ MySQL๊ณผ TestContainers Redis๋ฅผ ํจ๊ป ์ฌ์ฉ
- ํ
์คํธ ํ๋กํ์ผ(
@ActiveProfiles("test"))๋ก ๋ณ๋ ์ค์ ์ ์ฉ
- Redisson ๋ถ์ฐ ๋ฝ์ ์ฌ์ฉํ ์์ ํ ์ฌ๊ณ ์ฐจ๊ฐ
- ๋ฝ ํ๋ ์ต๋ ๋๊ธฐ์๊ฐ: 4์ด
- ๋ฝ ์ ์ง ์๊ฐ: 100ms
- ๋์ ์์ฒญ ์์๋ ์ ํํ ์ฌ๊ณ ๊ด๋ฆฌ ๋ณด์ฅ
- Redis ๊ธฐ๋ฐ ๋ถ์ฐ ํ๊ฒฝ ์ง์
- ์ด๋ฒคํธ ์์ฑ ์ WebSocket์ผ๋ก ์ค์๊ฐ ์๋ฆผ ๋ฐ์ก
- Spring Event๋ฅผ ํตํ ๋น๋๊ธฐ ์ฒ๋ฆฌ
- ์ค์ผ์ค๋ฌ๋ฅผ ํตํ ๋ง๋ฃ ์ด๋ฒคํธ ์๋ ์ญ์ (๋งค์ผ ์์ )
- ์ํ๋ณ ์ต์ ํ ์ธ๊ฐ ์๋ ๊ณ์ฐ (๋์ผ ์ํ ๋ค์ค ์ด๋ฒคํธ ์)
- ์ํ ์ ๋ณด ์กฐํ (ProductApiClient)
- ์ด๋ฒคํธ ํ ์ธ๊ฐ ์กฐํ (HotDealApiClient)
- ์ฃผ๋ฌธ ์์ฑ ๋ฐ ์ ์ฅ
- OrderCreatedEvent ๋ฐํ
- ๋น๋๊ธฐ๋ก ์ฌ๊ณ ์ฐจ๊ฐ ์ฒ๋ฆฌ (StockEventListener)
- ์ฌ๊ณ ๋ถ์กฑ ์ ์๋ ๋กค๋ฐฑ
- JWT ํ ํฐ ๊ธฐ๋ฐ ์ธ์ฆ
- Redis๋ฅผ ํ์ฉํ ํ ํฐ ๋ธ๋๋ฆฌ์คํธ ๊ด๋ฆฌ
- Spring Security ์ ์ฉ
- CustomException์ ํตํ ์ผ๊ด๋ ์๋ฌ ์ฒ๋ฆฌ
- ์ฌ๊ณ ๋ถ์กฑ, ์ํ ์์ ๋ฑ ๋น์ฆ๋์ค ์์ธ ์ฒ๋ฆฌ
- ํธ๋์ญ์ ๋กค๋ฐฑ ๋ฐ ๋ณด์ ์ฒ๋ฆฌ
- Spring Event๋ฅผ ํ์ฉํ ์คํจ ์ ๋ณด์ ํธ๋์ญ์
- ๋๋ฉ์ธ ๊ฐ ๋ฐ์ดํฐ ์ฐธ์กฐ ๋ฐฉ์ ์ค๊ณ
- ๋ด๋ถ API ํธ์ถ ์ ์ธ์ฆ ํ ํฐ ์ ๋ฌ ๋ฌธ์
- HTTP ํด๋ผ์ด์ธํธ ์ ํ
- Auth์ User ๊ฐ์ ๋ฐ์ดํฐ ์ผ๊ด์ฑ ๋ฌธ์
- ์น์์ผ ์๋ฆผ ์์คํ ์ต์ ํ
- ํซ๋ ์ด๋ฒคํธ ๋๋ฉ์ธ ์ฑ๋ฅ ์ต์ ํ
๋๋ฉ์ธ ๊ฐ์ ๊ฒฝ๊ณ๋ฅผ ๋ช
ํํ ํ๋ ๊ณผ์ ์์ ๋ฐ์ํ ํต์ฌ ๋๋ ๋ง
"์ฃผ๋ฌธ ์ ๋ณด๋ฅผ ์ด๋ป๊ฒ ๊ตฌ์ฑํ ๊ฒ์ธ๊ฐ?"
๊ณ ๋ คํ ๋ฐฉ์๋ค
| ๋ฐฉ์ | ์ค๋ช | ์ฅ์ | ๋จ์ |
|---|---|---|---|
| ๋ฐฉ์ 1 | ์ด๋ฒคํธ ์์ดํ ํ ์ด๋ธ์์ ๋ชจ๋ ๋ฐ์ดํฐ ์กฐํ | ๋จ์ํ ์กฐํ | ๋ฐ์ดํฐ ์ค๋ณต, ์ผ๊ด์ฑ ๋ฌธ์ |
| ๋ฐฉ์ 2 | ๋๋ฉ์ธ๋ณ ์ฑ ์ ๋ถ๋ฆฌ (์ํโ์ด๋ฒคํธ) | ์ผ๊ด์ฑ ๋ณด์ฅ, ๋๋ฉ์ธ ๋ ๋ฆฝ์ฑ | ๋ณต์กํ ๊ตฌํ |
๋ฐฉ์ 2: ๋๋ฉ์ธ๋ณ ์ฑ ์ ๋ถ๋ฆฌ
์ฃผ๋ฌธ ๋๋ฉ์ธ โ ์ํ ๋๋ฉ์ธ (๊ธฐ๋ณธ ์ ๋ณด)
โ ์ด๋ฒคํธ ๋๋ฉ์ธ (ํ ์ธ ์ ๋ณด)
- ๋ฐ์ดํฐ ์ผ๊ด์ฑ ๋ณด์ฅ: ์ํ ์ ๋ณด ๋ณ๊ฒฝ ์ ๋ฐ์ดํฐ ๋ถ์ผ์น ๋ฐฉ์ง
- ๋๋ฉ์ธ ๋ ๋ฆฝ์ฑ: ๊ฐ ๋๋ฉ์ธ์ ์ฑ ์ ๋ฒ์ ๋ช ํํ ๋ฐ ์์กด์ฑ ์ต์ํ
๊ด๋ฆฌ์ ์ฌ๊ณ ์ฆ๊ฐ ์์ฒญ โ ๋ด๋ถ ์ํ ๊ฒ์ฆ API ํธ์ถ โ 401 ์ธ์ฆ ์ค๋ฅ ๋ฐ์
์ธ๋ถ ์์ฒญ: ์ธ์ฆ๋จ โ
๋ด๋ถ API ํธ์ถ: ํ ํฐ ์ ๋ณด ์์ โ
๊ตฌํ ์ฝ๋
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.additionalInterceptors((request, body, execution) -> {
// ํ์ฌ ์์ฒญ์ Authorization ํค๋๋ฅผ ๋ณต์ฌ
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
if (attrs instanceof ServletRequestAttributes) {
HttpServletRequest httpRequest = ((ServletRequestAttributes) attrs).getRequest();
String authHeader = httpRequest.getHeader("Authorization");
if (authHeader != null) {
request.getHeaders().set("Authorization", authHeader);
log.debug("JWT ํ ํฐ ์ ๋ฌ - URL: {}", request.getURI());
}
}
return execution.execute(request, body);
})
.build();
}- ๋ด๋ถ API ํธ์ถ ์ ์๋ ํ ํฐ ์ ๋ฌ
- ์ธ์ฆ ๊ด๋ จ ์ค๋ฅ ์์ ํด๊ฒฐ
- ์ถ๊ฐ ์ค์ ์์ด ๋ชจ๋ RestTemplate ์์ฒญ์ ์ ์ฉ
| ๊ธฐ์ | ์ฅ์ | ๋จ์ |
|---|---|---|
| WebClient | ๋น๋๊ธฐ/๋ ผ๋ธ๋กํน, ๊ณ ์ฑ๋ฅ | ๋์ ํ์ต ๊ณก์ , ๋ณต์กํ ๋๋ฒ๊น |
| RestTemplate | ๊ฐ๋จํ ์ฌ์ฉ๋ฒ, ์ฌ์ด ๋๋ฒ๊น | ๋๊ธฐ ๋ฐฉ์, ์๋์ ์ ์ฑ๋ฅ |
DDD ๋๋ฉ์ธ ๊ฐ ํต์ ๊ตฌ์กฐ๋ฅผ ์ก์๊ฐ๋ ์ํฉ์์๋
๋น ๋ฅธ ๋ฌธ์ ํ์
๊ณผ ๋๋ฒ๊น
์ด ์ฑ๋ฅ๋ณด๋ค ์ฐ์ ์์๊ฐ ๋๋ค๊ณ ํ๋จ
ํ์๊ฐ์
ํ๋ก์ธ์ค:
1. Auth ํ
์ด๋ธ์ ๋ฐ์ดํฐ ์ ์ฅ โ
2. ์ด๋ฒคํธ ๋ฐํ โ
3. User ํ
์ด๋ธ์ ๋ฐ์ดํฐ ์ ์ฅ โ (์คํจ ๊ฐ๋ฅ)
๊ฒฐ๊ณผ: ๋ฐ์ดํฐ ์ผ๊ด์ฑ ๊นจ์ง
๊ตฌํ ๊ตฌ์กฐ
@EventListener
public void handlerUserRegisteredEvent(UserRegisteredEvent event) {
try {
User user = User.fromUserEvent(
event.getUserId(),
event.getEmail(),
event.getName(),
event.getCreatedAt()
);
userRepository.save(user);
} catch (Exception e) {
// User ์ ์ฅ ์คํจ ์ ๋ณด์ ํธ๋์ญ์
์ด๋ฒคํธ ๋ฐํ
eventPublisher.publishEvent(
UserCreationFailedEvent.of(event.getUserId())
);
}
}๋ณด์ ํธ๋์ญ์ ํ๋ฆ:
User ์ ์ฅ ์คํจ โ UserCreationFailedEvent ๋ฐํ โ Auth ๋ฐ์ดํฐ ๋กค๋ฐฑ
- ๋ฐ์ดํฐ ์ผ๊ด์ฑ ๋ณด์ฅ
- ๋ถ์ฐ ํธ๋์ญ์ ํ๊ฒฝ์์์ ์์ ์ฑ ํ๋ณด
- ์คํจ ์ํฉ์ ๋ํ ์๋ ๋ณต๊ตฌ ๋ฉ์ปค๋์ฆ
์๋ฒ โ ๊ตฌ๋
์ 1
โ ๊ตฌ๋
์ 2
โ ๊ตฌ๋
์ 3
โ ...
โ ๊ตฌ๋
์ N
| ๋ฌธ์ | ์ค๋ช | ์ํฅ |
|---|---|---|
| ์ฐ๊ฒฐ ํ๋ง ์ ํ | ๋์ ์ฐ๊ฒฐ ์ ํ๊ณ | ๋์ฉ๋ ์ฌ์ฉ์ ์ ์ฐ๊ฒฐ ๋๊น |
| ๋๊ธฐ ์ฒ๋ฆฌ ์ง์ฐ | ์์ฐจ์ ๋ฉ์์ง ์ ์ก | ๋ง์ง๋ง ์ฌ์ฉ์ ์๋ฆผ ์ง์ฐ |
- ์บ์ฑ ์ ์ฉ
- ๋ฐฐ์น ์ฒ๋ฆฌ
- ํ๊ณ: ์น์์ผ ์ฐ๊ฒฐ ํ๋ง ์ ํ์ ๊ทผ๋ณธ์ ํด๊ฒฐ ๋ถ๊ฐ
์๋ฒ: ๊ณตํต ์๋ฆผ ๋ธ๋ก๋์บ์คํธ
ํด๋ผ์ด์ธํธ: ๊ตฌ๋
์ํ ํํฐ๋ง ํ ์ฒ๋ฆฌ
๊ตฌํ ์์
์๋ฒ ์ธก (๋ธ๋ก๋์บ์คํธ)
@Service
public class NotificationService {
public void notifyProductEvent(WSEventProduct event) {
// ๋ชจ๋ ์ฐ๊ฒฐ๋ ํด๋ผ์ด์ธํธ์๊ฒ ๋ธ๋ก๋์บ์คํธ
messagingTemplate.convertAndSend(
"/topic/notification",
event.toNotificationMessage()
);
}
}ํด๋ผ์ด์ธํธ ์ธก (ํํฐ๋ง)
stompClient.subscribe('/topic/notification', function(message) {
const eventData = JSON.parse(message.body);
// ์ฌ์ฉ์๊ฐ ๊ตฌ๋
ํ ์ํ์ธ์ง ํ์ธ
if (userSubscribedProducts.includes(eventData.productId)) {
displayNotification(eventData);
}
});| ๊ฐ์ ํญ๋ชฉ | Before | After |
|---|---|---|
| ํ์ฅ์ฑ | ์ฌ์ฉ์ ์์ ๋น๋กํ ์ฑ๋ฅ ์ ํ | ์ฌ์ฉ์ ์ ๋ฌด๊ดํ ์ผ์ ์ฑ๋ฅ |
| ์ฒ๋ฆฌ ๋ฐฉ์ | ๋๊ธฐ ์์ฐจ ์ฒ๋ฆฌ | ๋จ์ผ ๋ธ๋ก๋์บ์คํธ |
| ์ฐ๊ฒฐ ๊ด๋ฆฌ | ๊ฐ๋ณ ์ฐ๊ฒฐ ๊ด๋ฆฌ ํ์ | ๋จ์ํ ์ฐ๊ฒฐ ๊ด๋ฆฌ |
| ์๋ฆผ ์ง์ฐ | ๋ง์ง๋ง ์ฌ์ฉ์ ์ง์ฐ ๋ฐ์ | ๋ชจ๋ ์ฌ์ฉ์ ๋์ ์์ |
| ๋ฌธ์ | ์ค๋ช | ์ํฅ |
|---|---|---|
| ์ฐ๊ฒฐ ํ๋ง ์ ํ | ๋์ ์ฐ๊ฒฐ ์ ํ๊ณ | ๋์ฉ๋ ์ฌ์ฉ์ ์ ์ฐ๊ฒฐ ๋๊น |
| ๋๊ธฐ ์ฒ๋ฆฌ ์ง์ฐ | ์์ฐจ์ ๋ฉ์์ง ์ ์ก | ๋ง์ง๋ง ์ฌ์ฉ์ ์๋ฆผ ์ง์ฐ |
- ์บ์ฑ ์ ์ฉ
- ๋ฐฐ์น ์ฒ๋ฆฌ
- ํ๊ณ: ์น์์ผ ์ฐ๊ฒฐ ํ๋ง ์ ํ์ ๊ทผ๋ณธ์ ํด๊ฒฐ ๋ถ๊ฐ
์๋ฒ: ๊ณตํต ์๋ฆผ ๋ธ๋ก๋์บ์คํธ
ํด๋ผ์ด์ธํธ: ๊ตฌ๋
์ํ ํํฐ๋ง ํ ์ฒ๋ฆฌ
๊ตฌํ ์์
์๋ฒ ์ธก (๋ธ๋ก๋์บ์คํธ)
@Service
public class NotificationService {
public void notifyProductEvent(WSEventProduct event) {
// ๋ชจ๋ ์ฐ๊ฒฐ๋ ํด๋ผ์ด์ธํธ์๊ฒ ๋ธ๋ก๋์บ์คํธ
messagingTemplate.convertAndSend(
"/topic/notification",
event.toNotificationMessage()
);
}
}ํด๋ผ์ด์ธํธ ์ธก (ํํฐ๋ง)
stompClient.subscribe('/topic/notification', function(message) {
const eventData = JSON.parse(message.body);
// ์ฌ์ฉ์๊ฐ ๊ตฌ๋
ํ ์ํ์ธ์ง ํ์ธ
if (userSubscribedProducts.includes(eventData.productId)) {
displayNotification(eventData);
}
});| ๊ฐ์ ํญ๋ชฉ | Before | After |
|---|---|---|
| ํ์ฅ์ฑ | ์ฌ์ฉ์ ์์ ๋น๋กํ ์ฑ๋ฅ ์ ํ | ์ฌ์ฉ์ ์ ๋ฌด๊ดํ ์ผ์ ์ฑ๋ฅ |
| ์ฒ๋ฆฌ ๋ฐฉ์ | ๋๊ธฐ ์์ฐจ ์ฒ๋ฆฌ | ๋จ์ผ ๋ธ๋ก๋์บ์คํธ |
| ์ฐ๊ฒฐ ๊ด๋ฆฌ | ๊ฐ๋ณ ์ฐ๊ฒฐ ๊ด๋ฆฌ ํ์ | ๋จ์ํ ์ฐ๊ฒฐ ๊ด๋ฆฌ |
| ์๋ฆผ ์ง์ฐ | ๋ง์ง๋ง ์ฌ์ฉ์ ์ง์ฐ ๋ฐ์ | ๋ชจ๋ ์ฌ์ฉ์ ๋์ ์์ |
1๋จ๊ณ - Event ์ ์ฅ ์๋ฃ: 22ms
2๋จ๊ณ - ProductApiClient ํธ์ถ ์๋ฃ: 236ms (์กฐํ๋ ์ํ ์: 10000)
3๋จ๊ณ - EventItem ๊ฐ์ฒด ์์ฑ ์๋ฃ: 4ms
4๋จ๊ณ - EventItem insert ์๋ฃ: 1052ms
5๋จ๊ณ - Event์ EventItem ๋ฆฌ์คํธ ์ค์ ์๋ฃ: 0ms
6๋จ๊ณ - WSEventProduct ๊ฐ์ฒด ์์ฑ ์๋ฃ: 1ms
7๋จ๊ณ - ์ด๋ฒคํธ ๋ฐํ ์๋ฃ: 799ms
=== createEvent ์ด ์คํ์๊ฐ: 2115ms ===
8๋จ๊ณ - ์ปจํธ๋กค๋ฌ ์คํ ์๋ฃ: 4126ms
๋ฌธ์ ์ :
- ์ปจํธ๋กค๋ฌ ์ ์ฒด ์คํ ์๊ฐ์ด 4126ms๋ก ์ฌ๊ฐํ ์ง์ฐ
- EventItem ๋ฒํฌ insert๊ฐ 1052ms๋ก ํฐ ๋ณ๋ชฉ
- ์ด๋ฒคํธ ๋ฆฌ์ค๋๊ฐ ๋๊ธฐ์ ์ผ๋ก ์ฒ๋ฆฌ๋์ด ์ง์ฐ ๋ฐ์
๊ตฌํ ์์
// ๊ธฐ์กด ๋ฐฉ์ - ๊ฐ๋ณ insert
notificationRepository.save(notification);
@Service
public class NotificationService {
private final List<Notification> buffer = new ArrayList<>();
public void addNotification(Notification notification) {
synchronized (lock) {
buffer.add(notification);
if(buffer.size() >= 1000) {
// 1000๊ฐ์ฉ ๋ฒํฌ insert
insertBatch();
}
}
}
@Async
public void insertBatch() {
List<Notification> notifications;
synchronized (lock) {
notifications = new ArrayList<>(buffer);
buffer.clear();
}
notificationRepository.insertNotifications(notifications);
}
}
๊ฐ์ ํจ๊ณผ: ๊ฐ๋ณ insert โ ๋ฒํฌ insert๋ก ๋ณ๊ฒฝํ์ฌ DB ํธ์ถ ํ์ ๋ํญ ๊ฐ์
๊ตฌํ ์์
// ๊ธฐ์กด ๋ฐฉ์
eventItemRepository.saveAll(eventItems);
@Repository
public class EventItemInsertRepository {
public void insertEventItem(List<EventItem> eventItems, Long eventId) {
String sql = "INSERT INTO event_items (event_id, product_id, product_name, original_price, discount_price, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)";
jdbcTemplate.batchUpdate(sql, eventItems, 1000, (ps, eventItem) -> {
ps.setLong(1, eventId);
ps.setLong(2, eventItem.getProductId());
ps.setString(3, eventItem.getProductName());
ps.setBigDecimal(4, eventItem.getOriginalPrice());
ps.setBigDecimal(5, eventItem.getDiscountPrice());
ps.setObject(6, LocalDateTime.now());
ps.setObject(7, LocalDateTime.now());
});
}
}
๊ฐ์ ํจ๊ณผ:
- JPA saveAll โ JdbcTemplate batchUpdate๋ก ๋ณ๊ฒฝ
- ๋ฐฐ์น ํฌ๊ธฐ 1000์ผ๋ก ์ค์ ํ์ฌ ๋ฉ๋ชจ๋ฆฌ ํจ์จ์ฑ ํฅ์
๊ตฌํ ์์
@Transactional
public EventResponse createEvent(EventCrateRequest request) {
// ... ๋ฐ์ดํฐ ์ ์ฅ ...
// ์ด๋ฒคํธ ๋ฐํ (๋๊ธฐ์ )
wsEventProducts.forEach(wsEvent -> {
eventPublisher.publishEvent(wsEvent);
});
return new EventResponse(event);
}
๋ฌธ์ ์ : ์ด๋ฒคํธ ๋ฐํ์ด ๋๊ธฐ์ ์ผ๋ก ์ฒ๋ฆฌ๋์ด ์ง์ฐ ๋ฐ์
@Component
public class NotificationListener {
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void addProductDiscountEvent(WSEventProduct event) {
try {
log.info("addProductDiscountEvent ์์ - ๋จ์ผ ์ด๋ฒคํธ: {}", event.product_id());
ListenProductEvent listenProductEvent = new ListenProductEvent(event);
notificationService.notifyProductEventMessage(listenProductEvent);
log.info("addProductDiscountEvent ์ข
๋ฃ");
} catch (Exception e) {
log.error("addProductDiscountEvent ์ฒ๋ฆฌ ์คํจ message : {}", e.getMessage());
}
}
}
๊ฐ์ ํจ๊ณผ:
- ํธ๋์ญ์ ์ปค๋ฐ ํ ์ด๋ฒคํธ ๋ฆฌ์ค๋ ์คํ์ผ๋ก ๋ฐ์ดํฐ ์ผ๊ด์ฑ ๋ณด์ฅ
- ๋น๋๊ธฐ ์ฒ๋ฆฌ๋ก ์ปจํธ๋กค๋ฌ ์๋ต ์๊ฐ ๋จ์ถ
- ํธ๋์ญ์ ๋กค๋ฐฑ ์ ์ด๋ฒคํธ ๋ฆฌ์ค๋ ๋ฏธ์คํ์ผ๋ก ์์ ์ฑ ํ๋ณด
๊ตฌํ ์์
@Entity
public class Event {
@OneToMany(mappedBy = "event", cascade = CascadeType.ALL)
private List<EventItem> products;
}
@Entity
public class EventItem {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "event_id")
private Event event;
}
๋ฌธ์ ์ :
- ์กฐ์ธ ํ ์ด๋ธ(events_products)์ ๋๋ insert ์ฟผ๋ฆฌ ๋ฐ์
- ํธ๋์ญ์ ์ข ๋ฃ ์ ์ง์ฐ ๋ฐ์
- ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ์ฆ๊ฐ
@Entity
public class Event {
@Transient // ์กฐํ์ฉ์ผ๋ก๋ง ์ฌ์ฉ
private List<EventItem> products;
}
@Entity
public class EventItem {
private Long eventId; // ๋จ์ ์ธ๋ํค๋ง ์ ์ฅ
}
๊ฐ์ ํจ๊ณผ:
- ์กฐ์ธ ํ ์ด๋ธ insert ์ฟผ๋ฆฌ ์ ๊ฑฐ
- ํธ๋์ญ์ ์ข ๋ฃ ์ ์ค๋ฒํค๋ ๋ํญ ๊ฐ์
- ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ ์ต์ ํ
1๋จ๊ณ - Event ์ ์ฅ ์๋ฃ: 29ms
2๋จ๊ณ - ProductApiClient ํธ์ถ ์๋ฃ: 260ms (์กฐํ๋ ์ํ ์: 10000)
3๋จ๊ณ - EventItem ๊ฐ์ฒด ์์ฑ ์๋ฃ: 3ms
4๋จ๊ณ - EventItem ๋ฒํฌ insert ์๋ฃ: 520ms
5๋จ๊ณ - Event์ EventItem ๋ฆฌ์คํธ ์ค์ ์๋ฃ: 0ms
6๋จ๊ณ - WSEventProduct ๊ฐ์ฒด ์์ฑ ์๋ฃ: 1ms
7๋จ๊ณ - ์ด๋ฒคํธ ๋ฐํ ์๋ฃ: 10ms
=== createEvent ์ด ์คํ์๊ฐ: 823ms ===
8๋จ๊ณ - ์ปจํธ๋กค๋ฌ ์คํ: 844ms
์ฐจ์คํธ
- https://juno0112.tistory.com/116 : ๋๋ฉ์ธ์ฃผ๋ ์ค๊ณ ๋ฐ ๋ฐ์ดํฐ ํ๋ฆ ์๊ฐํํ๊ธฐ
- https://juno0112.tistory.com/117 : ๋๋ฉ์ธ ๊ฐ ์์กด์ฑ ๋ฎ์ถ๊ธฐ : API ํธ์ถ ๋ฐ ์คํ๋ง ์ด๋ฒคํธ ๊ตฌ๋ ์ ์ํ Common ํจํค์ง ์ค๊ณ
- https://juno0112.tistory.com/118 : Spring Boot์์ RestTemplate ๋ด๋ถ API ํธ์ถ ์ JWT ํ ํฐ ์ ๋ฌํ๊ธฐ
๊น์ ์
- https://velog.io/@eggtart21/๋์์ฑ-์ ์ด-๊ตฌํ-i193xytg
- https://velog.io/@eggtart21/๋์์ฑ-์ ์ด-๊ตฌํ
- https://velog.io/@eggtart21/๋ฝ-์๊ฐ-์ ํ-์ด์ -๊ณต์
์ด์ค์
- https://t-era.tistory.com/292 : ํ์ฌ ์น์์ผ ๊ธฐ๋ฅ์ ๋ฌธ์ ์
- https://t-era.tistory.com/293 : ๋์ฉ๋ ๋ฐ์ดํฐ ์ฝ์ ์์ ์ฑ๋ฅ ๊ฐ์
์ต์์ฌ
- https://velog.io/@teopteop/TIL-Spring-%EB%B3%B4%EC%83%81-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98 : ์ด๋ฒคํธ ๊ธฐ๋ฐ ์ํคํ ์ฒ์์์ ๋ฌด๊ฒฐ์ฑ ๋ณด์ - ๋ณด์ ํธ๋์ญ์