멋사 Backend Plus 34일차 🦁

μ‹ μž¬μ›Β·2023λ…„ 12μ›” 12일

μ˜€λŠ˜λ„ μ˜€μ „ μ˜€ν›„μ‹œκ°„ λ™μ•ˆ ν”„λ‘œμ νŠΈλ₯Ό μ§„ν–‰ν•˜μ˜€κ³ , μƒν’ˆ μž¬κ³ μ— λŒ€ν•œ 비관적 락을 μ μš©ν•˜μ˜€λ‹€.

μ˜€μ „

Index λž€

Index λž€ λ³„λ„μ˜ 좔가적인 μž‘μ—…μ„ ν†΅ν•΄μ„œ ν…Œμ΄λΈ”μ˜ 쑰회 속도λ₯Ό ν–₯상 μ‹œμΌœμ£ΌλŠ” 역할을 ν•©λ‹ˆλ‹€. μ±…μ΄λ‚˜ μž‘μ§€λ₯Ό λ³Όλ•Œ μ±…κ°ˆν”Ό 역할을 Index κ°€ ν•˜λŠ” 것 μž…λ‹ˆλ‹€.

Index μ‚¬μš©μ‹œ 주의점

쑰회 μ„±λŠ₯의 ν–₯상을 κΈ°λŒ€ν•˜κ³ , 무쑰건 Index λ₯Ό μ‚¬μš©ν•˜λŠ” 방법은 μ˜³μ§€ μ•Šμ€ 방법 이라고 ν•©λ‹ˆλ‹€.

쑰회 과정을 μˆ˜ν–‰ν•˜μ§€ μ•ŠλŠ” : Insert 쿼리

쑰회 과정을 μˆ˜ν–‰ν•˜μ§€ μ•ŠλŠ” Insert의 κ²½μš°μ—λŠ” 좔가적인 연산이 ν•„μš” ν•˜λ―€λ‘œ μ„±λŠ₯ μ €ν•˜λ₯Ό μΌμœΌν‚¬μˆ˜ μžˆμŠ΅λ‹ˆλ‹€.

쑰회 과정을 μˆ˜ν–‰ν•˜λŠ” : Select, Delete, Update

Select, Delete, UpdateλŠ” 쑰회둜 μ‹œμž‘ν•˜λ©°, μ‘°νšŒμ— λŒ€ν•œ μ„±λŠ₯은 ν–₯상 λ©λ‹ˆλ‹€.

Indexλ₯Ό μ‚¬μš©ν•˜λ©΄ 쒋은 경우

  • μ™Έλž˜ ν‚€ κ΄€κ³„μ—μ„œλŠ” 자주 μ‚¬μš©λ˜λŠ” μ»¬λŸΌμ— 인덱슀λ₯Ό μƒμ„±ν•˜μ—¬ JOIN μ—°μ‚°μ˜ μ„±λŠ₯을 ν–₯μƒμ‹œν‚¬ 수 μžˆμŠ΅λ‹ˆλ‹€.

  • ORDER BYλ‚˜ GROUP BY μ ˆμ—μ„œ μ‚¬μš©λ˜λŠ” μ»¬λŸΌμ— 인덱슀λ₯Ό μΆ”κ°€ν•˜λ©΄ μ •λ ¬ 및 κ·Έλ£Ήν™” μž‘μ—…μ΄ 빨라질 수 μžˆμŠ΅λ‹ˆλ‹€.

Spring Entityμ—μ„œμ˜ 적용

@Entity
@Getter
@Table(name = "product_cart", indexes = {
        @Index(name = "idx_member_id", columnList = "member_id")
})
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Cart {

	@Id
    @GeneratedValue(strategy = IDENTITY)
    private Long id;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    @Column(name = "item_count_in_cart")
    private Integer totalCount;
}

Member μ—”ν‹°ν‹° ν΄λž˜μŠ€μ— λŒ€ν•œ μ™Έλž˜ν‚€λ₯Ό κ°€μ§€κ³  있으며 κ°„λ‹¨ν•œ Cart(카트) μ—”ν‹°ν‹° 클래슀 μž…λ‹ˆλ‹€.

@Table(indexes = { @Index(name = "인덱슀 이름", columnList = "데이터 베이슀의 μ‚½μž…λ  이름, μ—¬κΈ°μ„œλŠ” 컬럼 이름")})
➑ IndexλŠ” μ—¬λŸ¬κ°œ 생성이 κ°€λŠ₯ν•˜λ©°, μ—¬λŸ¬κ°œμ˜ μ»¬λŸΌμ„ μ‚¬μš©ν•΄μ„œ λ§Œλ“€ 수 μžˆμŠ΅λ‹ˆλ‹€.

μ˜€ν›„

  • 이전 λΈ”λ‘œκ·Έλ₯Ό μ°Έκ³ ν•˜κ²Œ 되면, μƒν’ˆμ˜ 재고λ₯Ό λ™μ‹œμ— λ‹΄μ•˜μ„λ•Œ λ™μ‹œμ„± 문제λ₯Ό 확인 ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

이런 λ™μ‹œμ„± λ¬Έμ œμ— λŒ€ν•΄ ν•΄κ²° ν• μˆ˜ μžˆλŠ”λ°©λ²•μ΄ μ—¬λŸ¬κ°€μ§€ μ΄μ§€λ§Œ, μ—¬λŸ¬ λΈ”λ‘œκ·Έλ₯Ό 찾아보며 μ €λŠ” 락 (Lock)을 적용 ν•΄μ•Όκ² λ‹€κ³  생각 ν•˜κ²Œ λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

이 κΈ€μ—μ„œλŠ” 락의 κ°œλ…, 비관적 락을 μ„ νƒν•œ 이유, 락 μ‚¬μš© 방법에 λŒ€ν•΄μ„œ 정리 ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

락 (Lock) 의 κ°œλ…

락은 λŒ€ν‘œμ μΈ λ™μ‹œμ„± μ œμ–΄ 기법 쀑 ν•˜λ‚˜λ‘œ, νŠΈλžœμž­μ…˜μ˜ 순차적 진행을 보μž₯ ν• μˆ˜ μžˆλŠ” 직렬화 μž₯치 μž…λ‹ˆλ‹€.

  • 일반적으둜 "λ™μ‹œμ—" 같은 데이터λ₯Ό μ ‘κ·Ό ν•˜λŠ” 것을 λ°©μ§€
  • λ™μ‹œμ— λ°œμƒν•˜λŠ” 즉 같은 μ‹œκ°„μ˜ λ™μ‹œ μš”μ²­μ„ ν•΄κ²°ν•  λ•Œ μ‚¬μš©

락 μ‚¬μš© 이유

κ°„λ‹¨ν•œ μ˜ˆμ‹œλ‘œ μƒν’ˆμ˜ 재고λ₯Ό μ˜ˆμ‹œλ‘œ λ“€κ² μŠ΅λ‹ˆλ‹€.

➑ 예λ₯Ό λ“€μ–΄, A μ‚¬μš©μžκ°€ μƒν’ˆμ˜ 재고λ₯Ό 5개둜 μ‘°νšŒν•˜κ³ , B μ‚¬μš©μžλ„ "λ™μ‹œμ—" 5개둜 μ‘°νšŒν•œ 후에 두 μ‚¬μš©μžκ°€ λ™μ‹œμ— 1μ”© κ°μ†Œμ‹œν‚€λ©΄,
μ‹€μ œλ‘œλŠ” -1개의 μž¬κ³ κ°€ 될 수 μžˆμŠ΅λ‹ˆλ‹€.
즉 ν•œκ°œμ˜ νŠΈλžœμž­μ…˜ μš”μ²­λ§Œ 반영이 λœλ‹€λŠ” 것 μž…λ‹ˆλ‹€.

➑ 좔가적인 μ˜ˆμ‹œλ‘œ A μ‚¬μš©μžκ°€ 5개의 재고λ₯Ό 4개둜 κ°μ†Œμ‹œν‚€κ³ , B μ‚¬μš©μžκ°€ λ™μ‹œμ— 5κ°œμ—μ„œ 3개둜 κ°μ†Œμ‹œν‚€λ©΄, B μ‚¬μš©μžμ˜ μ—…λ°μ΄νŠΈκ°€ 반영되고
A μ‚¬μš©μžμ˜ μ—…λ°μ΄νŠΈλŠ” 손싀될 수 μžˆμŠ΅λ‹ˆλ‹€.
즉 λ§ˆμ§€λ§‰μ— μ—…λ°μ΄νŠΈν•œ κ°’λ§Œ 반영되고 λ‚˜λ¨Έμ§€λŠ” 손싀될 수 μžˆμŠ΅λ‹ˆλ‹€.

λΉ„μŠ·ν•œ μƒν™©μœΌλ‘œ 또 μ–΄λ–€κ²Œ μžˆμ„κΉŒ ?

  • μˆ˜κ°•μ‹ μ²­ μ‹œμŠ€ν…œ (μ’Œμ„ μ‹œμŠ€ν…œ) μ—μ„œ 정원이 1λͺ…이 λ‚¨μ•˜μ„λ•Œ, 두λͺ…이 λ™μ‹œμ— μ‹ μ²­ν•΄μ„œ 정원이 1λͺ…뿐인 쑰건에 두λͺ…이 μ‹ μ²­ ν•˜κ²Œ λ˜μ—ˆμ„ λ•Œ

μœ„μ™€ 같은 λ°μ΄ν„°μ˜ 일관성과 무결성을 μ§€ν‚€κΈ° μœ„ν•΄ 락을 μ‚¬μš©ν•˜μ—¬ ν•΄λ‹Ή 문제의 λ™μ‹œμ„± μ œμ–΄ 문제λ₯Ό 사전에 λ°©μ§€ ν• μˆ˜ μžˆμŠ΅λ‹ˆλ‹€.

낙관적 락 비관적 락

낙관적 락

  • 낙관적, 즉 νŠΈλžœμž­μ…˜μ˜ μΆ©λŒμ€ μΌμ–΄λ‚˜μ§€ μ•Šμ„ 것을 κ°€μ • ν•˜λŠ” 방법 μž…λ‹ˆλ‹€.

  • JPA μ—μ„œλŠ” 일반적으둜 @Version μ–΄λ…Έν…Œμ΄μ…˜μ„ μ‚¬μš©ν•˜λ©°, λ°˜ν™˜ νƒ€μž…μœΌλ‘œλŠ” Long, Integer λ“±μœΌλ‘œ μ‚¬μš©ν•©λ‹ˆλ‹€.
  • μ½μ–΄μ˜¬λ•Œ Versionκ³Ό μˆ˜μ • ν›„ νŠΈλžœμž­μ…˜μ΄ 컀밋 (commit) λ˜λŠ” μ‹œμ μ— Version이 λ‹€λ₯΄λ©΄ 좩돌이 λ°œμƒν•˜κ²Œ λ©λ‹ˆλ‹€.
  • update 쿼리 λ°œμƒμ‹œ verison ν•„λ“œμ˜ 데이터 값이 1μ”© 증가 ν•©λ‹ˆλ‹€.

➑ μˆ˜μ •ν•  데이터λ₯Ό μ‘°νšŒμ‹œ, μžμ›μ„ μ„ μ ν•˜μ§€ 말고, 컀밋을 ν• λ•Œ λ™μ‹œμ„± λ¬Έμ œκ°€ λ°œμƒν•˜λ©΄ κ·Έλ•Œ 처리 ν•˜μžλŠ” 방식

비관적 락

  • 비관적, 즉 νŠΈλžœμž­μ…˜μ˜ μΆ©λŒμ€ μΌμ–΄λ‚œλ‹€κ³  κ°€μ •ν•˜λŠ” 방법 μž…λ‹ˆλ‹€.
  • νŠΈλžœμž­μ…˜μ˜ 좩돌이 무쑰건 μžˆλ‹€κ³  κ°€μ •ν•˜μ—¬, 데이터λ₯Ό 쑰회 ν• λ•Œ λΆ€ν„° 락을 κ±°λŠ” 방법 μž…λ‹ˆλ‹€.
  • SQL 쿼리문이 select for update 둜 λ°œμƒν•˜κ²Œ 되며,
    데이터λ₯Ό μˆ˜μ •ν•˜κΈ° μœ„ν•΄ 찾은 것 이닀.
  • 비관적 락을 μ‚¬μš©ν•˜κ²Œ 되면, νŠΈλžœμž­μ…˜μ΄ λŒ€κΈ°ν•˜λŠ” λ™μ•ˆ λ¬΄ν•œνžˆ κΈ°λ‹€λ¦΄μˆ˜ μ—†μœΌλ―€λ‘œ, νƒ€μž„μ•„μ›ƒ μ‹œκ°„μ„ κ±Έμ–΄ λ‘˜μˆ˜ μžˆμŠ΅λ‹ˆλ‹€.

➑ 데이터 μ‘°νšŒμ‹œλΆ€ν„° 락을 κ±Έμ–΄ λ™μ‹œμ„± λ¬Έμ œκ°€ λ°œμƒν•˜μ§€ λͺ»ν•˜κ²Œ 미리 처리 ν•˜μžλŠ” 방식


μƒν’ˆ μž¬κ³ μ²˜λ¦¬μ— λŒ€ν•΄μ„œ 비관적 락을 μ„ νƒν•œ 이유

μƒν’ˆ μž¬κ³ μ— λŒ€ν•œ 둜직이 λ‹€μ–‘ν•˜κ² μ§€λ§Œ, μƒν’ˆμ˜ μž¬κ³ κ°€ 0개 λ°‘μœΌλ‘œ λ–¨μ–΄μ§€κ²Œ λœλ‹€λ©΄ μ‹¬κ°ν•œ 문제이고 λ™μ‹œμ— μ—¬λŸ¬λͺ…μ˜ μ‚¬μš©μžμ˜ μš”μ²­
즉 μƒν’ˆμ˜ 재고λ₯Ό λ‹΄μ•˜μ„λ•Œ 낙관적인 락 인 경우 λ¨Όμ € μš”μ²­μ— μ„±κ³΅ν•œ μ‚¬λžŒμΈ 경우 μ΄μ™Έμ˜ λ‚˜λ¨Έμ§€μ˜ μš”μ²­λ“€μ€ 좩돌이 λ°œμƒν•˜λŠ”λ°, κ·Έ λͺ¨λ“  μΆ©λŒμ„ μ˜ˆμ™Έμ²˜λ¦¬μ™€ 둀백을 μ‹€ν–‰ ν•΄μ•Ό ν•©λ‹ˆλ‹€.
κ·Έ λΉ„μš©μ΄ 훨씬 더 λΉ„μŒ€ 것 κ°™λ‹€λŠ” 생각을 ν•˜μ˜€κ³ ,
그에 λ°˜ν•΄ 비관적 락 은 μˆ˜μ •λœ μ¦‰μ‹œμ— μž¬κ³ κ°€ 0개둜 보이게 됨으둜 둜직적으둜 0개이면 ꡬ맀λ₯Ό ν• μˆ˜ μ—†κ²Œ λ§‰κΈ°λ§Œ ν•œλ‹€λ©΄, λ‹€λ₯Έ 좔가적인 λΉ„μš©μ΄ λ“€ 것 κ°™μ§€ μ•Šμ•„ 비관적 락 을 μ‚¬μš© ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

락 μ‚¬μš© 방법

  • 낙관적 락 μ‚¬μš© 방법

μ—”ν‹°ν‹° 클래슀 ν•„λ“œμ— @Version μ–΄λ…Έν…Œμ΄μ…˜μ„ μ‚¬μš©ν•˜μ—¬ μ—”ν‹°ν‹°μ˜ "버전"을 κ΄€λ¦¬ν• μˆ˜ μžˆμŠ΅λ‹ˆλ‹€.

주의 μ‚¬ν•­μœΌλ‘œλŠ” 각 μ—”ν‹°ν‹° ν΄λž˜μŠ€μ—λŠ” ν•˜λ‚˜μ˜ 버전 μ†μ„±λ§Œ μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€.

κ·Έ λ‹€μŒμœΌλ‘œ μ„€μ •ν•  것은 @LockModeType 을 ν†΅ν•œ 락 μ˜΅μ…˜μ„ μ„€μ • ν•΄μ€„μˆ˜ μžˆμŠ΅λ‹ˆλ‹€.
(@LockModeType.PESSIMISTIC_WRITEλŠ” κ°„λ‹¨ν•˜κ²Œ 락을 κ±Έμ–΄ μ‘°νšŒλŠ” κ°€λŠ₯ν•œλ°, μˆ˜μ •ν•˜κ±°λ‚˜ μ‚­μ œν•˜λŠ” 것을 λ°©μ§€)

λ‹€μ–‘ν•œ 락 μ˜΅μ…˜μ΄ μ‘΄μž¬ν•˜λŠ”λ° ν•„μš”μ— 따라 μ •μ˜λ₯Ό 찾아보며 적용 ν•˜λ©΄ 쒋을 것 κ°™μŠ΅λ‹ˆλ‹€.

  • 비관적 락 μ‚¬μš© 방법

비관적 락은 낙관적 락과 달리 @Version μ–΄λ…Έν…Œμ΄μ…˜μ„ μ‚¬μš©ν•˜μ§€ μ•Šκ³ 
@Lock κ³Ό @LockModeType 을 μ‚¬μš©ν•©λ‹ˆλ‹€.

비관적 락 ν…ŒμŠ€νŠΈ

μ•„λž˜λŠ” μƒν’ˆμ„ μ €μž₯ν•˜κ³  μž₯λ°”κ΅¬λ‹ˆμ— λ‹΄λŠ” 행동인 removeStock λ©”μ†Œλ“œμ— λ™μ‹œ μš”μ²­μ— λŒ€ν•œ 비관적 락 ν…ŒμŠ€νŠΈ μž…λ‹ˆλ‹€.

@SpringBootTest
public class ItemCartPessimisticLockTest {
    @Autowired
    private ProductRepository productRepository;
    @Autowired
    private ItemCartServiceImpl itemCartService;

    private Product product;

    @BeforeEach
    void init() {
        product = Product.builder().productName("κ°•μ•„μ§€ μ‚¬λ£Œ")
                .price("25_000")
                .content("μœ ν†΅κΈ°ν•œ 1λ…„ 남은 μ‚¬λ£Œμž…λ‹ˆλ‹€.")
                .count(1000) // μƒν’ˆμ˜ 재고 1000개 μ €μž₯
                .category(Category.PET_FEED)
                .status("Y")
                .build();
        productRepository.save(product);
    }

    @Test
    public void λ™μ‹œμ—_1000개_μš”μ²­() throws InterruptedException {
        int threadCount = 1000; // 1000번의 μš”μ²­

		// 20개의 μ“°λ ˆλ“œν’€ 생성
        ExecutorService executorService = Executors.newFixedThreadPool(20);
        CountDownLatch latch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            executorService.submit(() -> {
                try {
                    itemCartService.removeStock(product.getId(), 1);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }
            });
        }
        latch.await();

        Product updatedProduct = productRepository.findById(product.getId()).orElse(null);
        Assertions.assertThat(updatedProduct).isNotNull();
        Assertions.assertThat(updatedProduct.getCount()).isEqualTo(0);
    }
}

μœ„μ˜ ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό μ‹€ν–‰ ν•˜κ²Œ λ˜λ©΄μ€ λ™μ‹œμ„± μš”μ²­μ—λ„ 순차적으둜 μƒν’ˆμ˜ 재고의 κ°―μˆ˜κ°€ 쀄어듀어 0개 둜 μˆ˜λ ΄ν•˜μ—¬ ν…ŒμŠ€νŠΈλ₯Ό 톡과 ν•˜μ˜€μŠ΅λ‹ˆλ‹€.


select for update 쿼리가 1000κ°œκ°€ λ°œμƒν•˜κ²Œ 되고, μ΄λŸ¬ν•œ μΏΌλ¦¬λŠ” @LockModeType.PESSIMISTIC_WRITE 을 μ˜΅μ…˜μœΌλ‘œ μ„€μ •ν•¨μœΌλ‘œμ¨ μ“°κΈ° μž κΈˆμ„ ν–ˆλ‹€λŠ” 것을 μ•Œμˆ˜ μžˆμŠ΅λ‹ˆλ‹€.

ν…ŒμŠ€νŠΈκ°€ 23초 κ°€λŸ‰ κ±Έλ¦° 것을 μ•Œ μˆ˜μžˆλŠ”λ°, λ‹€λ₯Έ νŒ€μ›λΆ„μ˜ λ§₯으둜 μ‹€ν–‰ν•˜μ˜€μ„λ•ŒλŠ” 3초 밖에 걸리지 μ•Šμ•˜λ‹€.

πŸ€” λ…ΈνŠΈλΆμ„ λ°”κΏ€λ•Œκ°€.....

0개의 λŒ“κΈ€