๋ณธ ํ๋ก์ ํธ๋ ์ค์ E-commerce ํ๊ฒฝ์์ ๋ฐ์ํ ์ ์๋ ๋๊ท๋ชจ ๋์ ์ ์ ์ํฉ์ ๊ฐ์ ํ์ฌ, ์ ์ฐฉ์ ์ฟ ํฐ ๋ฐ๊ธ ๋ฐ ํฌ์ธํธ ์์คํ ์ ์์ ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์๋ ๋ฐฑ์๋ ์์คํ ์ ์ค๊ณํ๊ณ ๊ตฌ์ถํ๋ ๊ฒ์ ๋ชฉํ๋ก ํฉ๋๋ค.
๋จ์ํ ๊ธฐ๋ฅ ๊ตฌํ์ ๋์ด, ์ฑ๋ฅ ํ ์คํธ ๋๊ตฌ(k6, nGrinder)์ ๋ชจ๋ํฐ๋ง ์์คํ (Prometheus, Grafana)์ ์ ๊ทน์ ์ผ๋ก ํ์ฉํ์ฌ ์์คํ ์ ๋ณ๋ชฉ ์ง์ ์ ๋ถ์ํ๊ณ , ๋น๋๊ธฐ ์ฒ๋ฆฌ, ๋ถ์ฐ ์บ์ฑ, ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ด์คํ ๋ฑ ๋ค์ํ ๊ธฐ์ ์ ์ ์ฉํ์ฌ ์์คํ ์ ์ ์ง์ ์ผ๋ก ๊ฐ์ ํด ๋๊ฐ๋ ๊ณผ์ ์ ์ง์คํ์ต๋๋ค.
-
์ ์ฐฉ์ ์ฟ ํฐ ๋ฐ๊ธ: ๋๊ท๋ชจ ๋์ ์์ฒญ์๋ ๋ฐ์ดํฐ ์ ํฉ์ฑ์ ๋ณด์ฅํ๋ ๋น๋๊ธฐ ์ฟ ํฐ ๋ฐ๊ธ API
-
์ฟ ํฐ ์ฌ์ฉ ๋ฐ ์กฐํ: ๋น๊ด์ ๋ฝ(Pessimistic Lock)์ ์ด์ฉํ ์์ ํ ์ฟ ํฐ ์ฌ์ฉ ์ฒ๋ฆฌ ๋ฐ ์กฐํ API
-
ํฌ์ธํธ ์์คํ : ์ฟ ํฐ๊ณผ ์ฐ๊ณํ์ฌ ์ฌ์ฉํ ์ ์๋ ํฌ์ธํธ ์ ๋ฆฝ/์ฌ์ฉ/์๋ฉธ ๊ธฐ๋ฅ์ ๊ธฐ๋ฐ ์ค๊ณ
-
ํตํฉ ๋ชจ๋ํฐ๋ง ๋ฐ ์๋ฆผ: Prometheus, Grafana, Alertmanager๋ฅผ ์ฐ๋ํ์ฌ ์์คํ ์ ์ฑ๋ฅ์ ์ค์๊ฐ์ผ๋ก ๋ชจ๋ํฐ๋งํ๊ณ , ์ด์ ์งํ ๋ฐ์ ์ Discord๋ก ์๋ ์๋ฆผ ์์
์ด๊ธฐ์๋ ํฌ์ธํธ ์ฌ์ฉ์ด๋ ์ ๋ฆฝ์ด ๋์์ ๋ฐ์ํ ์ผ์ ๊ฑฐ์ ์์ ๊ฒ์ด๋ผ๊ณ ๊ฐ์ ํ์์ต๋๋ค. ์ฆ, ๋๋ถ๋ถ์ ํธ๋์ญ์ ์ถฉ๋์ด ์์ ๊ฒ์ด๋ผ ๊ฐ์ ํ์ฌ ๋๊ด์ ๋ฝ์ ์ ํํ์ต๋๋ค.
ํ์ง๋ง ํ ์คํธ ๊ฒฐ๊ณผ ๋ฐ๋๋ฝ์ด ๋ฐ์ํ์๋ค. ๋ก๊ทธ๋ฅผ ํตํด ํ์ธํด๋ณด๋ S-LOCK์ด ๊ฑธ๋ ค ์๋๋ฐ X-LOCK์ ํ๋ํ๋ ค๋ ๊ณผ์ ์์ ๋ฐ์ํ ๊ฒ์ด์๋ค.
- S-LOCK: ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ๋ ์ฌ์ฉํ๋ ๋ฝ
- X-LOCK: ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํ ๋ ์ฌ์ฉํ๋ ๋ฝ
public PointResponse use(Long partnerId, String userId, int amount, String description) {
PointWallet wallet = pointWalletRepository.findByPartnerIdAndUserId(partnerId, userId)
.orElseGet(() -> {
PointWallet newWallet = PointWallet.create(partnerId, userId);
return pointWalletRepository.save(newWallet);
});
wallet.use(amount);
PointHistory history = PointHistory.builder()
.pointWallet(wallet)
.transactionType(TransactionType.USE)
.amount(amount)
.description(description)
.build();
pointHistoryRepository.save(history);
return PointResponse.from(wallet);
}์ด ์ฝ๋์ dead lock history ๋ก๊ทธ๋ฅผ ํตํด deadlock ๋ฐ์ ์๋๋ฆฌ์ค๋ฅผ ์์ฑํ ์ ์์์ต๋๋ค.
์ด ๋, ์ค๋ช ์ ํธ์๋ฅผ ์ํด ๋ ํธ๋์ญ์ ์ด ๊ฒฝ์ํ๋ค๊ณ ๊ฐ์ ํ๊ฒ ์ต๋๋ค.
- ๋์ ์ง์
๋ฐ s-lockํ๋:
- ๋ ํธ๋์ญ์ ๋ชจ๋ย pointWalletRepository.findByPartnerIdAndUserId(...)๋ฅผ ์คํํฉ๋๋ค. ์ดย SELECTย ์ฟผ๋ฆฌ๋ย point_walletsย ํ ์ด๋ธ์ row๋ฅผ ์ฝ๊ธฐ ์ํด ์ ๊ทผํฉ๋๋ค.
- ๋ก๊ทธ๋ฅผ ๋ณด๋ฉด, ๋ ํธ๋์ญ์ ๋ชจ๋ย lock mode Sย ๋ฅผย HOLDS THE LOCK(S)ย ํ๊ณ ์์ต๋๋ค. ์ฆ, ๋ ํธ๋์ญ์ ๋ชจ๋ย id=26์ธ row์ ๋ํดย ์ฝ๊ธฐ(๊ณต์ ) ๋ฝ์ ๋์์ ํ๋ํ๋ ๋ฐ ์ฑ๊ณตํ์ต๋๋ค.
- ๋๊ด์ ๋ฝ์ ์ํ ์
๋ฐ์ดํธ(UPDATE) ์๋:
- ๋ ์ค๋ ๋ ๋ชจ๋ ๋ฉ๋ชจ๋ฆฌ ์์์ย wallet.use(amount)๋ฅผ ์คํํฉ๋๋ค.
- ์ด์ ํธ๋์ญ์ ์ด ๋๋๋ ์์ ์, JPA๋ ๋ณ๊ฒฝ๋ย PointWalletย ์ํฐํฐ๋ฅผ DB์ ๋ฐ์ํ๊ธฐ ์ํดย UPDATEย ์ฟผ๋ฆฌ๋ฅผ ์คํํ๋ ค๊ณ ํฉ๋๋ค. ์ดย UPDATEย ์ฟผ๋ฆฌ์๋ย @Versionย ๋๋ฌธ์ย WHERE ... AND version=0ย ์กฐ๊ฑด์ด ํฌํจ๋์ด ์์ต๋๋ค.
- UPDATE๋ ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํด์ผ ํ๋ฏ๋ก, ๊ธฐ์กด์ ๊ณต์ ๋ฝ(S-Lock)์ย ๋ฐฐํ์ ๋ฝ(X-Lock)์ผ๋ก ์ ๊ทธ๋ ์ด๋ํ๋ ค๊ณ ์๋ํฉ๋๋ค.
- deadlock ๋ฐ์
- ์ค๋ ๋ A
- WAITING FOR THIS LOCK:ย id=26์ธ row์ ๋ํดย ๋ฐฐํ์ ๋ฝ(X-Lock)์ ํ๋ํ๋ ค๊ณ ๋๊ธฐํฉ๋๋ค.
- ์ ๋๊ธฐํ๋?ย ์ค๋ ๋ B๊ฐ ๋์ผํ row์ ๋ํดย ๊ณต์ ๋ฝ(S-Lock)์ ์ฅ๊ณ ๋์์ฃผ์ง ์๊ณ ์๊ธฐ ๋๋ฌธ์ ๋๋ค. (X-Lock์ ๋ค๋ฅธ ์ด๋ค ๋ฝ๊ณผ๋ ๊ณต์กดํ ์ ์์ต๋๋ค.)
- ์ค๋ ๋ B
- WAITING FOR THIS LOCK:ย id=26์ธ row์ ๋ํดย ๋ฐฐํ์ ๋ฝ(X-Lock)์ ํ๋ํ๋ ค๊ณ ๋๊ธฐํฉ๋๋ค.
- ์ ๋๊ธฐํ๋?ย ์ค๋ ๋ A๊ฐ ๋์ผํ row์ ๋ํดย ๊ณต์ ๋ฝ(S-Lock)์ ์ฅ๊ณ ๋์์ฃผ์ง ์๊ณ ์๊ธฐ ๋๋ฌธ์ ๋๋ค.
- ๊ฒฐ๊ณผ:ย ์ค๋ ๋ A๋ ์ค๋ ๋ B๊ฐ ๋๋๊ธฐ๋ฅผ ๊ธฐ๋ค๋ฆฌ๊ณ , ์ค๋ ๋ B๋ ์ค๋ ๋ A๊ฐ ๋๋๊ธฐ๋ฅผ ๊ธฐ๋ค๋ฆฌ๋,ย ์ ํ์ ์ธ ์ํ ๋๊ธฐ ์ํ, ์ฆ ๋ฐ๋๋ฝ์ด ์์ฑ๋์์ต๋๋ค.
- ์ค๋ ๋ A
- MySQL์ ํด๊ฒฐ
- InnoDB ์์ง์ ์ด ์ํ ๋๊ธฐ๋ฅผ ๊ฐ์งํ๊ณ , ๋ ์ค ํ๋์ ํธ๋์ญ์ (๋ก๊ทธ์์๋ย TRANSACTION (2))์ ํฌ์์์ผ๋ก ์ผ์ ๊ฐ์ ๋ก ๋กค๋ฐฑ์์ผ ๋ฐ๋๋ฝ์ ํ์ด๋ฒ๋ฆฝ๋๋ค.
์์ ๋ฐ์ํ ๋ฐ๋๋ฝ์ผ๋ก ์ธํด ๋๊ด์ ๋ฝ์ผ๋ก๋ ํด๋น ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ํ๋ค๋ค๊ณ ์๊ฐํ์ฌ ๋น๊ด์ ๋ฝ์ ๋์ ํ์ต๋๋ค.
๊ธฐ์กด์ ์๋๋ฆฌ์ค๋ ๋น๊ด์ ๋ฝ์ ๋์ ํ์ฌ ํด๊ฒฐํ ์ ์์์ต๋๋ค.
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select p from PointWallet p where p.partnerId = :partnerId and p.userId = :userId")
Optional<PointWallet> findByPartnerIdAndUserIdWithLock(
@Param("partnerId") Long partnerId,
@Param("userId") String userId
);- ์ค๋ ๋ A๊ฐย SELECT ... FOR UPDATE๋กย point_walletsย row์ X-Lock์ ํ๋ํฉ๋๋ค.
- ์ค๋ ๋ B๋ย SELECT ... FOR UPDATE๋ฅผ ์คํํ์ง๋ง, ์ค๋ ๋ A๊ฐ X-Lock์ ์ฅ๊ณ ์์ผ๋ฏ๋ก **SELECTย ๋จ๊ณ์์๋ถํฐ ๋๊ธฐ(Wait)**ํฉ๋๋ค.
- ์ ์ด์ ๋ ์ค๋ ๋๊ฐ ๋์์ ๋ฝ์ ์ก๋ ์ํฉ ์์ฒด๊ฐ ๋ฐ์ํ์ง ์์ผ๋ฏ๋ก, '๋ฝ ์ ๊ทธ๋ ์ด๋ ๊ฒฝ์'์ผ๋ก ์ธํ ๋ฐ๋๋ฝ์ย ์์ฒ์ ์ผ๋ก ์ฐจ๋จ๋ฉ๋๋ค.
ํ์ง๋ง ๋ค๋ฅธ ์๋๋ฆฌ์ค์ ๊ฒฝ์ฐ์๋ ๋น๊ด์ ๋ฝ์ ์ฌ์ฉํ๋๋ผ๋ ๋ฐ๋๋ฝ์ด ๋ฐ์ํ ์ ์์ต๋๋ค.
์ฌ์ฉ์ A๊ฐ ์ฌ์ฉ์ B์๊ฒ ํฌ์ธํธ๋ฅผ ์ด์ ํ๋ ๊ธฐ๋ฅ์ ๋ง๋ ๋ค๊ณ ๊ฐ์
- ํธ๋์ญ์
1 (์ค๋ ๋ 1):ย ์ฌ์ฉ์ A -> ์ฌ์ฉ์ B์๊ฒ 100์ ์ด์
- ์ฌ์ฉ์ A์ย point_walletsย row์ย SELECT ... FOR UPDATE๋กย ๋ฝ์ ๊ฑด๋ค.ย (์ฑ๊ณต)
- (์ด๋ค ๋ก์ง ์ฒ๋ฆฌ ํ...)
- ์ฌ์ฉ์ B์ย point_walletsย row์ย SELECT ... FOR UPDATE๋กย ๋ฝ์ ๊ฑธ๋ ค๊ณ ํ๋ค.
- ํธ๋์ญ์
2 (์ค๋ ๋ 2):ย ๊ฑฐ์ ๋์์, ์ฌ์ฉ์ B -> ์ฌ์ฉ์ A์๊ฒ 50์ ์ด์
- ์ฌ์ฉ์ B์ย point_walletsย row์ย SELECT ... FOR UPDATE๋กย ๋ฝ์ ๊ฑด๋ค.ย (์ฑ๊ณต)
- (์ด๋ค ๋ก์ง ์ฒ๋ฆฌ ํ...)
- ์ฌ์ฉ์ A์ย point_walletsย row์ย SELECT ... FOR UPDATE๋กย ๋ฝ์ ๊ฑธ๋ ค๊ณ ํ๋ค.
| ์๊ฐ | ํธ๋์ญ์ 1 (A -> B) | ํธ๋์ญ์ 2 (B -> A) |
|---|---|---|
| T1 | A์ row์ ๋ฝ ํ๋ | |
| T2 | B์ row์ ๋ฝ ํ๋ | |
| T3 | B์ row์ ๋ฝ์ ๊ฑธ๋ ค๊ณ ์๋ ->ย ๋๊ธฐ!ย (ํธ๋์ญ์ 2๊ฐ ๋ฝ์ ์ฅ๊ณ ์์) | |
| T4 | A์ row์ ๋ฝ์ ๊ฑธ๋ ค๊ณ ์๋ ->ย ๋๊ธฐ!ย (ํธ๋์ญ์ 1์ด ๋ฝ์ ์ฅ๊ณ ์์) |
์ด๋ฌํ ์ข ๋ฅ์ ๋ฐ๋๋ฝ์ ๋ฐฉ์งํ๊ธฐ ์ํ ๊ฐ์ฅ ์ผ๋ฐ์ ์ธ ๊ท์น์ "๋ฝ ํ๋ ์์๋ฅผ ํญ์ ๋์ผํ๊ฒ ์ ์งํ๋ ๊ฒ"์ ๋๋ค.
๋คํํ๋, ์ฐ๋ฆฌ๊ฐ ํ์ฌ ๊ตฌํํ๋ย pointService.use()ย ๋ฉ์๋๋ย ์ค์ง ํ๋์ย point_walletsย row์๋งย ๋ฝ์ ๊ฒ๋๋ค. ์ฌ๋ฌ ์ฌ์ฉ์์ ์ง๊ฐ์ ๋์์ ์ ๊ทธ๋ ๋ก์ง์ด ์์ต๋๋ค.
๋ฐ๋ผ์,ย ํ์ฌ์ย useย ๋ฉ์๋์ ํํด์๋ ๋น๊ด์ ๋ฝ์ ์ ์ฉํ๋ฉด ๋ฐ๋๋ฝ์ด ๋ฐ์ํ ๊ฐ๋ฅ์ฑ์ด ๊ฑฐ์ ์๋ค๊ณ ๋ณผ ์ ์์ต๋๋ค.
- ๋น๊ด์ ๋ฝ์ ๋ง๋ณํต์น์ฝ์ด ์๋๋ค.ย ๋ฝ ํ๋ ์์๊ฐ ๊ผฌ์ด๋ฉด ์ฌ์ ํ ๋ฐ๋๋ฝ์ด ๋ฐ์ํ ์ ์๋ค.
- ํ์ง๋ง ๋น๊ด์ ๋ฝ์ ์ฐ๋ฆฌ๊ฐ ๊ฒช์๋ย '๋ฝ ์ ๊ทธ๋ ์ด๋ ๊ฒฝ์' ๋ฐ๋๋ฝ์ ํ์คํ๊ฒ ๋ง์์ค๋ค.
- ์ฐ๋ฆฌ์ย ํ์ฌ ์๋๋ฆฌ์ค(๋จ์ผ ์ง๊ฐ ์ฐจ๊ฐ)๋ย ์ฌ๋ฌ ์์์ ๋ฝ ์์๊ฐ ๊ผฌ์ผ ์ผ์ด ์์ผ๋ฏ๋ก,ย ๋น๊ด์ ๋ฝ์ ์ฌ์ฉํ๋ ๊ฒ์ด ์์ ํ๊ณ ํจ๊ณผ์ ์ธ ํด๊ฒฐ์ฑ ์ด๋ค.
2๏ธโฃ [์ฑ๋ฅ ๊ฐ์ ] ์ ์ฐฉ์ ์ฟ ํฐ ๋ฐ๊ธ ์์คํ : ๋น๊ด์ ๋ฝ์ ํ๊ณ๋ฅผ ๋์ด ๋น๋๊ธฐ ์ํคํ ์ฒ๋ก
๊ฐ์ฅ ๋จผ์ ๊ณ ๋ คํ ๊ฒ์ย ๋ฐ์ดํฐ ์ ํฉ์ฑ์ด์์ต๋๋ค. ์ ์ฐฉ์ ์ฟ ํฐ์ ์ ํํ ์ฝ์๋ ์๋๋ง ๋ฐ๊ธ๋์ด์ผ ํฉ๋๋ค. ์ด๋ฅผ ์ํด, ์ฌ๋ฌ ์์ฒญ์ด ๋์์ ๋ง์ง๋ง ์ฟ ํฐ์ ์ ๊ทผํ๋๋ผ๋ ๋จ ํ๋์ ์์ฒญ๋ง ์ฑ๊ณตํ๋๋ก ๋ณด์ฅํด์ฃผ๋ JPA์ ๋น๊ด์ ๋ฝ(Pessimistic Lock)์ ๋์ ํ์ต๋๋ค.
- ๊ตฌํ:ย CouponTemplateย ์กฐํ ์ย SELECT ... FOR UPDATEย ์ฟผ๋ฆฌ๋ฅผ ๋ฐ์์ํค๋ย @Lock(LockModeType.PESSIMISTIC_WRITE)๋ฅผ ์ ์ฉ.
- ์ฅ์ :ย ๋ฐ์ดํฐ ์ ํฉ์ฑ์ 100% ๋ณด์ฅํ๋, ๊ฐ๋จํ๊ณ ํ์คํ ๋ฐฉ๋ฒ์ด์์ต๋๋ค.
๐คย ์ด๊ธฐ ๊ฐ์ค:ย "๋น๊ด์ ๋ฝ์ผ๋ก ๋ฐ์ดํฐ ์ ํฉ์ฑ๋ง ์งํค๋ฉด, ์ ์ฐฉ์ ๋ฌธ์ ๋ ํด๊ฒฐ๋ ๊ฒ์ด๋ค."
์ด ๊ฐ์ค์ ๊ฒ์ฆํ๊ธฐ ์ํดย nGrinder๋ฅผ ์ฌ์ฉํ์ฌ ๋ถํ ํ ์คํธ๋ฅผ ์งํํ์ต๋๋ค.
- ํ ์คํธ ์กฐ๊ฑด:ย ๊ฐ์ ์ฌ์ฉ์(VUser) 500๋ช , ์ฟ ํฐ 10,000๊ฐ
- ํ ์คํธ ๋ชฉํ:ย ์์คํ ์ด ์์ ์ ์ผ๋ก ๋ถํ๋ฅผ ๊ฒฌ๋๋ฉฐ, ์ด๋ ์ ๋์ ์ฒ๋ฆฌ๋(TPS)์ ๋ณด์ด๋์ง ํ์ธ
ํ ์คํธ ๊ฒฐ๊ณผ๋ ๋ค์๊ณผ ๊ฐ์์ต๋๋ค.
- DB CPU ์ฌ์ฉ๋ฅ 100%:ย docker stats๋ก ํ์ธํ MySQL ์ปจํ ์ด๋์ CPU ์ฌ์ฉ๋ฅ ์ด ํ ์คํธ ์์๊ณผ ๋์์ 100%์ ๊ทผ์ ํ๋ฉฐ ๋ณ๋ชฉ ํ์์ด ๋ฐ์ํ์ต๋๋ค.
- ๊ธ๊ฒฉํ TPS ์ ํ ๋ฐ ์๋ต ์๊ฐ ์ฆ๊ฐ:ย ํ ์คํธ ์ด๋ฐ ์ ์ ๋์ TPS๋ฅผ ๋ณด์ด๋ค๊ฐ, DB ๋ฝ ๊ฒฝํฉ(Lock Contention)์ด ์ฌํ๋๋ฉด์ TPS๋ ๊ธ๊ฒฉํ ๋จ์ด์ง๊ณ ํ๊ท ์๋ต ์๊ฐ์ ์์ฒ ms๊น์ง ์น์์์ต๋๋ค.
- ์ปค๋ฅ์ ํ ๊ณ ๊ฐ:ย JMC๋ก ํ์ธํ ๊ฒฐ๊ณผ, HikariCP์ ๋ชจ๋ DB ์ปค๋ฅ์ ์ด ๊ณ ๊ฐ๋์ด ๋๊ธฐํ๋ ์ค๋ ๋๊ฐ ๋๋์ผ๋ก ๋ฐ์ํ์ต๋๋ค.
๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด, DB์ ๋ถ๋ด์ ๋์ด์ฃผ๊ณ ์ฌ์ฉ์ ๊ฒฝํ์ ํฅ์์ํค๋ย ๋น๋๊ธฐ(Asynchronous) ์ํคํ ์ฒ๋ก ์ ํ์ ๊ฒฐ์ ํ์ต๋๋ค.
๐กย ํต์ฌ ์์ด๋์ด
"์ ์ฐฉ์ ์ค ์ธ์ฐ๊ธฐ๋ฅผ ๋๋ฆฐ DB๊ฐ ์๋, ๋งค์ฐ ๋น ๋ฅธ ๋ฉ๋ชจ๋ฆฌ(Redis)์์ ์ฒ๋ฆฌํ๊ณ , ์ค์ DB ์์ ์ ๋ฉ์์ง ํ(RabbitMQ)๋ฅผ ํตํด ๋์ค์ ์์ ์ ์ผ๋ก ์ฒ๋ฆฌํ์!"
๊ฐ์ ๋ ์ํคํ ์ฒ ํ๋ฆ:
- [1์ฐจ ๋ฐฉ์ด์ : Redis]ย API ์๋ฒ๋ ์์ฒญ์ ๋ฐ์ผ๋ฉด DB ๋์ Redis์ ๋จผ์ ์ ๊ทผํฉ๋๋ค.
- ์ค๋ณต ๋ฐ๊ธ ์ฒดํฌ:ย Redis Set ์๋ฃ๊ตฌ์กฐ(SADD)๋ฅผ ์ด์ฉํด ์ฌ์ฉ์๊ฐ ์ด๋ฏธ ์ฐธ์ฌํ๋์ง ์์์ ์ผ๋ก ํ์ธํฉ๋๋ค.
- ์๋ ์ฒดํฌ:ย Redis์ย INCRย ๋ช ๋ น์ด๋ฅผ ์ด์ฉํด ํ์ฌ ๋ฐ๊ธ ์๋ ํ์๋ฅผ ์์์ ์ผ๋ก ์นด์ดํธํ๊ณ , ์ด ์๋์ ๋์๋์ง ์ฆ์ ํ๋จํฉ๋๋ค.
- [๋น ๋ฅธ ์คํจ/์ฑ๊ณต ์๋ต]ย Redis ์ฒดํฌ๋ฅผ ํต๊ณผํ์ง ๋ชปํ ์์ฒญ์ DB์ ๋๋ฌํ๊ธฐ๋ ์ ์ "์์ง๋์์ต๋๋ค" ๋๋ "์ด๋ฏธ ๋ฐ์ผ์ จ์ต๋๋ค" ๋ผ๋ ์คํจ ์๋ต์ ์ฆ์ ๋ฐ์ต๋๋ค. Redis ์ฒดํฌ๋ฅผ ํต๊ณผํ ์์ฒญ์ **'์ฑ๊ณต ๋์'**์ผ๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
- [๋ถํ ์ ์ด: RabbitMQ]ย '์ฑ๊ณต ๋์'์ด ๋ ์์ฒญ ์ ๋ณด๋ง ๋ฉ์์ง ํ(RabbitMQ)์ ๋ฉ์์ง๋ก ์ ์กํ๊ณ , API ์๋ฒ๋ ์ฌ์ฉ์์๊ฒ "์ฟ ํฐ์ด ๋ฐ๊ธ๋์์ต๋๋ค" ๋ผ๋ ์ฑ๊ณต ์๋ต์ ์ฆ์ ๋ณด๋ ๋๋ค.
- [์์ ์ ์ธ ํ์ฒ๋ฆฌ: Consumer]ย ๋ณ๋์ ํ๋ก์ธ์ค๋ก ๋์ํ๋ RabbitMQ Consumer๋ ํ์ ์์ธ ๋ฉ์์ง๋ฅผ ์์ ์ ์ฒ๋ฆฌ ์๋์ ๋ง์ถฐ ์์ฐจ์ ์ผ๋ก ๊ฐ์ ธ์์, ์ค์ DB์ ์ฟ ํฐ ๋ฐ์ดํฐ๋ฅผย INSERTํ๊ณ ์๋์ UPDATE ํ๋ ์์ ์ ์ํํฉ๋๋ค.
์๋ก์ด ์ํคํ ์ฒ๋ฅผ ๊ตฌํํ ๋ค,ย ์์ ํ ๋์ผํ ์กฐ๊ฑด์ผ๋ก nGrinder ์ฑ๋ฅ ํ ์คํธ๋ฅผ ๋ค์ ์งํํ์ต๋๋ค.
- ๋์์ง TPS ๋ฐ ์๋ต ์๊ฐ ๊ฐ์ :ย API ์๋ฒ๋ ๋ ์ด์ DB๋ฅผ ๊ธฐ๋ค๋ฆฌ์ง ์์ผ๋ฏ๋ก, ์ด์ ๊ณผ๋ ๋น๊ตํ์ฌ TPS๋ ์ฝ 8๋ฐฐ์ ๋ ์์นํ์์ต๋๋ค. ์๋ต์๊ฐ ๋ํ ๊ธฐ์กด๋๋น 80% ๊ฐ์ํ์์ต๋๋ค.
์ํคํ ์ฒ๋ฅผ ๋ณ๊ฒฝํ์์๋ ์ฌ์ ํ ๋จ์์๋ ๋ฌธ์ ์ ๋ค์ด ์์์ต๋๋ค.
- DB CPU ์ฌ์ฉ๋ฅ :ย DB์ ๋ถํ๋ ์ฌ์ ํ ๋์์ต๋๋ค.
- ์์ง ๋ถ์กฑํ TPS: TPS๋ ์์นํ์์ง๋ง ๊ทธ๋ผ์๋ ๋ถ์กฑํ๋ค๊ณ ๋๊ปด์ก์ต๋๋ค.
- RABBITMQ ๋ถํ: RABBITMQ ๋ฅผ ๋์ ํ์ฌ CPU์ฌ์ฉ๋ฅ ์ ๊ด์ฐฐํ์ ๋, 90-100%์ด์์ CPU์ฌ์ฉ์ด ์ฟ ํฐ์ด ๋ฐ๊ธ๋๋ ๋์ ์ง์์ ์ผ๋ก ๋๊ฒ ๋ํ๋๋ ๋ชจ์ต์ ๋ณด์์ต๋๋ค.
๋น๊ด์ ๋ฝ์ ์ฌ์ฉํ ์ด๊ธฐ ๋๊ธฐ ๋ฐฉ์์ ๋ฐ์ดํฐ ์ ํฉ์ฑ์ ๋ณด์ฅํ๋ ๊ฐ์ฅ ๊ฐ๋จํ ๋ฐฉ๋ฒ์ด์์ง๋ง, ๋๊ท๋ชจ ๋์ ์์ฒญ ํ๊ฒฝ์์๋ ์์คํ ์ ์ฒด๋ฅผ ๋ง๋น์ํค๋ ๋ณ๋ชฉ ์ง์ ์ด ๋์์ต๋๋ค.
Redis๋ฅผ ์ด์ฉํด ์ ์ฐฉ์ ๋ก์ง์ ๋ฉ๋ชจ๋ฆฌ๋จ์์ ์ฒ๋ฆฌํ๊ณ ,ย RabbitMQ๋ฅผ ์ด์ฉํด DB ์์ ์ ๋น๋๊ธฐํํจ์ผ๋ก์จ, ๋ ์ข์ ๊ฒฐ๊ณผ๋ฅผ ์ป์ ์ ์์์ต๋๋ค.
- ์ฌ์ฉ์ ๊ฒฝํ:ย ์ฌ์ฉ์๋ DB ์ํ์ ๋ฌด๊ดํ๊ฒ ๊ฑฐ์ ์ฆ์ ์ฑ๊ณต/์คํจ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์๋ด ๋๋ค.
์ ํ๋ฆฌ์ผ์ด์ ์ ์ฑ๋ฅ์ ์ด์ผ๊ธฐํ ๋, ์ฐ๋ฆฌ๋ ํํ ๋ณต์กํ ๋น์ฆ๋์ค ๋ก์ง์ด๋ ๋ฌด๊ฑฐ์ด ์ฟผ๋ฆฌ์ ์ง์คํ๊ณค ํฉ๋๋ค. ํ์ง๋ง ๋๋ก๋ ๊ฐ์ฅ ๋จ์ํ๊ณ ๋ฐ๋ณต์ ์ธ ์์ ์ด ์์คํ ์ ์ฒด์ ๋ฐ๋ชฉ์ ์ก๋ '์จ๊ฒจ์ง ์์ด์'๊ฐ ๋๊ธฐ๋ ํฉ๋๋ค.
์ด๋ฒ์๋ nGrinder์ ๋ชจ๋ํฐ๋ง ํด์ ํตํด, ๋ชจ๋ API์ ๊ด๋ฌธ ์ญํ ์ ํ๋ย ์ธ์ฆ ์ธํฐ์ ํฐ(Interceptor)์ ์ฑ๋ฅ ๋ณ๋ชฉ์ ๋ฐ๊ฒฌํ๊ณ ย Redis ์บ์ฑ์ผ๋ก ํด๊ฒฐํ ๊ณผ์ ์ ๊ณต์ ํฉ๋๋ค.
์ ์ฐฉ์ ์ฟ ํฐ ๋ฐ๊ธ ๊ธฐ๋ฅ์ ๋น๋๊ธฐ ์ํคํ ์ฒ๋ก ๊ฐ์ ํ ํ, ์์คํ ์ ์ ๋ฐ์ ์ธ ์ฒ๋ฆฌ๋(TPS)์ ํฌ๊ฒ ํฅ์๋์์ต๋๋ค. ํ์ง๋ง ์ฌ์ ํ ์์ฐ์น ์์ ๋ถ๋ถ์ด ์์์ต๋๋ค. ๋ฐ๋กย DB์ CPU ์ฌ์ฉ๋์ด์์ต๋๋ค.
๐คย ์๋ฌธ์
์ฟ ํฐ ๋ฐ๊ธ ์์ฒญ์ ์ด์ Redis์์ ๋๋ถ๋ถ ์ฒ๋ฆฌ๋๊ณ DB๋ก ๊ฐ์ง ์๋๋ฐ๋, nGrinder๋ก ๋์ ๋ถํ๋ฅผ ๊ฐํ๋ฉด ์ฌ์ ํ DB์ CPU ์ฌ์ฉ๋ฅ ์ด ์์๋ณด๋ค ๋๊ฒ ๋ํ๋ฌ์ต๋๋ค.ย INSERTย ์์ ์ด ์๋ ๋จ์ ์กฐํ API์ ๋ถํ๋ฅผ ๊ฐํด๋ ๋น์ทํ ํ์์ด ๋ฐ์ํ์ต๋๋ค.
์ด๋ ๋น์ฆ๋์ค ๋ก์ง ์ธ์,ย ๋ชจ๋ API ์์ฒญ์ด ๊ณตํต์ ์ผ๋ก ๊ฑฐ์น๋ ๊ตฌ๊ฐย ์ด๋๊ฐ์ DB ๋ถํ๋ฅผ ์ ๋ฐํ๋ ์์ธ์ด ์จ์ด์๋ ๊ฒ์ด์์ต๋๋ค.
๋ฒ์ธ์ ์ฐพ๊ธฐ ์ํด ๋ช ๊ฐ์ง ๋๊ตฌ๋ฅผ ํ์ฉํ์ฌ ์์คํ ์ ๋ถ์ํ์ต๋๋ค.
Grafana, DB export๋ฅผ ํตํด DB๋ฅผ ๋ชจ๋ํฐ๋ง ํ์๊ณ ์์ํ์ง ๋ชปํ๋ select ์ฟผ๋ฆฌ๊ฐ ์๋น์ค ์ด๋ฐ์ ๋ง์ด ๋ฐ์ํ๋ ๊ฒ์ ๋ฐ๊ฒฌํ์ต๋๋ค.
์ฟ ํฐ ๋ฐ๊ธ ๋ก์ง์๋ select์ฟผ๋ฆฌ๊ฐ ๋๋์ผ๋ก ๋ฐ์ํ๋ ๋ถ๋ถ์ด ์์๊ธฐ ๋๋ฌธ์ ๋น์ฆ๋์ค ๋ก์ง ์ธ ๋ค๋ฅธ ๋ถ๋ถ์ ์กฐ์ฌํ์์ต๋๋ค.
2. ์ฝ๋ ์ญ์ถ์ :
์ด ์ฟผ๋ฆฌ๊ฐ ์ด๋์ ์คํ๋๋์ง ์ฝ๋๋ฅผ ์ญ์ถ์ ํ ๊ฒฐ๊ณผ, ๋ฒ์ธ์ ๋ฐ๋กย ApiKeyAuthInterceptorย ์์ต๋๋ค.
// ApiKeyAuthInterceptor.java (Before)
@Override
public boolean preHandle(HttpServletRequest request, ...) {
String apiKey = request.getHeader("X-API-KEY");
// ...
// [๋ฌธ์ ์ ์ง์ ] ๋ชจ๋ API ์์ฒญ๋ง๋ค DB๋ฅผ ์กฐํ
Partner partner = partnerRepository.findByApiKey(apiKey)
.orElseThrow(...);
// ...
return true;
}๋ชจ๋ API ์์ฒญ์ด ํต๊ณผํด์ผ ํ๋ '๊ด๋ฌธ'์์, ๋งค๋ฒ DB์ ์ ๋ถ์ฆ(API Key) ์กฐํ๋ฅผ ์์ฒญํ๊ณ ์์๋ ๊ฒ์ ๋๋ค. VUser 500๋ช ์ด ์ด๋น 200๋ฒ์ฉ ์์ฒญ์ ๋ณด๋ด๋ฉด, DB์๋ ์ด๋น 200๊ฐ์ย SELECTย ์ฟผ๋ฆฌ๊ฐ ๊ทธ๋๋ก ์ ๋ฌ๋๊ณ ์์์ต๋๋ค.
โ๏ธย ๋ฌธ์ ์ ์
ApiKeyAuthInterceptor์ ์ธ์ฆ ๋ก์ง์ดย ๋ชจ๋ API ์์ฒญ๋ง๋ค DB ์กฐํ๋ฅผ ์ ๋ฐํ์ฌ, ์์คํ ์ ์ฒด์ ๋ถํ์ํ DB ๋ถํ๋ฅผ ๊ฐํ๊ณ ์์์ต๋๋ค. ํํธ๋ APIํค ์ ๋ณด๋ ๊ฑฐ์ ๋ณ๊ฒฝ๋์ง ์๋ ๋ฐ์ดํฐ์์๋ ๋ถ๊ตฌํ๊ณ , ๋งค๋ฒ DB์ ์ ๊ทผํ๋ ๊ฒ์ ๋งค์ฐ ๋นํจ์จ์ ์ด์์ต๋๋ค.
์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํ ๊ฐ์ฅ ์ด์์ ์ธ ๋ฐฉ๋ฒ์, ์์ฃผ ์กฐํ๋์ง๋ง ๊ฑฐ์ ๋ณ๊ฒฝ๋์ง ์๋ ํํธ๋ ์ธ์ฆ ์ ๋ณด๋ฅผย Redis์ ์บ์ฑํ๋ ๊ฒ์ ๋๋ค.
๊ฐ์ ๋ ์ธ์ฆ ํ๋ฆ:
- [Cache-Aside ํจํด]ย ์ธํฐ์ ํฐ๋ ์์ฒญ์ ๋ฐ์ผ๋ฉด, ๋จผ์ ย Redis์ API Key๊ฐ ์๋์ง ํ์ธํฉ๋๋ค.
- (Cache Hit)ย Redis์ Key๊ฐ ์กด์ฌํ๋ฉด, DB๋ฅผ ์กฐํํ์ง ์๊ณ ์ฆ์ ์บ์๋ ํํธ๋ ์ ๋ณด๋ฅผ ์ฌ์ฉํ์ฌ ์ธ์ฆ์ ํต๊ณผ์ํต๋๋ค. (๋๋ถ๋ถ์ ์์ฒญ์ด ์ด ๊ฒฝ๋ก๋ฅผ ๋ฐ๋ฆ)
- (Cache Miss)ย Redis์ Key๊ฐ ์์ผ๋ฉด,ย ๊ทธ๋์์ผ DB์ ์ ๊ทผํ์ฌ ํํธ๋ ์ ๋ณด๋ฅผ ์กฐํํฉ๋๋ค.
- DB์์ ์กฐํํ ์ ๋ณด๋ ๋ค์์ ์ฌ์ฌ์ฉํ ์ ์๋๋ก Redis์ ์ ์ฅํ ๋ค, ์ธ์ฆ์ ํต๊ณผ์ํต๋๋ค.
์ธ์ฆ ๋ก์ง์ Redis ์บ์ฑ์ ์ ์ฉํ ํ, ๋์ผํ ์กฐ๊ฑด์ผ๋ก k6 ๋ถํ ํ ์คํธ๋ฅผ ๋ค์ ์งํํ์ต๋๋ค.
- DB CPU ์ฌ์ฉ๋ฅ ๋ํญ ๊ฐ์:ย SELECT ... FROM partnersย ์ฟผ๋ฆฌ๊ฐ ๊ฑฐ์ ๋ฐ์ํ์ง ์๊ฒ ๋๋ฉด์, DB CPU ์ฌ์ฉ๋ฅ ์ดย ํ๊ท 20% ๋ฏธ๋ง์ผ๋ก ๋งค์ฐ ์์ ์ ์ผ๋ก ์ ์ง๋์์ต๋๋ค.
- ์ ์ฒด TPS ํฅ์:ย DB์ ๋ถํ๊ฐ ์ค์ด๋ค์, ์์คํ ์ ์ฒด๊ฐ ๋ ๋ง์ ์์ฒญ์ ์ฒ๋ฆฌํ ์ ์๊ฒ ๋์ด ์ ์ฒด์ ์ธ TPS ๋ํ ์ํญ ์์นํ์ต๋๋ค.
| ์งํ | Before (๋งค๋ฒ DB ์กฐํ) | After (Redis ์บ์ฑ ์ ์ฉ) |
|---|---|---|
| SELECT ย ์ฟผ๋ฆฌ ์ | 3.400(MAX) | 67(MAX) |
| DB CPU ์ฌ์ฉ๋ฅ (max) | 120%(MAX) | 25%(MAX) |
์ด๋ฒ ์ฑ๋ฅ ๊ฐ์ ์ ํตํด, ๋ณต์กํ ๋น์ฆ๋์ค ๋ก์ง๋ฟ๋ง ์๋๋ผย ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ณตํต ๋ก์ง(์ธ์ฆ, ๋ก๊น ๋ฑ) ๋ํ ์ฑ๋ฅ์ ๋ฏธ์น๋ ์ํฅ์ด ๋งค์ฐ ํฌ๋ค๋ ๊ฒ์ ๋ค์ ํ๋ฒ ํ์ธํ์ต๋๋ค.
๋จ์ํ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ ๊ฒ์ ๋์ด, ๋ชจ๋ํฐ๋ง์ ํตํด ์์คํ ์ ๋ณ๋ชฉ ์ง์ ์ ๋ฐ์ดํฐ ๊ธฐ๋ฐ์ผ๋ก ์ฐพ์๋ด๊ณ , ์บ์ฑ๊ณผ ๊ฐ์ ์ ์ ํ ๊ธฐ์ ์ ์ ์ฉํ์ฌ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ๊ฒ์ด ์์ ์ ์ธ ์๋น์ค๋ฅผ ๋ง๋ค๊ธฐ ์ํ ๋ฐฑ์๋ ๊ฐ๋ฐ์์ ํต์ฌ ์ญ๋์์ ๊นจ๋ฌ์์ต๋๋ค.
4๏ธโฃ [์ฑ๋ฅ ๊ฐ์ ] ์ฟ ํฐ ๋ฐ๊ธ Consumer ์ฑ๋ฅ ์ต์ ํ: JPA saveAll์์ JdbcTemplate์ผ๋ก์ ์ ํ
์ ์ฐฉ์ ์ฟ ํฐ ๋ฐ๊ธ ์์คํ ์ API ์๋ต ์๋๋ฅผ ๊ฐ์ ํ๊ธฐ ์ํด RabbitMQ๋ฅผ ๋์ ํ์ฌ DB ์ ์ฅ์ ๋น๋๊ธฐ ๋ฐฉ์์ผ๋ก ์ ํํ์ต๋๋ค. ์ด๋ก์จ ์ฌ์ฉ์ ์์ฒญ์ ๋นจ๋ผ์ก์ง๋ง, ์ด์ ๋ ์ด๋น ์์ฒ ๊ฐ์ ๋ฉ์์ง๋ฅผ ์ฒ๋ฆฌํด์ผ ํ๋ Consumer์ DB ์ฐ๊ธฐ ์์ ์ด ์๋ก์ด ๋ณ๋ชฉ ์ง์ ์ผ๋ก ๋ ์ฌ๋์ต๋๋ค.
RabbitMQ Consumer๊ฐ ๋๋์ ๋ฉ์์ง๋ฅผ ๋ฐฐ์น(Batch)๋ก ์ฒ๋ฆฌํ ๋, ์์๋ณด๋ค DB INSERT ์ฑ๋ฅ์ด ๋์ค์ง ์๋ ๊ฒ์ ํ์ธํ์ต๋๋ค. repository.saveAll()๋ก ์์ฑ๋ ๋ฐฐ์น ์ ๋ต์ด ์์๊ณผ๋ ๋ฌ๋ฆฌ ๊ฐ๋ณ INSERT ๋ก ๋์ํ์๊ณ , ์ด๋ JPA์ ์ฐ๊ธฐ ์ง์ฐ ๋ฐ ๋ฐฐ์น INSERT ์ต์ ํ๋ฅผ ๋ฐฉํดํ๋ ์ฃผ๋ ์์ธ์ด์์ต๋๋ค.
๋จ์ํ for ๋ฃจํ ์์์ repository.save()๋ฅผ ํธ์ถํ๋ ๋ฐฉ์์, ์ฒ๋ฆฌํด์ผ ํ ๋ฉ์์ง ์๋งํผ DB์์ ๋คํธ์ํฌ ํต์ (Round Trip)๊ณผ ํธ๋์ญ์
์ ๋ฐ์์์ผ, ๋๊ท๋ชจ ํธ๋ํฝ ์ํฉ์์ DB์ ๊ทน์ฌํ ๋ถํ๋ฅผ ์ฃผ์์ต๋๋ค.
๊ฐ์ฅ ๋จผ์ JPA๊ฐ ์ ๊ณตํ๋ ๋ฐฐ์น(Batch) ์ฒ๋ฆฌ ๊ธฐ๋ฅ์ ํ์ฉํ์ฌ DB์์ ํต์ ํ์๋ฅผ ์ค์ด๋ ๊ฒ์ ๋ชฉํ๋ก ํ์ต๋๋ค.
MySQL ํ๊ฒฝ์์ ์ฌ์ฉํ๋ GenerationType.IDENTITY(AUTO_INCREMENT) ์ ๋ต์, INSERT ์ฟผ๋ฆฌ๊ฐ DB์์ ์คํ๋ ํ์์ผ ID๋ฅผ ์ ์ ์์ต๋๋ค. ์ด ๋๋ฌธ์ JPA๋ ์ฌ๋ฌ INSERT๋ฅผ ํ๋์ ๋ฐฐ์น๋ก ๋ฌถ์ด ๋ณด๋ด์ง ๋ชปํ๊ณ , ๊ฒฐ๊ตญ saveAll()์ ์ฌ์ฉํ๋๋ผ๋ ๋ด๋ถ์ ์ผ๋ก๋ ๊ฐ๋ณ INSERT๋ฅผ ์คํํ๋ ๊ฒ๊ณผ ๊ฐ์ ๋นํจ์จ์ด ๋ฐ์ํ์ต๋๋ค.
์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด, INSERT ์ ์ ID๋ฅผ ๋ฏธ๋ฆฌ ํ ๋น๋ฐ์ ์ ์๋ GenerationType.SEQUENCE ์ ๋ต์ผ๋ก ๋ณ๊ฒฝํ๊ณ ์ ํ์ต๋๋ค.
ํ์ง๋ง MYSQL์ SEQUENCE๋ฅผ ์ง์ํ์ง ์์๊ณ , Hibernate๊ฐ hibernate_sequence ํ
์ด๋ธ์ ํตํด ์ด๋ฅผ ํ๋ด ๋ด๋ ๋ฐฉ์์ผ๋ก ๋์ํ์ต๋๋ค.
์ถ๊ฐ์ ์ธ ํ
์ด๋ธ๊ณผ ๊ด๋ฆฌ๊ฐ ํ์ํ๋ค๋ ์ ๊ณผ JPA์ saveAll์ ๋งค์ฐ ํจ์จ์ ์ด์ง๋ง, ๋ด๋ถ์ ์ผ๋ก๋ ์ฌ์ ํ ์์์ฑ ์ปจํ
์คํธ(Persistence Context) ๊ด๋ฆฌ, Dirty Checking ๋ฑ JPA ๊ณ์ธต์ ์ค๋ฒํค๋๊ฐ ์กด์ฌํ๋ค๋ ๋จ์ ์ด ์์์ต๋๋ค.
์ค์๊ฐ์ผ๋ก ์๋ง ๊ฑด์ ๋ฉ์์ง๋ฅผ ์ฒ๋ฆฌํด์ผ ํ๋ Consumer ๋ก์ง์์๋ ์ด ๋ฏธ์ธํ ์ค๋ฒํค๋๋ง์ ์ ๊ฑฐํ์ฌ, ๊ฐ์ฅ ์์ํ JDBC ๋ ๋ฒจ์ ์ต๊ณ ์ฑ๋ฅ์ ํ๋ณดํ ํ์๊ฐ ์๋ค๊ณ ํ๋จํ์ต๋๋ค.
JPA/Hibernate ๊ณ์ธต์ ์์ ํ ์ฐํํ์ฌ, JDBC ๋๋ผ์ด๋ฒ๊ฐ ์ ๊ณตํ๋ ๊ฐ์ฅ ์ต์ ํ๋ ๋ฐฉ์์ผ๋ก ๋ฐฐ์น INSERT๋ฅผ ์คํํ๊ธฐ ์ํด JdbcTemplate์ ๋์
ํ์ต๋๋ค.
issueCouponsInBatch ๋ฉ์๋๋ฅผ ์์ ํ์ฌ, Consumer๊ฐ ๋ฐ์ List<CouponIssueMessage>๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์์ INSERT SQL์ ์คํํ๋๋ก ๋ณ๊ฒฝํ์ต๋๋ค.
private void issueCouponsInBatchByJdbc(List<CouponIssueMessage> messages) {
String sql = "INSERT INTO coupons (...) VALUES (?, ?, ...)";
jdbcTemplate.batchUpdate(sql,
messages,
100, // Batch Size
(PreparedStatement ps, CouponIssueMessage message) -> {
// PreparedStatement์ ์ง์ ํ๋ผ๋ฏธํฐ ์ค์
ps.setLong(1, message.getPartnerId());
// ...
});
}10๋ง ๊ฑด ์ด์์ ๋๊ท๋ชจ ๋ฐ์ดํฐ๋ฅผ ํ ๋ฒ์ ์ฒ๋ฆฌํ๋ ค ํ ๋, JDBC ๋๋ผ์ด๋ฒ๊ฐ ๊ฑฐ๋ํ SQL ์ฟผ๋ฆฌ ๋ฌธ์์ด์ ์์ฑํ๋ค๊ฐ OutOfMemoryError: Java heap space๊ฐ ๋ฐ์ํ๋ ๋ฌธ์ ๋ฅผ ๋ฐ๊ฒฌํ์ต๋๋ค.
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด, Google์ Guava ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ์ ์ฒด ๋ฉ์์ง ๋ฆฌ์คํธ๋ฅผ 10000๊ฐ ๋จ์์ ์์ ๋ฌถ์(Chunk)์ผ๋ก ๋๋๊ณ , ๊ฐ ๋ฌถ์์ ๋ํด batchUpdate๋ฅผ ๋ฐ๋ณต ์คํํ๋ ์ฒญํน(Chunking) ๊ธฐ๋ฒ์ ์ ์ฉํ์ต๋๋ค. ์ด๋ก์จ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ ์์ ์ ์ผ๋ก ์ ์งํ๋ฉด์ ๋์ฉ๋ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ ์ ์๊ฒ ๋์์ต๋๋ค.
List<List<CouponIssueMessage>> partitionedMessages = Lists.partition(messages, 1000);
for (List<CouponIssueMessage> chunk : partitionedMessages) {
jdbcTemplate.batchUpdate(sql, chunk, ...);
}BatchInsertPerformanceTest๋ฅผ ํตํด ์ฌ๋ฌ ๋ฐ์ดํฐ๋ฅผ ๊ธฐ์ค์ผ๋ก ๋ ๋ฐฉ์์ ์ฑ๋ฅ์ ์ธก์ ํ ๊ฒฐ๊ณผ, JdbcTemplate.batchUpdate๊ฐ JPA์ saveAll๋ณด๋ค ๋ ๋น ๋ฅด๊ณ ์์ ์ ์ธ ์ฑ๋ฅ์ ๋ณด์ฌ์ฃผ์์ต๋๋ค.
@Test
@DisplayName("์ฑ๋ฅ ํ
์คํธ: ๋ฉ์์ง x๊ฑด์ผ๋ก ์ฟ ํฐ ์์ฑ ๋ฐ ์๋ ์
๋ฐ์ดํธ ๋์ ์ฒ๋ฆฌ")
@Transactional
void issue_and_update_quantity_batch_test() {
// given
StopWatch stopWatch = new StopWatch();
// when
stopWatch.start();
couponIssueSyncService.issueCouponsAndUpdateQuantityInBatch(testMessages);
stopWatch.stop();
// then
System.out.println("--- ์ ์ฒด ๋ฐฐ์น ์์
(INSERT + UPDATE) ์คํ ์๊ฐ ---");
System.out.println("Total Time (ms) for " + DATA_SIZE + " messages: " + stopWatch.getTotalTimeMillis());
long actualCouponCount = couponRepository.count();
CouponTemplate updatedTemplate = couponTemplateRepository.findById(template.getId()).orElseThrow();
assertThat(actualCouponCount).isEqualTo(DATA_SIZE);
assertThat(updatedTemplate.getIssuedQuantity()).isEqualTo(DATA_SIZE);
}| ๋ฐฉ์ | 1000๊ฑด | 10_000๊ฑด | 100_000๊ฑด |
|---|---|---|---|
JPA saveAll |
886ms | 5995ms | 53254ms |
JdbcTemplate.batchUpdate |
161ms | 586ms | 3640ms |
| ๊ฐ์ ๋น์จ | 5.5๋ฐฐ | 10.2๋ฐฐ | 14.6๋ฐฐ |
๊ฒฐ๋ก ์ ์ผ๋ก, JdbcTemplate์ ์ง์ ์ฌ์ฉํ๋ ๋ฐฉ์์ ์ฝ๋์ ๋ณต์ก์ฑ์ด ์ฝ๊ฐ ์ฆ๊ฐํ๋ ๋์ , ์ค์๊ฐ์ผ๋ก ๋๋์ ์ฐ๊ธฐ ์์
์ด ๋ฐ์ํ๋ ๋ฉ์์ง ํ Consumer ๋ก์ง์์ ์ต๊ณ ์ ์ฑ๋ฅ๊ณผ ์์ ์ฑ์ ๋ณด์ฅํ๋ ๊ฐ์ฅ ํ์คํ ์ํคํ
์ฒ์์ ์ฆ๋ช
ํ์ต๋๋ค.
์ด๊ธฐ์ ๋๊ธฐ ๋ฐฉ์ ์ํคํ ์ฒ์ ์ต์ข ์ ์ธ ๋น๋๊ธฐ ์ํคํ ์ฒ์ ์ฑ๋ฅ์ k6๋ฅผ ์ฌ์ฉํ์ฌ ๋์ผํ ์กฐ๊ฑด์์ ์ธก์ ํ ๊ฒฐ๊ณผ, ๋ค์๊ณผ ๊ฐ์ ๊ทน์ ์ธ ์ฑ๋ฅ ํฅ์์ ํ์ธํ ์ ์์์ต๋๋ค.
| ์งํ | ๊ฐ์ ์ | ๊ฐ์ ํ | ๊ฐ์ ๊ฒฐ๊ณผ |
|---|---|---|---|
| ํ๊ท ์ฒ๋ฆฌ๋(RPS) | ์ฝ 177 RPS | ์ฝ 4300+ RPS | ์ฝ 24๋ฐฐ ์ด์ ์ฑ๋ฅ ํฅ์ |
| ์๋ต ์๊ฐ(P95) | 4.17s | 225ms | ์๋ต ์๊ฐ 18๋ฐฐ ์ด์ ์๋ ํฅ์(95%๋จ์ถ) |
๊ฐ์ ์ (Before)
๊ฐ์ ํ (After)
ํ์ฌ์ ๋ชจ์ต์ด ์๋ฒฝํ ์ค๊ณ๋ผ๊ณ ๋ ์๊ฐํ์ง ์๊ณ ๋ ๋ฐ์ ์ํฌ ๋ถ๋ถ์ด ๋ง๋ค๊ณ ์๊ฐํฉ๋๋ค. ๊ทธ๋ผ์๋ ์ ์ฐฉ์ ์ฟ ํฐ ๋ฐ๊ธ์ ๊ฒฝ์ฐ ์ฒ์์ ์ค๊ณ์ ๋น๊ตํ์ ๋, ์ ์๋ฏธํ ์ฑ๋ฅ ํฅ์์ ์ด๋ค๋ผ ์ ์์์ต๋๋ค.
-
์ฑ๋ฅ ๊ฐ์ ์ ๋จ์ํ ์ถ์ธก์ผ๋ก๋ง ํ๋ฉด ์๋๋ค -> ์ด์ ํ๋ก์ ํธ์์๋ ๋จ์ํ ์ด๋ก ์ ์ธ ์ถ์ธก์ผ๋ก๋ง ์ฑ๋ฅ์ ๊ณ ๋ คํ์ง๋ง ์ค์ ํ ์คํธ๋ฅผ ์งํํ์ ๋, ์ด๋ก ๊ณผ ๋ค๋ฅธ ๊ฒฝ์ฐ๊ฐ ๋ง์์ต๋๋ค. ์ง์ ํ ์คํธ๋ฅผ ํด๋ด์ผ์ง๋ง ๋ฌธ์ ์ ์ด ๋ฌด์์ธ์ง ํ์ ํ ์ ์์ต๋๋ค.
-
๊ธฐ์ ์ ํธ๋ ์ด๋ ์คํ ๊ด๊ณ๊ฐ ๋ง๋ค -> ๊ธฐ์ ์ ์ ์ฉํ ๋, ํน์ ๊ธฐ์ ์ด ์ธ์ ๋ ๋ฌด์กฐ๊ฑด ์ข์ ๊ฒฝ์ฐ๋ ๊ฑฐ์ ์๋ ๊ฒ ๊ฐ์ต๋๋ค. ๊ฐ ๊ธฐ์ ๋ง๋ค ์ฅ๋จ์ ์ด ์๊ธฐ ๋๋ฌธ์ ๋ณธ์ธ์ ์ํฉ์ ๋ง๊ฒ ์ ์ ์ฉํด์ผ ํฉ๋๋ค.

.png)
.png)
.png)





