Skip to content

Do1K/b2b-point-service

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

8 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿš€ ๋Œ€๊ทœ๋ชจ ํŠธ๋ž˜ํ”ฝ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ๋ถ„์‚ฐ ์ฟ ํฐ/ํฌ์ธํŠธ ์‹œ์Šคํ…œ

๐Ÿ“„ ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

๋ณธ ํ”„๋กœ์ ํŠธ๋Š” ์‹ค์ œ E-commerce ํ™˜๊ฒฝ์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๋Œ€๊ทœ๋ชจ ๋™์‹œ ์ ‘์† ์ƒํ™ฉ์„ ๊ฐ€์ •ํ•˜์—ฌ, ์„ ์ฐฉ์ˆœ ์ฟ ํฐ ๋ฐœ๊ธ‰ ๋ฐ ํฌ์ธํŠธ ์‹œ์Šคํ…œ์„ ์•ˆ์ •์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฑ์—”๋“œ ์‹œ์Šคํ…œ์„ ์„ค๊ณ„ํ•˜๊ณ  ๊ตฌ์ถ•ํ•˜๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค.

๋‹จ์ˆœํ•œ ๊ธฐ๋Šฅ ๊ตฌํ˜„์„ ๋„˜์–ด, ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ ๋„๊ตฌ(k6, nGrinder)์™€ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ(Prometheus, Grafana)์„ ์ ๊ทน์ ์œผ๋กœ ํ™œ์šฉํ•˜์—ฌ ์‹œ์Šคํ…œ์˜ ๋ณ‘๋ชฉ ์ง€์ ์„ ๋ถ„์„ํ•˜๊ณ , ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ, ๋ถ„์‚ฐ ์บ์‹ฑ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ด์ค‘ํ™” ๋“ฑ ๋‹ค์–‘ํ•œ ๊ธฐ์ˆ ์„ ์ ์šฉํ•˜์—ฌ ์‹œ์Šคํ…œ์„ ์ ์ง„์ ์œผ๋กœ ๊ฐœ์„ ํ•ด ๋‚˜๊ฐ€๋Š” ๊ณผ์ •์— ์ง‘์ค‘ํ–ˆ์Šต๋‹ˆ๋‹ค.

โœจ ์ฃผ์š” ๊ธฐ๋Šฅ

  • ์„ ์ฐฉ์ˆœ ์ฟ ํฐ ๋ฐœ๊ธ‰: ๋Œ€๊ทœ๋ชจ ๋™์‹œ ์š”์ฒญ์—๋„ ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ์„ ๋ณด์žฅํ•˜๋Š” ๋น„๋™๊ธฐ ์ฟ ํฐ ๋ฐœ๊ธ‰ API

  • ์ฟ ํฐ ์‚ฌ์šฉ ๋ฐ ์กฐํšŒ: ๋น„๊ด€์  ๋ฝ(Pessimistic Lock)์„ ์ด์šฉํ•œ ์•ˆ์ „ํ•œ ์ฟ ํฐ ์‚ฌ์šฉ ์ฒ˜๋ฆฌ ๋ฐ ์กฐํšŒ API

  • ํฌ์ธํŠธ ์‹œ์Šคํ…œ: ์ฟ ํฐ๊ณผ ์—ฐ๊ณ„ํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ํฌ์ธํŠธ ์ ๋ฆฝ/์‚ฌ์šฉ/์†Œ๋ฉธ ๊ธฐ๋Šฅ์˜ ๊ธฐ๋ฐ˜ ์„ค๊ณ„

  • ํ†ตํ•ฉ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์•Œ๋ฆผ: Prometheus, Grafana, Alertmanager๋ฅผ ์—ฐ๋™ํ•˜์—ฌ ์‹œ์Šคํ…œ์˜ ์„ฑ๋Šฅ์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๊ณ , ์ด์ƒ ์ง•ํ›„ ๋ฐœ์ƒ ์‹œ Discord๋กœ ์ž๋™ ์•Œ๋ฆผ ์ˆ˜์‹ 

๐Ÿ—๏ธ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„ ๋ฐ ๋ฐœ์ „ ๊ณผ์ •

1๏ธโƒฃ [์ด์Šˆ] ํฌ์ธํŠธ ์‚ฌ์šฉ: ๋‚™๊ด€์  ๋ฝ VS ๋น„๊ด€์  ๋ฝ

์ดˆ๊ธฐ์—๋Š” ํฌ์ธํŠธ ์‚ฌ์šฉ์ด๋‚˜ ์ ๋ฆฝ์ด ๋™์‹œ์— ๋ฐœ์ƒํ•  ์ผ์€ ๊ฑฐ์˜ ์—†์„ ๊ฒƒ์ด๋ผ๊ณ  ๊ฐ€์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์ฆ‰, ๋Œ€๋ถ€๋ถ„์˜ ํŠธ๋žœ์žญ์…˜ ์ถฉ๋Œ์ด ์—†์„ ๊ฒƒ์ด๋ผ ๊ฐ€์ •ํ•˜์—ฌ ๋‚™๊ด€์  ๋ฝ์„ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ ๋ฐ๋“œ๋ฝ์ด ๋ฐœ์ƒํ•˜์˜€๋‹ค. ๋กœ๊ทธ๋ฅผ ํ†ตํ•ด ํ™•์ธํ•ด๋ณด๋‹ˆ 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 ๋ฐœ์ƒ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์ด ๋•Œ, ์„ค๋ช…์˜ ํŽธ์˜๋ฅผ ์œ„ํ•ด ๋‘ ํŠธ๋žœ์žญ์…˜์ด ๊ฒฝ์Ÿํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

  1. ๋™์‹œ ์ง„์ž… ๋ฐ s-lockํš๋“:
    • ๋‘ ํŠธ๋žœ์žญ์…˜ ๋ชจ๋‘ย pointWalletRepository.findByPartnerIdAndUserId(...)๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์ดย SELECTย ์ฟผ๋ฆฌ๋Š”ย point_walletsย ํ…Œ์ด๋ธ”์˜ row๋ฅผ ์ฝ๊ธฐ ์œ„ํ•ด ์ ‘๊ทผํ•ฉ๋‹ˆ๋‹ค.
    • ๋กœ๊ทธ๋ฅผ ๋ณด๋ฉด, ๋‘ ํŠธ๋žœ์žญ์…˜ ๋ชจ๋‘ย lock mode Sย ๋ฅผย HOLDS THE LOCK(S)ย ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ฆ‰, ๋‘ ํŠธ๋žœ์žญ์…˜ ๋ชจ๋‘ย id=26์ธ row์— ๋Œ€ํ•ดย ์ฝ๊ธฐ(๊ณต์œ ) ๋ฝ์„ ๋™์‹œ์— ํš๋“ํ•˜๋Š” ๋ฐ ์„ฑ๊ณตํ–ˆ์Šต๋‹ˆ๋‹ค.
  2. ๋‚™๊ด€์  ๋ฝ์„ ์œ„ํ•œ ์—…๋ฐ์ดํŠธ(UPDATE) ์‹œ๋„:
    • ๋‘ ์Šค๋ ˆ๋“œ ๋ชจ๋‘ ๋ฉ”๋ชจ๋ฆฌ ์ƒ์—์„œย wallet.use(amount)๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
    • ์ด์ œ ํŠธ๋žœ์žญ์…˜์ด ๋๋‚˜๋Š” ์‹œ์ ์—, JPA๋Š” ๋ณ€๊ฒฝ๋œย PointWalletย ์—”ํ‹ฐํ‹ฐ๋ฅผ DB์— ๋ฐ˜์˜ํ•˜๊ธฐ ์œ„ํ•ดย UPDATEย ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•˜๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. ์ดย UPDATEย ์ฟผ๋ฆฌ์—๋Š”ย @Versionย ๋•Œ๋ฌธ์—ย WHERE ... AND version=0ย ์กฐ๊ฑด์ด ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.
    • UPDATE๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•ด์•ผ ํ•˜๋ฏ€๋กœ, ๊ธฐ์กด์˜ ๊ณต์œ  ๋ฝ(S-Lock)์„ย ๋ฐฐํƒ€์  ๋ฝ(X-Lock)์œผ๋กœ ์—…๊ทธ๋ ˆ์ด๋“œํ•˜๋ ค๊ณ  ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค.
  3. 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๊ฐ€ ๋๋‚˜๊ธฐ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๋Š”,ย ์ „ํ˜•์ ์ธ ์ˆœํ™˜ ๋Œ€๊ธฐ ์ƒํƒœ, ์ฆ‰ ๋ฐ๋“œ๋ฝ์ด ์™„์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  4. 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)**ํ•ฉ๋‹ˆ๋‹ค.
  • ์• ์ดˆ์— ๋‘ ์Šค๋ ˆ๋“œ๊ฐ€ ๋™์‹œ์— ๋ฝ์„ ์žก๋Š” ์ƒํ™ฉ ์ž์ฒด๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ, '๋ฝ ์—…๊ทธ๋ ˆ์ด๋“œ ๊ฒฝ์Ÿ'์œผ๋กœ ์ธํ•œ ๋ฐ๋“œ๋ฝ์€ย ์›์ฒœ์ ์œผ๋กœ ์ฐจ๋‹จ๋ฉ๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ๋‹ค๋ฅธ ์‹œ๋‚˜๋ฆฌ์˜ค์˜ ๊ฒฝ์šฐ์—๋Š” ๋น„๊ด€์  ๋ฝ์„ ์‚ฌ์šฉํ•˜๋”๋ผ๋„ ๋ฐ๋“œ๋ฝ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์‹œ๋‚˜๋ฆฌ์˜ค: ํฌ์ธํŠธ ์ด์ „(Transfer) ๊ธฐ๋Šฅ

์‚ฌ์šฉ์ž A๊ฐ€ ์‚ฌ์šฉ์ž B์—๊ฒŒ ํฌ์ธํŠธ๋ฅผ ์ด์ „ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๋งŒ๋“ ๋‹ค๊ณ  ๊ฐ€์ •

  • ํŠธ๋žœ์žญ์…˜ 1 (์Šค๋ ˆ๋“œ 1):ย ์‚ฌ์šฉ์ž A -> ์‚ฌ์šฉ์ž B์—๊ฒŒ 100์  ์ด์ „
    1. ์‚ฌ์šฉ์ž A์˜ย point_walletsย row์—ย SELECT ... FOR UPDATE๋กœย ๋ฝ์„ ๊ฑด๋‹ค.ย (์„ฑ๊ณต)
    2. (์–ด๋–ค ๋กœ์ง ์ฒ˜๋ฆฌ ํ›„...)
    3. ์‚ฌ์šฉ์ž B์˜ย point_walletsย row์—ย SELECT ... FOR UPDATE๋กœย ๋ฝ์„ ๊ฑธ๋ ค๊ณ  ํ•œ๋‹ค.
  • ํŠธ๋žœ์žญ์…˜ 2 (์Šค๋ ˆ๋“œ 2):ย ๊ฑฐ์˜ ๋™์‹œ์—, ์‚ฌ์šฉ์ž B -> ์‚ฌ์šฉ์ž A์—๊ฒŒ 50์  ์ด์ „
    1. ์‚ฌ์šฉ์ž B์˜ย point_walletsย row์—ย SELECT ... FOR UPDATE๋กœย ๋ฝ์„ ๊ฑด๋‹ค.ย (์„ฑ๊ณต)
    2. (์–ด๋–ค ๋กœ์ง ์ฒ˜๋ฆฌ ํ›„...)
    3. ์‚ฌ์šฉ์ž A์˜ย point_walletsย row์—ย SELECT ... FOR UPDATE๋กœย ๋ฝ์„ ๊ฑธ๋ ค๊ณ  ํ•œ๋‹ค.

๊ต์ฐฉ ์ƒํƒœ (Deadlock) ๋ฐœ์ƒ!

์‹œ๊ฐ„ ํŠธ๋žœ์žญ์…˜ 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๏ธโƒฃ [์„ฑ๋Šฅ ๊ฐœ์„ ] ์„ ์ฐฉ์ˆœ ์ฟ ํฐ ๋ฐœ๊ธ‰ ์‹œ์Šคํ…œ: ๋น„๊ด€์  ๋ฝ์˜ ํ•œ๊ณ„๋ฅผ ๋„˜์–ด ๋น„๋™๊ธฐ ์•„ํ‚คํ…์ฒ˜๋กœ

1. ์ดˆ๊ธฐ ์ ‘๊ทผ: ๋น„๊ด€์  ๋ฝ์„ ์ด์šฉํ•œ ๋™๊ธฐ ์ฒ˜๋ฆฌ

๊ฐ€์žฅ ๋จผ์ € ๊ณ ๋ คํ•œ ๊ฒƒ์€ย ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ์ด์—ˆ์Šต๋‹ˆ๋‹ค. ์„ ์ฐฉ์ˆœ ์ฟ ํฐ์€ ์ •ํ™•ํžˆ ์•ฝ์†๋œ ์ˆ˜๋Ÿ‰๋งŒ ๋ฐœ๊ธ‰๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด, ์—ฌ๋Ÿฌ ์š”์ฒญ์ด ๋™์‹œ์— ๋งˆ์ง€๋ง‰ ์ฟ ํฐ์— ์ ‘๊ทผํ•˜๋”๋ผ๋„ ๋‹จ ํ•˜๋‚˜์˜ ์š”์ฒญ๋งŒ ์„ฑ๊ณตํ•˜๋„๋ก ๋ณด์žฅํ•ด์ฃผ๋Š” JPA์˜ ๋น„๊ด€์  ๋ฝ(Pessimistic Lock)์„ ๋„์ž…ํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ๊ตฌํ˜„:ย CouponTemplateย ์กฐํšŒ ์‹œย SELECT ... FOR UPDATEย ์ฟผ๋ฆฌ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š”ย @Lock(LockModeType.PESSIMISTIC_WRITE)๋ฅผ ์ ์šฉ.
  • ์žฅ์ :ย ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ์„ 100% ๋ณด์žฅํ•˜๋Š”, ๊ฐ„๋‹จํ•˜๊ณ  ํ™•์‹คํ•œ ๋ฐฉ๋ฒ•์ด์—ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿค”ย ์ดˆ๊ธฐ ๊ฐ€์„ค:ย "๋น„๊ด€์  ๋ฝ์œผ๋กœ ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ๋งŒ ์ง€ํ‚ค๋ฉด, ์„ ์ฐฉ์ˆœ ๋ฌธ์ œ๋Š” ํ•ด๊ฒฐ๋  ๊ฒƒ์ด๋‹ค."

2. ๋ฌธ์ œ ๋ฐœ๊ฒฌ: nGrinder ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ์™€ ๋งˆ์ฃผํ•œ ํ˜„์‹ค

์ด ๊ฐ€์„ค์„ ๊ฒ€์ฆํ•˜๊ธฐ ์œ„ํ•ดย nGrinder๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ํ…Œ์ŠคํŠธ ์กฐ๊ฑด:ย ๊ฐ€์ƒ ์‚ฌ์šฉ์ž(VUser) 500๋ช…, ์ฟ ํฐ 10,000๊ฐœ
  • ํ…Œ์ŠคํŠธ ๋ชฉํ‘œ:ย ์‹œ์Šคํ…œ์ด ์•ˆ์ •์ ์œผ๋กœ ๋ถ€ํ•˜๋ฅผ ๊ฒฌ๋””๋ฉฐ, ์–ด๋А ์ •๋„์˜ ์ฒ˜๋ฆฌ๋Ÿ‰(TPS)์„ ๋ณด์ด๋Š”์ง€ ํ™•์ธ

ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์•˜์Šต๋‹ˆ๋‹ค.

  • DB CPU ์‚ฌ์šฉ๋ฅ  100%:ย docker stats๋กœ ํ™•์ธํ•œ MySQL ์ปจํ…Œ์ด๋„ˆ์˜ CPU ์‚ฌ์šฉ๋ฅ ์ด ํ…Œ์ŠคํŠธ ์‹œ์ž‘๊ณผ ๋™์‹œ์— 100%์— ๊ทผ์ ‘ํ•˜๋ฉฐ ๋ณ‘๋ชฉ ํ˜„์ƒ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ๊ธ‰๊ฒฉํ•œ TPS ์ €ํ•˜ ๋ฐ ์‘๋‹ต ์‹œ๊ฐ„ ์ฆ๊ฐ€:ย ํ…Œ์ŠคํŠธ ์ดˆ๋ฐ˜ ์ž ์‹œ ๋†’์€ TPS๋ฅผ ๋ณด์ด๋‹ค๊ฐ€, DB ๋ฝ ๊ฒฝํ•ฉ(Lock Contention)์ด ์‹ฌํ™”๋˜๋ฉด์„œ TPS๋Š” ๊ธ‰๊ฒฉํžˆ ๋–จ์–ด์ง€๊ณ  ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„์€ ์ˆ˜์ฒœ ms๊นŒ์ง€ ์น˜์†Ÿ์•˜์Šต๋‹ˆ๋‹ค.
  • ์ปค๋„ฅ์…˜ ํ’€ ๊ณ ๊ฐˆ:ย JMC๋กœ ํ™•์ธํ•œ ๊ฒฐ๊ณผ, HikariCP์˜ ๋ชจ๋“  DB ์ปค๋„ฅ์…˜์ด ๊ณ ๊ฐˆ๋˜์–ด ๋Œ€๊ธฐํ•˜๋Š” ์Šค๋ ˆ๋“œ๊ฐ€ ๋Œ€๋Ÿ‰์œผ๋กœ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.


๐Ÿ“‹ Before: ๋น„๊ด€์  ๋ฝ ๋ฐฉ์‹ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ (ํด๋ฆญํ•˜์—ฌ ํŽผ์น˜๊ธฐ)

๊ฐœ์„  ์ „ ์„ฑ๋Šฅ ๊ทธ๋ž˜ํ”„ ๊ฐœ์„  ์ „ ์„ฑ๋Šฅ ๊ทธ๋ž˜ํ”„ ๊ฐœ์„  ์ „ ์„ฑ๋Šฅ ๊ทธ๋ž˜ํ”„

3. ํ•ด๊ฒฐ ๋ฐฉ์•ˆ ์„ค๊ณ„: Redis์™€ RabbitMQ๋ฅผ ํ™œ์šฉํ•œ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ

๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด, DB์˜ ๋ถ€๋‹ด์„ ๋œ์–ด์ฃผ๊ณ  ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ–ฅ์ƒ์‹œํ‚ค๋Š”ย ๋น„๋™๊ธฐ(Asynchronous) ์•„ํ‚คํ…์ฒ˜๋กœ ์ „ํ™˜์„ ๊ฒฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ’กย ํ•ต์‹ฌ ์•„์ด๋””์–ด

"์„ ์ฐฉ์ˆœ ์ค„ ์„ธ์šฐ๊ธฐ๋ฅผ ๋А๋ฆฐ DB๊ฐ€ ์•„๋‹Œ, ๋งค์šฐ ๋น ๋ฅธ ๋ฉ”๋ชจ๋ฆฌ(Redis)์—์„œ ์ฒ˜๋ฆฌํ•˜๊ณ , ์‹ค์ œ DB ์ž‘์—…์€ ๋ฉ”์‹œ์ง€ ํ(RabbitMQ)๋ฅผ ํ†ตํ•ด ๋‚˜์ค‘์— ์•ˆ์ •์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜์ž!"

๊ฐœ์„ ๋œ ์•„ํ‚คํ…์ฒ˜ ํ๋ฆ„:

  1. [1์ฐจ ๋ฐฉ์–ด์„ : Redis]ย API ์„œ๋ฒ„๋Š” ์š”์ฒญ์„ ๋ฐ›์œผ๋ฉด DB ๋Œ€์‹  Redis์— ๋จผ์ € ์ ‘๊ทผํ•ฉ๋‹ˆ๋‹ค.
    • ์ค‘๋ณต ๋ฐœ๊ธ‰ ์ฒดํฌ:ย Redis Set ์ž๋ฃŒ๊ตฌ์กฐ(SADD)๋ฅผ ์ด์šฉํ•ด ์‚ฌ์šฉ์ž๊ฐ€ ์ด๋ฏธ ์ฐธ์—ฌํ–ˆ๋Š”์ง€ ์›์ž์ ์œผ๋กœ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
    • ์ˆ˜๋Ÿ‰ ์ฒดํฌ:ย Redis์˜ย INCRย ๋ช…๋ น์–ด๋ฅผ ์ด์šฉํ•ด ํ˜„์žฌ ๋ฐœ๊ธ‰ ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ์›์ž์ ์œผ๋กœ ์นด์šดํŠธํ•˜๊ณ , ์ด ์ˆ˜๋Ÿ‰์„ ๋„˜์—ˆ๋Š”์ง€ ์ฆ‰์‹œ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค.
  2. [๋น ๋ฅธ ์‹คํŒจ/์„ฑ๊ณต ์‘๋‹ต]ย Redis ์ฒดํฌ๋ฅผ ํ†ต๊ณผํ•˜์ง€ ๋ชปํ•œ ์š”์ฒญ์€ DB์— ๋„๋‹ฌํ•˜๊ธฐ๋„ ์ „์— "์†Œ์ง„๋˜์—ˆ์Šต๋‹ˆ๋‹ค" ๋˜๋Š” "์ด๋ฏธ ๋ฐ›์œผ์…จ์Šต๋‹ˆ๋‹ค" ๋ผ๋Š” ์‹คํŒจ ์‘๋‹ต์„ ์ฆ‰์‹œ ๋ฐ›์Šต๋‹ˆ๋‹ค. Redis ์ฒดํฌ๋ฅผ ํ†ต๊ณผํ•œ ์š”์ฒญ์€ **'์„ฑ๊ณต ๋Œ€์ƒ'**์œผ๋กœ ๊ฐ„์ฃผ๋ฉ๋‹ˆ๋‹ค.
  3. [๋ถ€ํ•˜ ์ œ์–ด: RabbitMQ]ย '์„ฑ๊ณต ๋Œ€์ƒ'์ด ๋œ ์š”์ฒญ ์ •๋ณด๋งŒ ๋ฉ”์‹œ์ง€ ํ(RabbitMQ)์— ๋ฉ”์‹œ์ง€๋กœ ์ „์†กํ•˜๊ณ , API ์„œ๋ฒ„๋Š” ์‚ฌ์šฉ์ž์—๊ฒŒ "์ฟ ํฐ์ด ๋ฐœ๊ธ‰๋˜์—ˆ์Šต๋‹ˆ๋‹ค" ๋ผ๋Š” ์„ฑ๊ณต ์‘๋‹ต์„ ์ฆ‰์‹œ ๋ณด๋ƒ…๋‹ˆ๋‹ค.
  4. [์•ˆ์ •์ ์ธ ํ›„์ฒ˜๋ฆฌ: Consumer]ย ๋ณ„๋„์˜ ํ”„๋กœ์„ธ์Šค๋กœ ๋™์ž‘ํ•˜๋Š” RabbitMQ Consumer๋Š” ํ์— ์Œ“์ธ ๋ฉ”์‹œ์ง€๋ฅผ ์ž์‹ ์˜ ์ฒ˜๋ฆฌ ์†๋„์— ๋งž์ถฐ ์ˆœ์ฐจ์ ์œผ๋กœ ๊ฐ€์ ธ์™€์„œ, ์‹ค์ œ DB์— ์ฟ ํฐ ๋ฐ์ดํ„ฐ๋ฅผย INSERTํ•˜๊ณ  ์ˆ˜๋Ÿ‰์„ UPDATE ํ•˜๋Š” ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

4. ๊ฐœ์„  ๊ฒฐ๊ณผ ๊ฒ€์ฆ: ๋‹ค์‹œ nGrinder๋กœ

์ƒˆ๋กœ์šด ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ตฌํ˜„ํ•œ ๋’ค,ย ์™„์ „ํžˆ ๋™์ผํ•œ ์กฐ๊ฑด์œผ๋กœ nGrinder ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ๋ฅผ ๋‹ค์‹œ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ๋†’์•„์ง„ TPS ๋ฐ ์‘๋‹ต ์‹œ๊ฐ„ ๊ฐœ์„ :ย API ์„œ๋ฒ„๋Š” ๋” ์ด์ƒ DB๋ฅผ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š์œผ๋ฏ€๋กœ, ์ด์ „๊ณผ๋Š” ๋น„๊ตํ•˜์—ฌ TPS๋Š” ์•ฝ 8๋ฐฐ์ •๋„ ์ƒ์Šนํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์‘๋‹ต์‹œ๊ฐ„ ๋˜ํ•œ ๊ธฐ์กด๋Œ€๋น„ 80% ๊ฐ์†Œํ•˜์˜€์Šต๋‹ˆ๋‹ค.


๐Ÿ“Š After: Redis+MQ ๋ฐฉ์‹ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ (ํด๋ฆญํ•˜์—ฌ ํŽผ์น˜๊ธฐ)

๊ฐœ์„  ์ „ ์„ฑ๋Šฅ ๊ทธ๋ž˜ํ”„

5. ์—ฌ์ „ํ•œ ๋ฌธ์ œ:

์•„ํ‚คํ…์ฒ˜๋ฅผ ๋ณ€๊ฒฝํ–ˆ์Œ์—๋„ ์—ฌ์ „ํžˆ ๋‚จ์•„์žˆ๋Š” ๋ฌธ์ œ์ ๋“ค์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

  • DB CPU ์‚ฌ์šฉ๋ฅ  :ย DB์˜ ๋ถ€ํ•˜๋Š” ์—ฌ์ „ํžˆ ๋†’์•˜์Šต๋‹ˆ๋‹ค.
  • ์•„์ง ๋ถ€์กฑํ•œ TPS: TPS๋Š” ์ƒ์Šนํ•˜์˜€์ง€๋งŒ ๊ทธ๋Ÿผ์—๋„ ๋ถ€์กฑํ•˜๋‹ค๊ณ  ๋А๊ปด์กŒ์Šต๋‹ˆ๋‹ค.
  • RABBITMQ ๋ถ€ํ•˜: RABBITMQ ๋ฅผ ๋„์ž…ํ•˜์—ฌ CPU์‚ฌ์šฉ๋ฅ ์„ ๊ด€์ฐฐํ–ˆ์„ ๋•Œ, 90-100%์ด์ƒ์˜ CPU์‚ฌ์šฉ์ด ์ฟ ํฐ์ด ๋ฐœ๊ธ‰๋˜๋Š” ๋™์•ˆ ์ง€์†์ ์œผ๋กœ ๋†’๊ฒŒ ๋‚˜ํƒ€๋‚˜๋Š” ๋ชจ์Šต์„ ๋ณด์˜€์Šต๋‹ˆ๋‹ค.

6. ์ตœ์ข… ๊ฒฐ๋ก 

๋น„๊ด€์  ๋ฝ์„ ์‚ฌ์šฉํ•œ ์ดˆ๊ธฐ ๋™๊ธฐ ๋ฐฉ์‹์€ ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ์„ ๋ณด์žฅํ•˜๋Š” ๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ ๋ฐฉ๋ฒ•์ด์—ˆ์ง€๋งŒ, ๋Œ€๊ทœ๋ชจ ๋™์‹œ ์š”์ฒญ ํ™˜๊ฒฝ์—์„œ๋Š” ์‹œ์Šคํ…œ ์ „์ฒด๋ฅผ ๋งˆ๋น„์‹œํ‚ค๋Š” ๋ณ‘๋ชฉ ์ง€์ ์ด ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

Redis๋ฅผ ์ด์šฉํ•ด ์„ ์ฐฉ์ˆœ ๋กœ์ง์„ ๋ฉ”๋ชจ๋ฆฌ๋‹จ์—์„œ ์ฒ˜๋ฆฌํ•˜๊ณ ,ย RabbitMQ๋ฅผ ์ด์šฉํ•ด DB ์ž‘์—…์„ ๋น„๋™๊ธฐํ™”ํ•จ์œผ๋กœ์จ, ๋” ์ข‹์€ ๊ฒฐ๊ณผ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

  1. ์‚ฌ์šฉ์ž ๊ฒฝํ—˜:ย ์‚ฌ์šฉ์ž๋Š” DB ์ƒํƒœ์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ ๊ฑฐ์˜ ์ฆ‰์‹œ ์„ฑ๊ณต/์‹คํŒจ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›์•„๋ด…๋‹ˆ๋‹ค.

3๏ธโƒฃ [์„ฑ๋Šฅ ๊ฐœ์„ ] ๋ณด์ด์ง€ ์•Š๋Š” ๋ณ‘๋ชฉ, API ์ธ์ฆ ์ธํ„ฐ์…‰ํ„ฐ ์ตœ์ ํ™”๊ธฐ

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์„ฑ๋Šฅ์„ ์ด์•ผ๊ธฐํ•  ๋•Œ, ์šฐ๋ฆฌ๋Š” ํ”ํžˆ ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด๋‚˜ ๋ฌด๊ฑฐ์šด ์ฟผ๋ฆฌ์— ์ง‘์ค‘ํ•˜๊ณค ํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋•Œ๋กœ๋Š” ๊ฐ€์žฅ ๋‹จ์ˆœํ•˜๊ณ  ๋ฐ˜๋ณต์ ์ธ ์ž‘์—…์ด ์‹œ์Šคํ…œ ์ „์ฒด์˜ ๋ฐœ๋ชฉ์„ ์žก๋Š” '์ˆจ๊ฒจ์ง„ ์•”์‚ด์ž'๊ฐ€ ๋˜๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค.

์ด๋ฒˆ์—๋Š” nGrinder์™€ ๋ชจ๋‹ˆํ„ฐ๋ง ํˆด์„ ํ†ตํ•ด, ๋ชจ๋“  API์˜ ๊ด€๋ฌธ ์—ญํ• ์„ ํ•˜๋˜ย ์ธ์ฆ ์ธํ„ฐ์…‰ํ„ฐ(Interceptor)์˜ ์„ฑ๋Šฅ ๋ณ‘๋ชฉ์„ ๋ฐœ๊ฒฌํ•˜๊ณ ย Redis ์บ์‹ฑ์œผ๋กœ ํ•ด๊ฒฐํ•œ ๊ณผ์ •์„ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค.

1. ๋ฌธ์ œ์˜ ๋ฐœ๋‹จ: "๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์€ ๊ฐ€๋ฒผ์šด๋ฐ, ์™œ DB๋Š” ํž˜๋“ค์–ดํ• ๊นŒ?"

์„ ์ฐฉ์ˆœ ์ฟ ํฐ ๋ฐœ๊ธ‰ ๊ธฐ๋Šฅ์„ ๋น„๋™๊ธฐ ์•„ํ‚คํ…์ฒ˜๋กœ ๊ฐœ์„ ํ•œ ํ›„, ์‹œ์Šคํ…œ์˜ ์ „๋ฐ˜์ ์ธ ์ฒ˜๋ฆฌ๋Ÿ‰(TPS)์€ ํฌ๊ฒŒ ํ–ฅ์ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์—ฌ์ „ํžˆ ์„์—ฐ์น˜ ์•Š์€ ๋ถ€๋ถ„์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฐ”๋กœย DB์˜ CPU ์‚ฌ์šฉ๋Ÿ‰์ด์—ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿค”ย ์˜๋ฌธ์ 

์ฟ ํฐ ๋ฐœ๊ธ‰ ์š”์ฒญ์€ ์ด์ œ Redis์—์„œ ๋Œ€๋ถ€๋ถ„ ์ฒ˜๋ฆฌ๋˜๊ณ  DB๋กœ ๊ฐ€์ง€ ์•Š๋Š”๋ฐ๋„, nGrinder๋กœ ๋†’์€ ๋ถ€ํ•˜๋ฅผ ๊ฐ€ํ•˜๋ฉด ์—ฌ์ „ํžˆ DB์˜ CPU ์‚ฌ์šฉ๋ฅ ์ด ์˜ˆ์ƒ๋ณด๋‹ค ๋†’๊ฒŒ ๋‚˜ํƒ€๋‚ฌ์Šต๋‹ˆ๋‹ค.ย INSERTย ์ž‘์—…์ด ์—†๋Š” ๋‹จ์ˆœ ์กฐํšŒ API์— ๋ถ€ํ•˜๋ฅผ ๊ฐ€ํ•ด๋„ ๋น„์Šทํ•œ ํ˜„์ƒ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์™ธ์—,ย ๋ชจ๋“  API ์š”์ฒญ์ด ๊ณตํ†ต์ ์œผ๋กœ ๊ฑฐ์น˜๋Š” ๊ตฌ๊ฐ„ย ์–ด๋”˜๊ฐ€์— DB ๋ถ€ํ•˜๋ฅผ ์œ ๋ฐœํ•˜๋Š” ์›์ธ์ด ์ˆจ์–ด์žˆ๋Š” ๊ฒƒ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

2. ์›์ธ ์ถ”์ : ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ์ฝ”๋“œ ๋ถ„์„

๋ฒ”์ธ์„ ์ฐพ๊ธฐ ์œ„ํ•ด ๋ช‡ ๊ฐ€์ง€ ๋„๊ตฌ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์‹œ์Šคํ…œ์„ ๋ถ„์„ํ–ˆ์Šต๋‹ˆ๋‹ค.

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์— ์ ‘๊ทผํ•˜๋Š” ๊ฒƒ์€ ๋งค์šฐ ๋น„ํšจ์œจ์ ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

3. ํ•ด๊ฒฐ ๋ฐฉ์•ˆ ์„ค๊ณ„: Redis๋ฅผ ์ด์šฉํ•œ ์ธ์ฆ ์ •๋ณด ์บ์‹ฑ

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•œ ๊ฐ€์žฅ ์ด์ƒ์ ์ธ ๋ฐฉ๋ฒ•์€, ์ž์ฃผ ์กฐํšŒ๋˜์ง€๋งŒ ๊ฑฐ์˜ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š๋Š” ํŒŒํŠธ๋„ˆ ์ธ์ฆ ์ •๋ณด๋ฅผย  Redis์— ์บ์‹ฑํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๊ฐœ์„ ๋œ ์ธ์ฆ ํ๋ฆ„:

  1. [Cache-Aside ํŒจํ„ด]ย ์ธํ„ฐ์…‰ํ„ฐ๋Š” ์š”์ฒญ์„ ๋ฐ›์œผ๋ฉด, ๋จผ์ €ย Redis์— API Key๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
  2. (Cache Hit)ย Redis์— Key๊ฐ€ ์กด์žฌํ•˜๋ฉด, DB๋ฅผ ์กฐํšŒํ•˜์ง€ ์•Š๊ณ  ์ฆ‰์‹œ ์บ์‹œ๋œ ํŒŒํŠธ๋„ˆ ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ธ์ฆ์„ ํ†ต๊ณผ์‹œํ‚ต๋‹ˆ๋‹ค. (๋Œ€๋ถ€๋ถ„์˜ ์š”์ฒญ์ด ์ด ๊ฒฝ๋กœ๋ฅผ ๋”ฐ๋ฆ„)
  3. (Cache Miss)ย Redis์— Key๊ฐ€ ์—†์œผ๋ฉด,ย ๊ทธ๋•Œ์„œ์•ผ DB์— ์ ‘๊ทผํ•˜์—ฌ ํŒŒํŠธ๋„ˆ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
  4. DB์—์„œ ์กฐํšŒํ•œ ์ •๋ณด๋Š” ๋‹ค์Œ์— ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก Redis์— ์ €์žฅํ•œ ๋’ค, ์ธ์ฆ์„ ํ†ต๊ณผ์‹œํ‚ต๋‹ˆ๋‹ค.

4. ๊ฐœ์„  ๊ฒฐ๊ณผ ๊ฒ€์ฆ:

์ธ์ฆ ๋กœ์ง์— 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)


๐Ÿ“Š Before/After ๋น„๊ต ๋ฐ์ดํ„ฐ (ํด๋ฆญํ•˜์—ฌ ํŽผ์น˜๊ธฐ)

๊ฐœ์„  ์ „ (Before) before1 before2
๊ฐœ์„  ํ›„ (After) after1 after2

5. ์ตœ์ข… ๊ฒฐ๋ก 

์ด๋ฒˆ ์„ฑ๋Šฅ ๊ฐœ์„ ์„ ํ†ตํ•ด, ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๋ฟ๋งŒ ์•„๋‹ˆ๋ผย ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๊ณตํ†ต ๋กœ์ง(์ธ์ฆ, ๋กœ๊น… ๋“ฑ) ๋˜ํ•œ ์„ฑ๋Šฅ์— ๋ฏธ์น˜๋Š” ์˜ํ–ฅ์ด ๋งค์šฐ ํฌ๋‹ค๋Š” ๊ฒƒ์„ ๋‹ค์‹œ ํ•œ๋ฒˆ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.

๋‹จ์ˆœํžˆ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์„ ๋„˜์–ด, ๋ชจ๋‹ˆํ„ฐ๋ง์„ ํ†ตํ•ด ์‹œ์Šคํ…œ์˜ ๋ณ‘๋ชฉ ์ง€์ ์„ ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฐพ์•„๋‚ด๊ณ , ์บ์‹ฑ๊ณผ ๊ฐ™์€ ์ ์ ˆํ•œ ๊ธฐ์ˆ ์„ ์ ์šฉํ•˜์—ฌ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๊ฒƒ์ด ์•ˆ์ •์ ์ธ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž์˜ ํ•ต์‹ฌ ์—ญ๋Ÿ‰์ž„์„ ๊นจ๋‹ฌ์•˜์Šต๋‹ˆ๋‹ค.

4๏ธโƒฃ [์„ฑ๋Šฅ ๊ฐœ์„ ] ์ฟ ํฐ ๋ฐœ๊ธ‰ Consumer ์„ฑ๋Šฅ ์ตœ์ ํ™”: JPA saveAll์—์„œ JdbcTemplate์œผ๋กœ์˜ ์ „ํ™˜

1. ๋ฌธ์ œ ๋ฐœ๊ฒฌ: ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ์˜ ์ƒˆ๋กœ์šด ๋ณ‘๋ชฉ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค

์„ ์ฐฉ์ˆœ ์ฟ ํฐ ๋ฐœ๊ธ‰ ์‹œ์Šคํ…œ์˜ API ์‘๋‹ต ์†๋„๋ฅผ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•ด RabbitMQ๋ฅผ ๋„์ž…ํ•˜์—ฌ DB ์ €์žฅ์„ ๋น„๋™๊ธฐ ๋ฐฉ์‹์œผ๋กœ ์ „ํ™˜ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋กœ์จ ์‚ฌ์šฉ์ž ์š”์ฒญ์€ ๋นจ๋ผ์กŒ์ง€๋งŒ, ์ด์ œ๋Š” ์ดˆ๋‹น ์ˆ˜์ฒœ ๊ฐœ์˜ ๋ฉ”์‹œ์ง€๋ฅผ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๋Š” Consumer์˜ DB ์“ฐ๊ธฐ ์ž‘์—…์ด ์ƒˆ๋กœ์šด ๋ณ‘๋ชฉ ์ง€์ ์œผ๋กœ ๋– ์˜ฌ๋ž์Šต๋‹ˆ๋‹ค.

RabbitMQ Consumer๊ฐ€ ๋Œ€๋Ÿ‰์˜ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐฐ์น˜(Batch)๋กœ ์ฒ˜๋ฆฌํ•  ๋•Œ, ์˜ˆ์ƒ๋ณด๋‹ค DB INSERT ์„ฑ๋Šฅ์ด ๋‚˜์˜ค์ง€ ์•Š๋Š” ๊ฒƒ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค. repository.saveAll()๋กœ ์ž‘์„ฑ๋œ ๋ฐฐ์น˜ ์ „๋žต์ด ์˜ˆ์ƒ๊ณผ๋Š” ๋‹ฌ๋ฆฌ ๊ฐœ๋ณ„ INSERT ๋กœ ๋™์ž‘ํ•˜์˜€๊ณ , ์ด๋Š” JPA์˜ ์“ฐ๊ธฐ ์ง€์—ฐ ๋ฐ ๋ฐฐ์น˜ INSERT ์ตœ์ ํ™”๋ฅผ ๋ฐฉํ•ดํ•˜๋Š” ์ฃผ๋œ ์›์ธ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

๋‹จ์ˆœํžˆ for ๋ฃจํ”„ ์•ˆ์—์„œ repository.save()๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ์‹์€, ์ฒ˜๋ฆฌํ•ด์•ผ ํ•  ๋ฉ”์‹œ์ง€ ์ˆ˜๋งŒํผ DB์™€์˜ ๋„คํŠธ์›Œํฌ ํ†ต์‹ (Round Trip)๊ณผ ํŠธ๋žœ์žญ์…˜์„ ๋ฐœ์ƒ์‹œ์ผœ, ๋Œ€๊ทœ๋ชจ ํŠธ๋ž˜ํ”ฝ ์ƒํ™ฉ์—์„œ DB์— ๊ทน์‹ฌํ•œ ๋ถ€ํ•˜๋ฅผ ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

2. 1์ฐจ ํ•ด๊ฒฐ์ฑ…: JPA Batch Insert (saveAll + SEQUENCE)

๊ฐ€์žฅ ๋จผ์ € JPA๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๋ฐฐ์น˜(Batch) ์ฒ˜๋ฆฌ ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜์—ฌ DB์™€์˜ ํ†ต์‹  ํšŸ์ˆ˜๋ฅผ ์ค„์ด๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ฐ€. IDENTITY ์ „๋žต์˜ ํ•œ๊ณ„ ๋ฐœ๊ฒฌ

MySQL ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉํ•˜๋Š” GenerationType.IDENTITY(AUTO_INCREMENT) ์ „๋žต์€, INSERT ์ฟผ๋ฆฌ๊ฐ€ DB์—์„œ ์‹คํ–‰๋œ ํ›„์—์•ผ ID๋ฅผ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋•Œ๋ฌธ์— JPA๋Š” ์—ฌ๋Ÿฌ INSERT๋ฅผ ํ•˜๋‚˜์˜ ๋ฐฐ์น˜๋กœ ๋ฌถ์–ด ๋ณด๋‚ด์ง€ ๋ชปํ•˜๊ณ , ๊ฒฐ๊ตญ saveAll()์„ ์‚ฌ์šฉํ•˜๋”๋ผ๋„ ๋‚ด๋ถ€์ ์œผ๋กœ๋Š” ๊ฐœ๋ณ„ INSERT๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๊ฒƒ๊ณผ ๊ฐ™์€ ๋น„ํšจ์œจ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

๋‚˜. SEQUENCE ์ „๋žต์œผ๋กœ ์ „ํ™˜, ํ•˜์ง€๋งŒ..?

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด, INSERT ์ „์— ID๋ฅผ ๋ฏธ๋ฆฌ ํ• ๋‹น๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” GenerationType.SEQUENCE ์ „๋žต์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ MYSQL์€ SEQUENCE๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š์•˜๊ณ , Hibernate๊ฐ€ hibernate_sequence ํ…Œ์ด๋ธ”์„ ํ†ตํ•ด ์ด๋ฅผ ํ‰๋‚ด ๋‚ด๋Š” ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ถ”๊ฐ€์ ์ธ ํ…Œ์ด๋ธ”๊ณผ ๊ด€๋ฆฌ๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋Š” ์ ๊ณผ JPA์˜ saveAll์€ ๋งค์šฐ ํšจ์œจ์ ์ด์ง€๋งŒ, ๋‚ด๋ถ€์ ์œผ๋กœ๋Š” ์—ฌ์ „ํžˆ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ(Persistence Context) ๊ด€๋ฆฌ, Dirty Checking ๋“ฑ JPA ๊ณ„์ธต์˜ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ์กด์žฌํ•œ๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์‹ค์‹œ๊ฐ„์œผ๋กœ ์ˆ˜๋งŒ ๊ฑด์˜ ๋ฉ”์‹œ์ง€๋ฅผ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๋Š” Consumer ๋กœ์ง์—์„œ๋Š” ์ด ๋ฏธ์„ธํ•œ ์˜ค๋ฒ„ํ—ค๋“œ๋งˆ์ € ์ œ๊ฑฐํ•˜์—ฌ, ๊ฐ€์žฅ ์ˆœ์ˆ˜ํ•œ JDBC ๋ ˆ๋ฒจ์˜ ์ตœ๊ณ  ์„ฑ๋Šฅ์„ ํ™•๋ณดํ•  ํ•„์š”๊ฐ€ ์žˆ๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.


4. ์ตœ์ข… ํ•ด๊ฒฐ์ฑ…: JdbcTemplate.batchUpdate() ๋„์ž…

JPA/Hibernate ๊ณ„์ธต์„ ์™„์ „ํžˆ ์šฐํšŒํ•˜์—ฌ, JDBC ๋“œ๋ผ์ด๋ฒ„๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๊ฐ€์žฅ ์ตœ์ ํ™”๋œ ๋ฐฉ์‹์œผ๋กœ ๋ฐฐ์น˜ INSERT๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•ด JdbcTemplate์„ ๋„์ž…ํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ฐ€. JdbcTemplate์„ ์‚ฌ์šฉํ•œ ๋ฐฐ์น˜ INSERT ๊ตฌํ˜„

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());
                // ...
            });
}

๋‚˜. ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ: OutOfMemoryError์™€ ์ฒญํ‚น(Chunking)

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, ...);
}

5. ์ตœ์ข… ์„ฑ๋Šฅ ๋น„๊ต ๋ฐ ๊ฒฐ๋ก 

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)

๊ฐœ์„  ํ›„ ์„ฑ๋Šฅ ๊ทธ๋ž˜ํ”„

๐Ÿ‘‘ ๋ฐฐ์šด์ 

ํ˜„์žฌ์˜ ๋ชจ์Šต์ด ์™„๋ฒฝํ•œ ์„ค๊ณ„๋ผ๊ณ ๋Š” ์ƒ๊ฐํ•˜์ง€ ์•Š๊ณ  ๋” ๋ฐœ์ „์‹œํ‚ฌ ๋ถ€๋ถ„์ด ๋งŽ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿผ์—๋„ ์„ ์ฐฉ์ˆœ ์ฟ ํฐ ๋ฐœ๊ธ‰์˜ ๊ฒฝ์šฐ ์ฒ˜์Œ์˜ ์„ค๊ณ„์™€ ๋น„๊ตํ–ˆ์„ ๋•Œ, ์œ ์˜๋ฏธํ•œ ์„ฑ๋Šฅ ํ–ฅ์ƒ์„ ์ด๋ค„๋‚ผ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

  1. ์„ฑ๋Šฅ ๊ฐœ์„ ์€ ๋‹จ์ˆœํžˆ ์ถ”์ธก์œผ๋กœ๋งŒ ํ•˜๋ฉด ์•ˆ๋œ๋‹ค -> ์ด์ „ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋‹จ์ˆœํžˆ ์ด๋ก ์ ์ธ ์ถ”์ธก์œผ๋กœ๋งŒ ์„ฑ๋Šฅ์„ ๊ณ ๋ คํ–ˆ์ง€๋งŒ ์‹ค์ œ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ–ˆ์„ ๋•Œ, ์ด๋ก ๊ณผ ๋‹ค๋ฅธ ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•˜์Šต๋‹ˆ๋‹ค. ์ง์ ‘ ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด๋ด์•ผ์ง€๋งŒ ๋ฌธ์ œ์ ์ด ๋ฌด์—‡์ธ์ง€ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  2. ๊ธฐ์ˆ ์€ ํŠธ๋ ˆ์ด๋“œ ์˜คํ”„ ๊ด€๊ณ„๊ฐ€ ๋งŽ๋‹ค -> ๊ธฐ์ˆ ์„ ์ ์šฉํ•  ๋•Œ, ํŠน์ • ๊ธฐ์ˆ ์ด ์–ธ์ œ๋‚˜ ๋ฌด์กฐ๊ฑด ์ข‹์€ ๊ฒฝ์šฐ๋Š” ๊ฑฐ์˜ ์—†๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ๊ฐ ๊ธฐ์ˆ ๋งˆ๋‹ค ์žฅ๋‹จ์ ์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ณธ์ธ์˜ ์ƒํ™ฉ์— ๋งž๊ฒŒ ์ž˜ ์ ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages