๐Ÿšš ์žฌ๊ณ  ๊ฐ์†Œ์— ๋”ฐ๋ฅธ ๋™์‹œ์„ฑ ๋ฌธ์ œ - ๋น„๊ด€์ ๋ฝ

๋””ํ•˜ยท2024๋…„ 9์›” 1์ผ
1

bucket๐Ÿ“ฆ

๋ชฉ๋ก ๋ณด๊ธฐ
10/10
post-thumbnail

e-commerce ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค๋ฉฐ ๋™์‹œ์— ๋‹ค์ˆ˜์˜ ์ฃผ๋ฌธ์ด ๋“ค์–ด์˜ฌ๋•Œ 100๊ฐœ์˜

1. ๐Ÿช ๋ฌธ์ œ์ 

  • ํ”„๋กœ์ ํŠธ์˜ ์ฃผ์š” ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜์ธ ์žฌ๊ณ  ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์—์„œ ๋™์‹œ์— ๋‹ค์ˆ˜์˜ ์ฃผ๋ฌธ์ด ๋“ค์–ด์˜ฌ ๋•Œ ์žฌ๊ณ ๊ฐ€ ์ •ํ™•ํžˆ ๊ฐ์†Œํ•˜์ง€ ์•Š๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒ
  • ์ดˆ๊ธฐ ํ…Œ์ŠคํŠธ์—์„œ 100๊ฐœ์˜ ์žฌ๊ณ ์— ๋Œ€ํ•ด 100๊ฑด์˜ ์ฃผ๋ฌธ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋™์•ˆ ์ผ๋ถ€ ์žฌ๊ณ ๊ฐ€ ๋‚จ๋Š” ํ˜„์ƒ์ด ๋ฐœ๊ฒฌ๋จ (์˜ˆ์ƒ ๋‚จ์€ ์žฌ๊ณ : 0, ์‹ค์ œ ๋‚จ์€ ์žฌ๊ณ : 2)

2. ํ•ด๊ฒฐ๊ณผ์ •

1) rock ์ฒ˜๋ฆฌ

์ฒ˜์Œ์œผ๋กœ ์ƒ๊ฐํ–ˆ๋˜ ๋ฐฉ๋ฒ•์€ ์•„๋ฌด๋ž˜๋„ Race Condition ๋ฌธ์ œ๋กœ ์žฌ๊ณ ๊ฐ€ ์ œ๋Œ€๋กœ ๊ฐ์†Œ ๋˜์ง€ ์•Š๋Š” ํ˜„์ƒ์ด๋ผ ์ƒ๊ฐ์„ ํ–ˆ๋‹ค

๐Ÿ›ธ Race Condition : ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž๊ฐ€ ๋™์‹œ์— ๊ฐ™์€ ์ƒํ’ˆ์„ ์ฃผ๋ฌธํ•˜๋Š” ๊ฒฝ์šฐ, ์žฌ๊ณ  ์ˆ˜๋Ÿ‰์„ ๊ฐ์†Œ์‹œํ‚ค๋Š” ์ž‘์—…์ด ๋™์‹œ์— ์ด๋ฃจ์–ด์งˆ ๋•Œ, ๋‘๊ฐœ์˜ ํŠธ๋žœ์žญ์…˜์ด ๋™์‹œ์— ๊ฐ™์€ ์žฌ๊ณ  ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผํ•˜์—ฌ ๊ฐฑ์‹ ์„ ์‹œ๋„ํ•˜์—ฌ ํ•œ ํŠธ๋ž™์ ์…˜์ด ๋‹ค๋ฅธ ํŠธ๋žœ์ ์…˜ ๊ฒฐ๊ณผ๋ฅผ ๋ฎ์–ด ์“ธ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๋‹ค
-> ์ผ๋ถ€ ์ฃผ๋ฌธ์ด ์ž˜๋ชป ์ฒ˜๋ฆฌ๋˜๊ฑฐ๋‚˜ ์žฌ๊ณ  ์ˆ˜๋Ÿ‰์ด ์ •ํ™•ํ•˜๊ฒŒ ๊ฐ์†Œํ•˜์ง€ ์•Š๋Š” ๋ ˆ์ด์Šค ์ปจ๋””์…˜ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค

๊ทธ๋ž˜์„œ ๋‹จ์ˆœํžˆ ๋‚™๊ด€์ ๋ฝ/ ๋น„๊ด€์ ๋ฝ ์ฒ˜๋Ÿผ ๋ฝ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๋ฉด ํ•ด๊ฒฐ์ด ๋˜๊ฒ ์ง€๋ผ๊ณ  ์ƒ๊ฐ์„ ํ–ˆ์—ˆ๋‹ค

ํ•˜์ง€๋งŒ, ๊ธฐ์กด ๋กœ์ง์— ๋ฝ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์คฌ๋Š”๋ฐ๋„ ์žฌ๊ณ ๊ฐ์†Œ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š๊ฒŒ ๋˜๋Š” ์˜ค๋ฅ˜๋Š” ์ง€์†์ ์œผ๋กœ ๋ฐœ์ƒ๋˜์—ˆ๋‹ค .. ๐Ÿคฏ

<๋น„๊ด€์ ๋ฝ ์ฒ˜๋ฆฌํ•œ ์ฝ”๋“œ>


@Transactional
    public CommonResponseDto<Object> createOrder(CustomUserDetails customUserDetails, OrderRequestDto orderRequestDto) {
        // ์œ ์ € ํ™•์ธ
        String email = customUserDetails.getEmail();
        Users user = userRepository.findByEmail(email)
                                   .orElseThrow(() -> new NotFoundException(ErrorCode.USER_NOT_FOUND));

        // ์ƒํ’ˆ ํ™•์ธ
        DessertItem dessertItem = dessertItemRepository.findById(orderRequestDto.getDessertId())
                                                       .orElseThrow(() -> new NotFoundException(ErrorCode.ITEM_NOT_FOUND));

  



        // ์žฌ๊ณ  ๊ฐ์†Œ
        stockLockService.decreaseStock(dessertItem.getStock.getId(), orderRequestDto.getCount(), dessertItem);



        OrderItem orderItem = OrderItem.builder()
                                       .orderPrice(dessertItem.getPrice())
                                       .count(orderRequestDto.getCount())
                                       .dessertItem(dessertItem)
                                       .build();


        Orders order = Orders.builder()
                             .users(user)
                             .orderStatus(OrderStatus.ORDER_COMPLETED) // ๊ธฐ๋ณธ๊ฐ’ ์„ค์ •
                             .orderDate(LocalDateTime.now())
                             .orderItems(Collections.singletonList(orderItem))
                             .deliveryAddress(orderRequestDto.getDeliveryAddress())
                             .build();

        orderItemRepository.save(orderItem);
        // ์ฃผ๋ฌธ ํ•ญ๋ชฉ์„ ์ฃผ๋ฌธ์— ์ถ”๊ฐ€
        order.addOrderItem(orderItem);
        orderRepository.save(order);


        return commonService.successResponse(SuccessCode.EXAMPLE_SUCCESS.getDescription(), HttpStatus.OK, null);
    }

    @Transactional(propagation = Propagation.REQUIRED) // ์ด ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ ์‹œ ์ƒ์œ„ ํŠธ๋žœ์žญ์…˜์„ ์ด์–ด ๋ฐ›์Œ
    public Stock decreaseStock(Long stockId, int amount, DessertItem dessertItem) {
        // ๋น„๊ด€์  ๋ฝ์„ ์‚ฌ์šฉํ•˜์—ฌ ์žฌ๊ณ ๋ฅผ ๊ฐ€์ ธ์˜ด
        Stock stock = stockRepository.findByStockIdWithPessimisticLock(stockId)
                                     .orElseThrow(() -> new NotFoundException(ErrorCode.OUT_OF_STOCK));

        stock.decreaseStock(amount, dessertItem);


        return stockRepository.saveAndFlush(stock);
    }

    public void decreaseStock(int amount , DessertItem dessertItem) {
        if (this.stockAmount < amount) {
            throw new OutOfStockException(ErrorCode.OUT_OF_STOCK);

        }
        this.stockAmount -= amount;
        this.sellAmount += amount;
        this.dessertItem = dessertItem;
        log.info("์žฌ๊ณ  ๊ฐ์†Œ: stockId={}, ๊ฐ์†Œ๋Ÿ‰={}, ๋‚จ์€ ์žฌ๊ณ ={}", id, amount, stockAmount);
    }

๋กœ์ง์„ ์‚ดํŽด๋ณด๋ฉด, ์ฃผ๋ฌธํ•œ ์œ ์ €๋ฅผ ํ™•์ธํ•˜๊ณ  ์ฃผ๋ฌธํ•œ ์ƒํ’ˆ์„ ํ™•์ธํ•˜๊ณ  ์žฌ๊ณ ๊ฐ์†Œ๊ฐ€ ๋˜๊ณ  ์ฃผ๋ฌธ์ฒ˜๋ฆฌ๊ฐ€ ๋˜๋Š” ๋กœ์ง์ด๋‹ค

Hibernate: 
    select
        di1_0.dessert_id,
        di1_0.contents,
        di1_0.created_at,
        di1_0.deleted_at,
        di1_0.dessert_name,
        di1_0.dessert_type,
        di1_0.price,
        di1_0.sale_status,
        di1_0.updated_at 
    from
        dessert_item di1_0 
    where
        di1_0.dessert_id=?
Hibernate: 
    select
        s1_0.stock_id,
        di1_0.dessert_id,
        di1_0.contents,
        di1_0.created_at,
        di1_0.deleted_at,
        di1_0.dessert_name,
        di1_0.dessert_type,
        di1_0.price,
        di1_0.sale_status,
        di1_0.updated_at,
        s1_0.sell_amount,
        s1_0.stock_amount 
    from
        stock s1_0 
    left join
        dessert_item di1_0 
            on di1_0.dessert_id=s1_0.dessert_id 
    where
        s1_0.dessert_id=?
Hibernate: 
    select
        s1_0.stock_id,
        s1_0.dessert_id,
        s1_0.sell_amount,
        s1_0.stock_amount 
    from
        stock s1_0 
    where
        s1_0.stock_id=? for update
Hibernate: 
    select
        s1_0.stock_id,
        s1_0.dessert_id,
        s1_0.sell_amount,
        s1_0.stock_amount 
    from
        stock s1_0 
    where
        s1_0.stock_id=? for update
Hibernate: 
    select
        s1_0.stock_id,
        s1_0.dessert_id,
        s1_0.sell_amount,
        s1_0.stock_amount 
    from
        stock s1_0 
    where
        s1_0.stock_id=? for update
Hibernate: 
    select
        s1_0.stock_id,
        s1_0.dessert_id,
        s1_0.sell_amount,
        s1_0.stock_amount 
    from
        stock s1_0 
    where
        s1_0.stock_id=? for update

for update ๊ฐ€ ์—ฌ๋Ÿฌ๋ฒˆ ๋จผ์ € ๋ฐœ์ƒํ•˜๋ฉด์„œ ์œ„์— ๋จผ์ € ์—ฐ๋‹ฌํ•œ ๋ฐœ์ƒํ•œ ์ˆ˜๋งŒํผ ์žฌ๊ณ ๊ฐ€ ์ œ๋Œ€๋กœ ์ˆ˜์ •๋˜์ง€ ๋ชปํ•˜๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๊ณ  ์žˆ์—ˆ๋‹ค

  • ์žฌ๊ณ  : 30
  • ์ฃผ๋ฌธ : 30
    โ†’ ํŒ”๋ฆฐ ์ˆ˜: 26
    โ†’ ๋‚จ์€ ์žฌ๊ณ  : 4

Transation ๊ฒฉ๋ฆฌ ์ˆ˜์ค€์˜ ๋ฌธ์ œ์ธ๊ฐ€ ์‹ถ์–ด์„œ ๊ฒฉ๋ฆฌ ์ˆ˜์ค€๋„ ์˜ฌ๋ฆฌ๊ณ , ๋ฝ ์‹œ๋„ ์‹œ๊ฐ„ ์ฒ˜๋ฆฌ๋„ ์กฐ์ •ํ•˜๋ฉด์„œ ์—ฌ๋Ÿฌ๊ฐ€์ง€๋ฅผ ์กฐ์ •ํ•ด๋ณด์•˜๋Š”๋ฐ๋„ ํ•ด๊ฒฐ์ด ๋˜์ง€ ์•Š์•˜๋‹ค ๊ทธ๋Ÿฌ๋‹ค๊ฐ€ ์•„๋ž˜๊ฐ€ ๋ฌธ์ œ๊ฐ€ ๋œ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ ๋˜์—ˆ๋‹ค

<๐Ÿซ  ๋ฌธ์ œ์˜ ์ฝ”๋“œ>


 // ์ƒํ’ˆ ํ™•์ธ
        DessertItem dessertItem = dessertItemRepository.findById(orderRequestDto.getDessertId())
                                                       .orElseThrow(() -> new NotFoundException(ErrorCode.ITEM_NOT_FOUND));

  



        // ์žฌ๊ณ  ๊ฐ์†Œ
        stockLockService.decreaseStock(dessertItem.getStock.getId(), orderRequestDto.getCount(), dessertItem);

์ƒํ’ˆ์„ ์กฐํšŒํ•˜๊ณ , ์žฌ๊ณ ๋ฅผ ์กฐํšŒํ•˜๋ฉด์„œ ์—ฌ๊ธฐ์„œ ๋น„๊ด€์  ๋ฝ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๋Š”๋ฐ ์—ฌ๊ธฐ์„œ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธฐ์ง€ ์•Š์„๊นŒ ์‹ถ์–ด์กŒ๋‹ค

Hibernate: 
    select
        di1_0.dessert_id,
        di1_0.contents,
        di1_0.created_at,
        di1_0.deleted_at,
        di1_0.dessert_name,
        di1_0.dessert_type,
        di1_0.price,
        di1_0.sale_status,
        di1_0.updated_at 
    from
        dessert_item di1_0 
    where
        di1_0.dessert_id=?
Hibernate: 
    select
        di1_0.dessert_id,
        di1_0.contents,
        di1_0.created_at,
        di1_0.deleted_at,
        di1_0.dessert_name,
        di1_0.dessert_type,
        di1_0.price,
        di1_0.sale_status,
        di1_0.updated_at 
    from
        dessert_item di1_0 
    where
        di1_0.dessert_id=?
Hibernate: 
    select
        di1_0.dessert_id,
        di1_0.contents,
        di1_0.created_at,
        di1_0.deleted_at,
        di1_0.dessert_name,
        di1_0.dessert_type,
        di1_0.price,
        di1_0.sale_status,
        di1_0.updated_at 
    from
        dessert_item di1_0 
    where
        di1_0.dessert_id=?
Hibernate: 
    select
        di1_0.dessert_id,
        di1_0.contents,
        di1_0.created_at,
        di1_0.deleted_at,
        di1_0.dessert_name,
        di1_0.dessert_type,
        di1_0.price,
        di1_0.sale_status,
        di1_0.updated_at 
    from
        dessert_item di1_0 
    where
        di1_0.dessert_id=?
Hibernate: 
    select
        s1_0.stock_id,
        s1_0.dessert_id,
        s1_0.sell_amount,
        s1_0.stock_amount 
    from
        stock s1_0 
    where
        s1_0.stock_id=? for update
Hibernate: 
    select
        s1_0.stock_id,
        s1_0.dessert_id,
        s1_0.sell_amount,
        s1_0.stock_amount 
    from
        stock s1_0 
    where
        s1_0.stock_id=? for update
Hibernate: 
    select
        s1_0.stock_id,
        s1_0.dessert_id,
        s1_0.sell_amount,
        s1_0.stock_amount 
    from
        stock s1_0 
    where
        s1_0.stock_id=? for update
Hibernate: 
    select
        s1_0.stock_id,
        s1_0.dessert_id,
        s1_0.sell_amount,
        s1_0.stock_amount 
    from
        stock s1_0 
    where
        s1_0.stock_id=? for update

๋กœ๊ทธ๋ฅผ ๋ณด๋ฉด dessertItem(์ƒํ’ˆ)์„ ์กฐํšŒํ•˜๋Š” ์ฟผ๋ฆฌ ์ˆ˜ ๋งŒํผ

Hibernate: 
    select
        s1_0.stock_id,
        s1_0.dessert_id,
        s1_0.sell_amount,
        s1_0.stock_amount 
    from
        stock s1_0 
    where
        s1_0.stock_id=? for update

์ด ์ฝ”๋“œ๊ฐ€ ๋ฐ˜๋ณต๋˜์„œ ์—ฐ๋‹ฌ์•„ ๋‚˜์˜ค๊ณ  ๊ทธ ์ˆ˜๋งŒํผ ์žฌ๊ณ ๊ฐ€ ๊ฐ์†Œ๊ฐ€ ๋˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค

  • ์ƒํ’ˆ๊ณผ ์žฌ๊ณ ๋ฅผ ๋ณ„๋„์˜ ์ฟผ๋ฆฌ๋กœ ์กฐํšŒํ•˜๋Š” ๊ณผ์ •์—์„œ, ๋‘ ์ฟผ๋ฆฌ ์‚ฌ์ด์˜ ์‹œ๊ฐ„ ์ฐจ์ด๋กœ ์ธํ•ด ์žฌ๊ณ ์— ๋Œ€ํ•œ ๋น„๊ด€์  ๋ฝ์ด ์ ์šฉ๋˜๊ธฐ ์ „์— ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค ํŒ๋‹จํ–ˆ๋‹ค


2) ๐Ÿš€ ํ†ตํ•ฉ์ฟผ๋ฆฌ

์ƒํ’ˆ์„ ์กฐํšŒํ• ๋•Œ ์žฌ๊ณ ๋„ ํ•œ๊บผ๋ฒˆ์— ์กฐํšŒ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์„ ์จ์„œ ๋น„๊ด€์ ๋ฝ์ด item ์กฐํšŒ์‹œ for update ์ฒ˜๋ฆฌ๊ฐ€ ๋  ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ๋‹ค

@Override
public Optional<DessertDto> findDessertItemByPessimisticLock(Long dessertId) {
    QDessertItem qDessertItem = QDessertItem.dessertItem;
    QStock qStock = QStock.stock;

    DessertDto result = queryFactory.select(Projections.constructor(DessertDto.class, qDessertItem, qStock))
                                    .from(qDessertItem)
                                    .join(qDessertItem.stock, qStock).fetchJoin()
                                    .where(qDessertItem.id.eq(dessertId))
                                    .setLockMode(LockModeType.PESSIMISTIC_WRITE)
                                    .fetchOne();

    return Optional.ofNullable(result);
}
  • ์ƒํ’ˆ์„ ์กฐํšŒํ•  ๋•Œ ์žฌ๊ณ ๋ฅผ fetch join์œผ๋กœ ํ•จ๊ป˜ ๊ฐ€์ ธ์˜ค๋ฉด์„œ ๋น„๊ด€์  ์ž ๊ธˆ์„ ๊ฑธ์–ด ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค
  • ๋™์‹œ์— ์ž ๊ธˆ์„ ๊ฑฐ๋Š” ์‹œ์ ์˜ ๋ณ€๊ฒฝ: ํ†ตํ•ฉ ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด ์žฌ๊ณ  ์ •๋ณด๋ฅผ fetch join ์กฐํšŒํ•˜๋Š” ๋™์‹œ์— ๋น„๊ด€์  ๋ฝ์„ ๊ฑธ์–ด, ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ๊ฐ€ ๋™์ผํ•œ ์žฌ๊ณ  ํ•ญ๋ชฉ์— ๋™์‹œ์— ์ ‘๊ทผํ•˜์ง€ ๋ชปํ•˜๋„๋ก ๋ฐฉ์ง€

ํ†ตํ•ฉ ์ฟผ๋ฆฌ ์ ์šฉ ํ›„ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ, 100๊ฐœ์˜ ์žฌ๊ณ ์— ๋Œ€ํ•ด 100๊ฑด์˜ ์ฃผ๋ฌธ์ด ๋“ค์–ด์™”์„ ๋•Œ ๋‚จ์€ ์žฌ๊ณ ๊ฐ€ 0๊ฐœ๋กœ ์ •ํ™•ํžˆ ์ฒ˜๋ฆฌ๋˜์—ˆ๋‹ค ๐Ÿ”ฅ๐Ÿ”ฅ

๊ทธ๋ฆฌ๊ณ  Jmeter์„ ํ†ตํ•ด 5000๊ฑด์˜ ์ฃผ๋ฌธ์ฒ˜๋ฆฌ๋ฅผ ํ•ด๋ณด์•˜๊ณ  ์•„๋ž˜ ๊ทธ๋ฆผ ์ฒ˜๋ฆผ error ์—†์ด ์ž˜ ์ฒ˜๋ฆฌ ๋˜๋Š” ๊ฒƒ ๊นŒ์ง€ ํ™•์ธ์„ ํ•ด๋ดค๋‹ค

profile
๐Ÿ–ฅ๏ธ โŒจ๏ธ๐Ÿ–ฑ๏ธ๐Ÿฉต

0๊ฐœ์˜ ๋Œ“๊ธ€

๊ด€๋ จ ์ฑ„์šฉ ์ •๋ณด