๐ฑ ๋ฌธ์ ์ํฉ
- ํ๋งค ์ ์ฐฐ๊ณผ ๊ตฌ๋งค ์ ์ฐฐ์ด ์กด์ฌํ๋ค.
- ํ๋งค ์ ์ฐฐ๊ณผ ๊ตฌ๋งค ์ ์ฐฐ์ 1:1 ๊ด๊ณ์ด๋ค.
- ๊ตฌ๋งค ์ ์ฐฐ ์์ฒญ์ด ๋์์ ๋ค์ด์๋, 1๊ฐ์ ๊ตฌ๋งค ์ ์ฐฐ๋ง ์ฑ๊ณตํด์ผ ํ๋ค.
ํ๋งค ์ ์ฐฐ์ ๊ด๋ จ๋ ๊ตฌ๋งค ์ ์ฐฐ ์์ฑ ๋ก์ง
@RequiredArgsConstructor
@UseCase
public class BidUseCase {
private final BidRepository bidRepository;
@Transactional
public void order(User user, OrderRequestDto request) {
Bid sellBid = bidRepository.findFirstByProductIdAndPriceAndStatusOrderByCreatedAtAsc(
request.productId(), request.price()).orElseThrow(() -> NOT_FOUND_BID_WITH_CONDITION);
Bid buyBid = bidRepository.save(Bid.transactSellBidAndCreateBuyBid(user, bid));
}
}
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Bid extends BaseTimeEntity {
public enum BidType {
SELL, BUY
}
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Status status;
@Column(updatable = false)
private LocalDateTime transactionAt;
// ... ์๋ต
public static Bid transactSellBidAndCreateBuyBid(User user, Bid sellBid) {
LocalDateTime transactionAt = LocalDateTime.now();
sellBid.doTransaction(transactionAt);
return create(user, sellBid, Status.IN_TRANSACTION, BidType.BUY, transactionAt);
}
private void doTransaction(LocalDateTime transactionAt) {
validate(Status.LIVE);
this.status = Status.IN_TRANSACTION;
this.transactionAt = transactionAt;
}
}
์ด๋ ์ฃผ์ด์ง ์ํ๊ณผ ๊ฐ๊ฒฉ์ ๋ง๋ ์ ์ฐฐ์ ์กฐํํ๊ณ , ํด๋น ์ ์ฐฐ์ ๊ฑฐ๋ ์ค์ผ๋ก ์ํ ๋ณ๊ฒฝํ ํ ์๋ก์ด ๊ตฌ๋งค ์ ์ฐฐ์ ์์ฑํ๋ ์ฃผ๋ฌธ API ๋ก์ง์ด๋ค.
๋น์ฆ๋์ค ๋ก์ง ์ ํ๋งค ์ ์ฐฐ ํ ๊ฐ ๋น ํ ๊ฐ์ ๊ตฌ๋งค ์ ์ฐฐ์ด ์์ฑ๋์ด์ผ ํ๋๋ฐ ํด๋น ๋ก์ง๋๋ก๋ผ๋ฉด ์ฌ๋ฌ ๊ฐ์ ์ฃผ๋ฌธ API๋ฅผ ๋์์ ํธ์ถํ ๊ฒฝ์ฐ ๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ๊ฒ์ด๋ค.
์ค์ ๋ก cmd์์ curl & ๋ช ๋ น์ ํตํด ๋์์ 5๊ฐ์ ์ฃผ๋ฌธ API๋ฅผ ํธ์ถํ์ ๋ 5๊ฐ์ ๊ตฌ๋งค ์ ์ฐฐ์ด ์์ฑ๋๋ ๊ฒ์ ํ์ธํ ์ ์์๋ค.
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ ๊ฐ ์ด์์ ์ค๋ ๋๊ฐ ๋์์ ์ ๊ทผํ์ง ๋ชปํ๋๋ก ๋ฉ์๋์ synchronized ํค์๋๋ฅผ ์ ์ธํ๋ฉด ์ด๋ป๊ฒ ๋ ๊น?
๐ฑ ํด๊ฒฐ๋ฐฉ์
1๏ธโฃ synchronized
@RequiredArgsConstructor
@UseCase
public class OrderUseCase {
private final BidRepository bidRepository;
@Transactional
public synchronized void order(User user, OrderRequestDto request) {
Bid sellBid = bidService.readOrderableBid(request.productId(), request.price())
.orElseThrow(() -> NOT_FOUND_BID_WITH_CONDITION);
Bid buyBid = bidRepository.save(Bid.transactSellBidAndCreateBuyBid(user, bid));
}
}
์ฃผ๋ฌธ ๋ก์ง์ synchronized ํค์๋๋ฅผ ๋ถ์ฌ ๋ค์ 5๊ฐ์ ์ฃผ๋ฌธ API๋ฅผ ๋์์ ํธ์ถํด ๋ณด์๋ค.
์๊น๋ณด๋ค๋ ๊ตฌ๋งค ์ ์ฐฐ์ ๊ฐ์๊ฐ ์ค์ด๋ค๊ธฐ๋ ํ์ง๋ง ์๋๋๋ก 1๊ฐ์ ๊ตฌ๋งค ์ ์ฐฐ์ด ์์ฑ๋ ๊ฒ์ ์๋๋ค. synchronized๋ฅผ ์ฌ์ฉํ๋ฉด ํ ๊ฐ์ ์ค๋ ๋๋ง ๊ณต์ ์์์ ์ ์ ํ ํ ๋ฐ ์ ์ด๋ฐ ๊ฒฐ๊ณผ๊ฐ ๋์์๊น?
๐ค synchronized๊ฐ ๋จนํ์ง ์์ ์ด์
๊ทธ ์ด์ ๋ ๋ฐ๋ก @Transactional ๋๋ฌธ์ด๋ค. @Transactional ์ฌ์ฉ ์ Spring AOP๋ฅผ ํตํด ํธ๋์ญ์ ๊ด๋ฆฌ ๊ธฐ๋ฅ์ด ์ ์ฉ๋๋๋ฐ ์ด ๊ณผ์ ์์ ํ๋ก์ ๊ฐ์ฒด๊ฐ ๋ง๋ค์ด์ง๊ฒ ๋๋ค.
// ํ๋ก์ ๊ฐ์ฒด
public class OrderUseCaseProxy extends OrderUseCase {
private final BidRepository bidRepository;
@Override
public void order(User user, OrderRequestDto request) {
// ...
}
}
// ๊ธฐ์กด ๊ฐ์ฒด
public class OrderUseCase {
private final BidRepository bidRepository;
public synchronized void order(User user, OrderRequestDto request) {
// ...
}
}
ํ๋ก์ ๊ฐ์ฒด๋ ๊ธฐ์กด ๊ฐ์ฒด์ธ OrderUseCase๋ฅผ ์์ํด์ ๋ง๋ค์ด์ง๋๋ฐ, synchronized๋ ๋ฉ์๋ ์๊ทธ๋์ฒ(๋ฉ์๋ ์ด๋ฆ / ํ๋ผ๋ฏธํฐ ํ์ ๊ณผ ๊ฐ์)๊ฐ ์๋๊ธฐ ๋๋ฌธ์ ์์๋์ง ์๋๋ค. ๋ฐ๋ผ์ ํ๋ก์ ๊ฐ์ฒด์ order() ๋ฉ์๋๋ ์ฌ๋ฌ ์ค๋ ๋๊ฐ ์ฌ์ฉํ ์ ์๊ฒ ๋๋ค.
์ ๋ฆฌํด์ ๊ธฐ์กด ๊ฐ์ฒด์ order()๋ ๋์์ฑ ์ ์ด๋ฅผ ๋ฐ์ง๋ง, @Transactional ์ด๋ ธํ ์ด์ ์ผ๋ก ์ธํด ์์ฑ๋ ํ๋ก์ ๊ฐ์ฒด์ order()๋ ๋์์ฑ ์ ์ด๋ฅผ ๋ฐ์ง ์์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ ๊ฒ์ด๋ค.
๐ค ๊ทธ๋ผ @Transactional์ ์ฌ์ฉํ์ง ์๋ ๊ฒ์ ์ด๋จ๊น?
@RequiredArgsConstructor
@UseCase
public class OrderUseCase {
private final BidRepository bidRepository;
public synchronized void order(User user, OrderRequestDto request) {
Bid sellBid = bidService.readOrderableBid(request.productId(), request.price())
.orElseThrow(() -> NOT_FOUND_BID_WITH_CONDITION);
Bid buyBid = bidRepository.save(Bid.transactSellBidAndCreateBuyBid(user, bid));
bidRepository.save(sellBid);
}
}
@Transactional ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ์ง ์์ ํธ๋์ญ์ ์ด ์๋์ผ๋ก ๊ด๋ฆฌ๋์ง ์๊ธฐ ๋๋ฌธ์ sellBid์ ์ํ๋ฅผ ๋ณ๊ฒฝ(LIVE → IN_TRANSACTION, transactionAt ๊ฐฑ์ ) ํ ํ save๋ฅผ ํธ์ถํ์ฌ ๋ณ๊ฒฝ์ฌํญ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ช ์์ ์ผ๋ก ์ ์ฅํ๋ ์ฝ๋๋ฅผ ์ถ๊ฐํด ์ฃผ์๋ค.
์ด ๊ฒฝ์ฐ ์๋ํ๋ ๋น์ฆ๋์ค ๋ก์ง๋๋ก ํ ๊ฐ์ ๊ตฌ๋งค ์ ์ฐฐ๋ง ์์ฑ๋๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
ํ์ง๋ง synchronized๋ ๊ทผ๋ณธ์ ์ธ ํด๊ฒฐ์ฑ ์ด ๋ ์ ์๋ค. ์ฐ์ synchronized๋ ๋ชจ๋ ์ค๋ ๋ ๋์์ ๋ํด ๋ฝ์ ๊ฑธ์ด ์ค๋ฒํค๋๊ฐ ๋ฐ์ํ ์ ์๋ค. ๋ฟ๋ง ์๋๋ผ synchronized๋ ํ ํ๋ก์ธ์ค ๋ด์์ ํ ๋ฒ์ ํ๋์ ์ค๋ ๋๋ง ๊ณต์ ์์์ ์ ๊ทผํ๋ ๊ฒ์ ๋ณด์ฅํ๋ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ๋ ๊ฐ ์ด์์ ์๋ฒ๋ฅผ ์ฌ์ฉํ๊ฒ ๋๋ฉด ๋์์ฑ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์๋ค.
2๏ธโฃ DB Lock
๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฝ์๋ ๋ ๊ฐ์ง๊ฐ ์๋ค.
๐ ๋๊ด์ ๋ฝ(Optimistic Lock)
๋๊ด์ ๋ฝ์ ๋์ ์์ฒญ์ ์ง์ ์ ์ผ๋ก ๋ง์ง ์๊ณ , ํ์ฌ ํธ๋์ญ์ ์ ๋ฐ์ดํฐ๋ฅผ ์ปค๋ฐํ๊ธฐ ์ ๋ค๋ฅธ ํธ๋์ญ์ ์์ ๋ณ๊ฒฝ์ ํ๋์ง ํ์ธ(version or timestamp)์ ํตํด ๋์์ ๋ฐ์ดํฐ๊ฐ ์์ ๋๋ ๊ฒ์ ๋ง๋๋ค.
๋ง์ฝ ๋ฐ์ดํฐ์ ๋ฒ์ ์ด๋ ํ์์คํฌํ๊ฐ ๋ค๋ฅผ ๊ฒฝ์ฐ ์ถฉ๋์ ๊ฐ์งํด ํ์ฌ ํธ๋์ญ์ ์ ๋กค๋ฐฑํ๊ฑฐ๋ ์ฌ์๋ํ์ฌ ๋์์ฑ ๋ฌธ์ ๋ฅผ ์ฒ๋ฆฌํ๋ค.
๐ ์ฅ์
- ๋์ ์์ฒญ์ ๋ํด DB ๋ฝ์ ๊ฑธ์ง ์๊ธฐ ๋๋ฌธ์ ๋น๊ด์ ๋ฝ๋ณด๋ค ์ฑ๋ฅ ํฅ์ ์ด์ ์ด ์๋ค.
- ๋๊ด์ ๋ฝ์ ์ถฉ๋์ด ์์ฃผ ๋ฐ์ํ์ง ์๋๋ค๊ณ ๊ฐ์ ํ๊ธฐ ๋๋ฌธ์, ๋ง์ ์ฌ์ฉ์๊ฐ ๋์์ ๋ฐ์ดํฐ์ ์ ๊ทผํ ์ ์์ด ์ฒ๋ฆฌ๋์ด ๋๋ค.
๐ฅฒ ๋จ์
- ๋์ ์์ฒญ์ผ๋ก ๋ฐ์ดํฐ ์ถฉ๋์ด ๋ฐ์ํ์ ๋ ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํ ์ถ๊ฐ์ ์ธ ๋ก์ง์ ๊ตฌํ์ด ํ์ํ๋ค.
- ๋ฐ์ดํฐ์ ๋ณ๊ฒฝ ๋น๋๊ฐ ๋์ ์์คํ ์์๋ ์ถฉ๋์ด ์์ฃผ ๋ฐ์ํ๊ธฐ ๋๋ฌธ์ ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํ ์ถ๊ฐ์ ์ธ ์๊ฐ์ด ํ์ํ๋ค.
์ฆ, ๋๊ด์ ๋ฝ์ ์ถฉ๋์ด ๋น๋ฒํ์ง ์๊ณ ์ฑ๋ฅ์ด ์ฐ์ ์๋๋ ๊ฒฝ์ฐ์ ๋์ ์์ฒญ์์ ํ๋์ ์์ฒญ๋ง ์ฒ๋ฆฌํด์ผ ํ๋ ๊ฒฝ์ฐ ์ ํฉํ๋ค.
๐ ๋น๊ด์ ๋ฝ(Pessimistic Lock)
๋น๊ด์ ๋ฝ์ ์ด๋ ํธ๋์ญ์ ์์ ํน์ ํ ์ด๋ธ์ ์ ๊ทผ ์ ๋ฝ์ ๊ฑธ์ด(DB ์ ์ฒด, ํ ์ด๋ธ, ์ด) ํด์ ๋ ๋๊น์ง ๋ค๋ฅธ ํธ๋์ญ์ ์์ ํด๋น ํ ์ด๋ธ์ ์ ๊ทผํ์ง ๋ชปํ๋๋ก ์ ๊ทผ๋ค.
๐ ์ฅ์
- ๋๊ด์ ๋ฝ๊ณผ๋ ๋ค๋ฅด๊ฒ ํ ์ด๋ธ์ ๋ฝ์ ๊ฑธ์ด ๋ค๋ฅธ ํธ๋์ญ์ ์์ ์์ ์ ๊ทผ์ ๋ชปํ๊ธฐ ๋๋ฌธ์ ๋ฐ์ดํฐ์ ์ผ๊ด์ฑ์ด ๋ณด์ฅ๋๋ค.
- ํธ๋์ญ์ ์์ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ๊ธฐ ์ ๋ฝ์ ๊ฑธ๊ธฐ ๋๋ฌธ์ ๋ฐ์ดํฐ ๋ณ๊ฒฝ ์ค ๋ค๋ฅธ ํธ๋์ญ์ ๊ณผ์ ์ถฉ๋ ๊ฐ๋ฅ์ฑ์ด ๋ฎ๋ค.
๐ฅฒ ๋จ์
- ๋ฐ์ดํฐ์ ์ผ๊ด์ฑ์ ๋ณด์ฅํ์ง๋ง ๋์ ์ ์์๊ฐ ๋ง์ ํ๊ฒฝ์์๋ ๋ฝ ๋๊ธฐ์๊ฐ์ผ๋ก ์ธํด ์ฑ๋ฅ์ ์ํฅ์ด ์๋ค.
- ๋ค์์ ํธ๋์ญ์ ์ด ์๋ก ๋ค๋ฅธ ์์๋ก ์ฌ๋ฌ ๋ฐ์ดํฐ์ ๋ฝ์ ์์ฒญํ๋ฉด ๋ฐ๋๋ฝ์ด ๋ฐ์ํ ์ ์๋ค.
์ฆ, ๋น๊ด์ ๋ฝ์ ๋ฐ์ดํฐ์ ์ ํฉ์ฑ์ด ์ค์ํ๊ณ ํธ๋์ญ์ ๊ฐ ์ถฉ๋์ด ๋น๋ฒํ๊ฒ ๋ฐ์ํ ์ ์๋ ๊ฒฝ์ฐ์ ์ ํฉํ๋ค.
๐ซง ๋น๊ด์ ๋ฝ ์ ์ฉ
๋น๊ด์ ๋ฝ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฐฐํ๋ฝ์ ์ฌ์ฉํ๋ค.
- ํธ๋์ญ์
T1์ด ๋ฐ์ดํฐ๋ฅผ ์กฐํํ ๋, ๋ฐฐํ๋ฝ์ ๊ฑด๋ค.
- select for update๋ฅผ ์ฌ์ฉํ ๋ฝ
- ํธ๋์ญ์
T2๊ฐ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๋ ค๊ณ ํ์ผ๋, ๋ฐฐํ๋ฝ์ด ๊ฑธ๋ ค ์์ด ์กฐํ๊ฐ ๋ถ๊ฐ๋ฅํ๋ค.
- T2๋ ๋ฐ์ดํฐ์ ๋ฝ์ด ํด์ ๋ ๋๊น์ง ๋๊ธฐํ๋ค.
- T1์ด ๋ฐ์ดํฐ๋ฅผ ์์ ํ๊ณ , ์ปค๋ฐํ๋ค.
- T2๊ฐ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ ๋, ๋ฐฐํ๋ฝ์ ๊ฑด๋ค.
- T2๊ฐ ๋ฐ์ดํฐ๋ฅผ ์์ ํ๊ณ , ์ปค๋ฐํ๋ค.
๋น๊ด์ ๋ฝ์ org.springframework.data.jpa.repository.Lock ์ด๋ ธํ ์ด์ ์ ๋ฝ ์ต์ ์ PESSIMISTIC_WRITE์ผ๋ก ์ง์ ํด DB์ ๋ฐฐํ๋ฝ์ ๊ฑฐ๋ ๊ฒ์ด๋ค.
public interface BidRepository extends JpaRepository<Bid, Long>, BidCustomRepository {
@Lock(value = LockModeType.PESSIMISTIC_WRITE)
@QueryHints( {@QueryHint(name = "javax.persistence.lock.timeout", value = "3000")})
Optional<Bid> findFirstByProductIdAndPriceAndStatusOrderByCreatedAtAsc(Long productId,
Integer price, Status status);
}
ํธ๋์ญ์ ์ Lock์ ํ๋ํ ๋๊น์ง ๋๊ธฐํ๋ฏ๋ก, ์ถ๊ฐ์ ์ผ๋ก Lock Timeout์ ์ค์ ํด ์ฃผ์๋ค.
๋์์ 5๊ฐ์ ์ฃผ๋ฌธ API๋ฅผ ํธ์ถํ์ ๋ 1๊ฐ์ ๊ตฌ๋งค ์ ์ฐฐ์ด ์์ฑ๋๋ ๊ฒ์ ํ์ธํ ์ ์์๋ค.
๋๊ด์ ๋ฝ vs ๋น๊ด์ ๋ฝ
๋๊ด์ ๋ฝ๊ณผ ๋น๊ด์ ๋ฝ ์ค ์ด๋ค ๋ฝ์ ์ฌ์ฉํ ์ง๋ ํธ๋์ญ์ ๊ฐ ์ถฉ๋ ๋น๋์ ๋ฐ๋ผ ๊ฒฐ์ ํ ์ ์๋ค.
- ์ถฉ๋ ๋น๋ ๋ฎ์. ๋ฐ์ดํฐ ์ ํฉ์ฑ๋ณด๋ค ์ฑ๋ฅ์ด ์ค์ → ๋๊ด์ ๋ฝ
- ์ถฉ๋ ๋น๋ ๋น๋ฒ. ๋ฐ์ดํฐ ์ ํฉ์ฑ ์ค์ → ๋น๊ด์ ๋ฝ
๋ฐ๋ผ์ ์ฌ๋ฌ ์ฌ๋์ด ๋์์ ์ํ์ ์ฃผ๋ฌธํ๋ ๊ฒฝ์ฐ ์ถฉ๋์ด ๋น๋ฒํ๊ฒ ์ผ์ด๋๊ธฐ ๋๋ฌธ์ ๋น๊ด์ ๋ฝ ์ฌ์ฉ์ ๊ณ ๋ คํด ๋ณด๊ณ , ์ฌ๋ฌ ์ฌ๋์ด ์ฃผ๋ฌธํ๊ธฐ๋ ํ๋ ์ฃผ๋ฌธํ๋ ์๊ฐ์ด ๊ฐ์ ๋ค๋ฅธ ๊ฒฝ์ฐ ์ถฉ๋์ด ๋น๊ต์ ์ ๊ฒ ์ผ์ด๋๊ธฐ ๋๋ฌธ์ ๋๊ด์ ๋ฝ ์ฌ์ฉ์ ๊ณ ๋ คํด ๋ณผ ์ ์๋ค.
๊ทธ ์ด์ ๋ ์ถฉ๋์ด ๋น๋ฒํ๊ฒ ์ผ์ด๋๋ ์ํฉ์์ ๋๊ด์ ๋ฝ์ ์ฌ์ฉํ๊ฒ ๋๋ค๋ฉด ๊ตฌํ๋ง๋ค ๋ค๋ฅด์ง๋ง ๋ชจ๋ ์์ฒญ์ด ์๋ฃ๋ ๋๊น์ง ์ฌ์๋๋ฅผ ์ํํ๊ฒ ๋์ด ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๊ต์ฅํ ๋ง์ ์์ฒญ์ ๋ณด๋ด๊ฒ ๋ ๊ฒ์ด๋ค.
๋ฐ๋ฉด ๋น๊ด์ ๋ฝ์ ์ฌ์ฉํ๋ค๋ฉด Lock์ ๊ฑธ๊ณ ์์ฒญ์ ์ํํ๊ธฐ ๋๋ฌธ์ ์ดํ์ ์์ฒญ๋ค์ ์ ๋ฐ์ดํธ๊ฐ ์๋ฃ๋ ๋๊น์ง ๊ธฐ๋ค๋ฆฐ ํ ์์ฐจ์ ์ผ๋ก ์์ฒญ์ ์งํํ๊ฒ ๋๋ค. ๋ฐ๋ผ์ ์ถฉ๋ ๊ฐ๋ฅ์ฑ์ด ๋์ผ๋ฉด ๋น๊ด์ ๋ฝ์ด ๋ ์ข์ ์ ํ์ด ๋ ์ ์๋ ๊ฒ์ด๋ค.
ํ์ง๋ง ํฐ์ผํ ์ฒ๋ผ ์ ํด์ง ์๊ฐ์ ํธ๋ํฝ์ด ๋ง์ด ๋ชฐ๋ฆฌ๋ ์ด๋ฒคํธ์ธ ๊ฒฝ์ฐ ์ถฉ๋์ด ์ฆ๋ค๊ณ ๋ณผ ์ ์์ผ๋, ์ฃผ๋ฌธ ๋ก์ง์ ๊ฒฝ์ฐ ์ค์ ํธ๋ํฝ์ด ์๊ธฐ๊ธฐ ์ ๊น์ง๋ ์ถฉ๋ ๋น๋๋ฅผ ์ถ์ ํ๊ธฐ๊ฐ ์ด๋ ต๋ค. ๋ฐ๋ผ์ ์ด๋ ๊ฒ ์ถฉ๋์ด ๋ง์ด ์ผ์ด๋ ์ง ์ ์ ์๋ ๊ฒฝ์ฐ์๋ ์ฐ์ ๋๊ด์ ๋ฝ์ ์ฌ์ฉ(๋น๊ด์ ๋ฝ์ DB ๋ฝ์ ๋น์ฉ์ด ํ์ํ๊ธฐ ๋๋ฌธ)ํ๋ค๊ฐ, ์ด์์ ์ฑ๋ฅ ์ด์๊ฐ ๋ฐ์ํ ๋ ๋น๊ด์ ๋ฝ์ด๋ ๋ค๋ฅธ ๋ฐฉ๋ฒ์ ๊ณ ๋ คํด ๋ณด๋ ๊ฒ์ด ์ข๋ค๊ณ ํ๋ค.
์ด์ ์ ๋๊ด์ ๋ฝ์ ์ฌ์ฉํด๋ณธ ๊ฒฝํ์ด ์์ด์ ๋น๊ด์ ๋ฝ์ ์ฌ์ฉํด๋ณด๊ณ ์ถ์์ผ๋ ๋น๊ด์ ๋ฝ ์ฌ์ฉ ์ ๋ฐ์ํ ์ ์๋ ๋ฌธ์ ์ (์ฑ๋ฅ ์ ํ, ๋ฐ๋๋ฝ ๋ฐ์ ๊ฐ๋ฅ์ฑ, ํ์ฅ์ฑ ์ ํ, ์์ ๋ญ๋น ๋ฑ)์ผ๋ก ์ฐ์ ์ ๋๊ด์ ๋ฝ์ ์ฌ์ฉํ์ฌ ์์คํ ์ ๊ตฌ์ถํ๊ณ , ์ค์ ํธ๋ํฝ ํจํด์ ๋ชจ๋ํฐ๋งํ ํ ํ์ํ ๊ฒฝ์ฐ ๋น๊ด์ ๋ฝ์ผ๋ก ์ ํํ๋ ๊ฒ์ด ๋ ์ข์ ๊ฒ์ด๋ผ ํ๋จํด ๋๊ด์ ๋ฝ์ ์ฌ์ฉํ๊ธฐ๋ก ํ์๋ค.
@Version
JPA๋ @Version ์ด๋ ธํ ์ด์ ์ ํตํด ์ํฐํฐ์ ๋ฒ์ ์ ๊ด๋ฆฌํ ์ ์๋ค. ์ด ์์ฑ์ ์ ์ํ ๋๋ ์๋์ ๊ท์น์ ์ง์ผ์ผ ํ๋ค.
- ๊ฐ Entity ํด๋์ค์๋ @Version ์์ฑ์ด ํ ๊ฐ๋ง ์์ด์ผ ํ๋ค.
- ์ฌ๋ฌ ํ ์ด๋ธ์ ๋งคํ๋ Entity์ ๊ฒฝ์ฐ ๊ธฐ๋ณธ ํ ์ด๋ธ์ ๋ฐฐ์น๋์ด์ผ ํ๋ค.
- ๋ฒ์ ํ์ ์ Integer(int), Long(long), Short(short), java.sql.Timestamp ์ค ํ๋์ฌ์ผ ํ๋ค.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Bid extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
..
@Version
private Long version;
}
์ด ํ๋์ ๊ฐ ๋๋ ์๊ฐ์ผ๋ก ์ฒ์ ์กฐํ๋ ๋์ ๋ฒ์ ๊ณผ ์ปค๋ฐ๋ ๋์ ๋ฒ์ ์ ๋น๊ตํด ๋ง์ฝ ๋ค๋ฅผ ๊ฒฝ์ฐ ์ถฉ๋์ด ๋ฐ์ํ ๊ฒ์ผ๋ก ํ๋จํด ์์ธ๋ฅผ ๋ฐ์์ํจ๋ค.
[transaction-1]: ํ๋งค ์
์ฐฐ ์กฐํ / version: 1
[transaction-2]: ํ๋งค ์
์ฐฐ ์กฐํ / verison: 1
---- ๋ ํธ๋์ญ์
์ค transaction-1 ๋จผ์ ์๋ฃ๋จ ----
[transaction-1]: ๊ตฌ๋งค ์
์ฐฐ ์์ฑ / version: 2 ๋ก ์
๋ฐ์ดํธํ๊ณ ์ปค๋ฐ
[transaction-2]: ๊ตฌ๋งค ์
์ฐฐ ์์ฑ / verison: 2 ๋ก ์
๋ฐ์ดํธํ๊ณ ์ปค๋ฐํ๋ คํ ๋
transaction-1 ์์ version์ด 2๋ก ๋ณ๊ฒฝ๋์ด ์กฐํํ๋ ๋ฒ์ ๊ณผ ๋ค๋ฅด๋ฏ๋ก ์
๋ฐ์ดํธ ์คํจ
UPDATE bid
SET
bid_type=?,
version=2
WHERE id=?
AND version=1
์์ ๊ฐ์ด ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ์ง๋ง version์ด ์ด๋ฏธ [transaction-1]๋ก ์ธํด 2๋ก ์ฆ๊ฐ๋ ์ํ๋ผ ์ ๋ฐ์ดํธ ๋์์ ์ฐพ์ง ๋ชปํด ์์ธ๊ฐ ๋ฐ์ํ๋ ๊ฒ์ด๋ค.
LockModeType
LockModeType๋ฅผ OPTIMISTIC์ผ๋ก ์ค์ ํด ๋๊ด์ ๋ฝ์ ์ฌ์ฉํ๋ค.
public interface BidRepository extends JpaRepository<Bid, Long>, BidCustomRepository {
@Lock(LockModeType.OPTIMISTIC)
Optional<Bid> findFirstByIdAndBidTypeAndStatusOrderByCreatedAtAsc(Long id, BidType bidType,
Status status);
}
๋๊ด์ ๋ฝ์ ์์ธ ์ข ๋ฅ
- javax.persistence.OptimisticLockException (JPA)
- org.hibernate.StaleObjectStateException (Hibernate)
- org.springframework.orm.ObjectOptimisticLockingFailureException (Spring)
Spring ๊ธฐ๋ฐ JPA์์ ๋๊ด์ ๋ฝ์ ์ฌ์ฉํ๋ฉด version ์ถฉ๋ ์ Hibernate์์ StaleStateException์ ๋ฐ์์ํจ๋ค. ๊ทธ ํ Spring์์ ํด๋น ์์ธ๋ฅผ ObjectOptimisticLockingFailureException๋ก ๊ฐ์ธ์ ์๋ตํ๋ค.
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
...
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(ObjectOptimisticLockingFailureException.class)
public ErrorResponse handleObjectOptimisticLockingFailureException(ObjectOptimisticLockingFailureException e) {
log.info("๊ตฌ๋งค ์
์ฐฐ ์์ฑ ๋์์ฑ ๋ฌธ์ ๋ฐ์", e);
return ErrorResponse.of("UNEXPECTED_ERROR", e.getMessage());
}
}
GlobalExceptionHandler์ ObjectOptimisticLockingFailureException์ ์ฒ๋ฆฌํ๋ ๋ก์ง์ ์ถ๊ฐํด ์ฃผ์๋ค.
+ ๋ค๋ฅธ ํด๊ฒฐ ๋ฐฉ์
๊ธ์ ์๋ถ๋ถ์์ ๋๊ด์ ๋ฝ์ ์ฌ์ฉํ๋ค ์ด์์ ์ฑ๋ฅ ์ด์๊ฐ ๋ฐ์ํ ๋ ๋น๊ด์ ๋ฝ์ ๊ณ ๋ คํด ๋ณด๋ ๊ฒ์ด ์ข๋ค๊ณ ์จ๋์์ง๋ง, ๋น๊ด์ ๋ฝ์ Lock์ด ํ์ํ์ง ์์ ์ํฉ์์๋ ๋ชจ๋ ํธ๋์ญ์ ์ ๋ํด Lock์ ์ฌ์ฉํด ํธ๋ํฝ์ด ๋ง์ ๊ฒฝ์ฐ O(N^2) ์ ๋ ์ฑ๋ฅ์ด ์ ํ๋ ์ ์์ผ๋ฉฐ ๋ ๊ฐ ์ด์์ column ํน์ ํ ์ด๋ธ์ Lock์ ๊ฑธ ๊ฒฝ์ฐ ๋ฐ๋๋ฝ์ด ๋ฐ์ํ ์ ์๋ค๋ ๋จ์ ์ด ์๋ค.
๋ฐ๋ผ์ ์ด๋ฐ ์ด์๋ค์ ๋น๊ด์ ๋ฝ์ด ์๋ ๋ค์ ๋ฐฉ๋ฒ์ ํค์๋๋ก ์ฐธ๊ณ ํด ๋ณด๋ฉด ๋ ๊ฒ ๊ฐ๋ค.
- Redis Sorted Set ํ์ฉ
- Redis์ Lua Script ํ์ฉ
- Kafka์ ๊ฐ์ ๋ฉ์์ง ํ ๋์
- API Gateway์์ ์ฒ๋ฆฌ์จ ์ ํ ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ
- ์ฒ๋ฆฌ์จ ์ ํ๊ธฐ ๋ฏธ๋ค์จ์ด ๋์
๐ ์ฐธ๊ณ
[JPA] @Transactional ๊ณผ ๋์์ฑ
How to specify @lock timeout in spring data jpa query?
[E-commerce] ๋์์ฑ ๋ฌธ์ ํด๊ฒฐํ๊ธฐ (๋น๊ด์ ๋ฝ, ๋ค์๋ ๋ฝ, ๋ถ์ฐ ๋ฝ)
๋์์ฑ ๋ฌธ์ ํด๊ฒฐํ๊ธฐ V2 - ๋น๊ด์ ๋ฝ(Pessimistic Lock)
๋๊ด์ ๋ฝ๊ณผ ๋น๊ด์ ๋ฝ์ ๊ฐฑ์ ์์ค ๋ฌธ์ ๋ฅผ ์ด๋ป๊ฒ ํด๊ฒฐํ๋๊ฐ