e-commerce ํ๋ก์ ํธ๋ฅผ ๋ง๋ค๋ฉฐ ๋์์ ๋ค์์ ์ฃผ๋ฌธ์ด ๋ค์ด์ฌ๋ 100๊ฐ์
์ฒ์์ผ๋ก ์๊ฐํ๋ ๋ฐฉ๋ฒ์ ์๋ฌด๋๋ 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
์ด ์ฝ๋๊ฐ ๋ฐ๋ณต๋์ ์ฐ๋ฌ์ ๋์ค๊ณ ๊ทธ ์๋งํผ ์ฌ๊ณ ๊ฐ ๊ฐ์๊ฐ ๋์ง ์์๊ธฐ ๋๋ฌธ์ด๋ค
์ํ์ ์กฐํํ ๋ ์ฌ๊ณ ๋ ํ๊บผ๋ฒ์ ์กฐํ ํ ์ ์๋ ๋ฐฉ๋ฒ์ ์จ์ ๋น๊ด์ ๋ฝ์ด 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 ์์ด ์ ์ฒ๋ฆฌ ๋๋ ๊ฒ ๊น์ง ํ์ธ์ ํด๋ดค๋ค