문제 상황
최근 입찰가보다 1000원 이상 높은 입찰가로 입찰해야 한다는 제약 조건이 있다.
그러나 1000개의 스레드로 동시에 같은 금액으로 입찰했을 때 10건이 중복 저장되는 문제가 발생했다.

해결 1 - 비관적 락 적용
경매 조회 시 비관적 락을 적용해 트랜잭션이 종료될 때까지 유지되도록 하였다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select a from Auction a where a.id = :id")
Optional<Auction> findByIdWithPessimisticLock(Long id);
한 번에 하나의 요청만 접근할 수 있으므로 입찰이 중복으로 저장되는 문제가 해결되었다.
한계
경매는 핵심 엔티티로 수정할 일이 많았다.
비관적 락을 걸면 조회 시점부터 업데이트를 거쳐 트랜잭션이 끝날 때까지 기다려야 하므로 성능 저하가 우려되었다.
해결 2- 유니크 제약조건 추가
입찰 엔티티에 (경매 PK, 입찰 금액)으로 유니크 제약 조건을 추가하였다.
@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"auction_id","bidding_price"})})
동일 입찰가로 중복 저장을 하려고 하면 DataIntegrityViolationException 예외가 발생한다.
한계
동일가는 데이터 정합성이 지켜지지만, 서로 다른 금액으로 동시에 요청을 할 때 lost update가 발생한다.
예를 들어 15,000원 입찰이 저장되고, 이후에 14,000원 입찰이 덮어쓸 수 있다.

해결 3 - 조건부 업데이트 적용
업데이트 시점에 최근 입찰가를 확인하여 업데이트해준다.
신규 입찰가≥최근 입찰가+1000 가 성립할 경우 최근 입찰가를 신규 입찰가로 갱신하는 코드이다.
@Modifying(clearAutomatically = true)
@Query("update Auction a set "
+ "a.currentBiddingPrice=:biddingPrice "
"where a.id = :id "
+ "and :biddingPrice >= a.currentBiddingPrice+1000)"
)
int updateAuctionBiddingPrice(Long id, int biddingPrice);
업데이트 시 where조건이 성립하지 않으면 바로 실패한다.
updatedRow=0이면 업데이트된 행이 없다는 의미이므로, 더 높은 입찰가를 설정하라는 예외를 반환하였다.
int updatedRow = auctionRepository.updateAuctionBiddingPrice(auctionId, request.biddingPrice());
if (updatedRow == 0) {
throw new ValidationException(BiddingErrorCode.BIDDING_PRICE_NOT_HIGH_ENOUGH);
}
where 조건이 만족하면 갱신 시점에 락을 걸기 때문에 lost update 문제가 해결된다.

조회 시점부터 락을 잡는 비관적 락에 비해 락 점유 시간이 짧다는 이점이 있다.
추가로 고려한 점
최초 입찰 시 제약 사항
신규 입찰가≥초기 입찰가 제약 조건을 비즈니스 단에서 검사할지 아니면 DB에서 검증할지 고민하였다.
아래의 이유로 비즈니스 단에서 검사하기로 하였다.
1. 초기 입찰가는 변하지 않으므로 DB를 거칠 필요가 없다.
2. 쿼리 가독성을 높이고, 예외 메시지를 명확히 분리할 수 있다.
Auction auction = getAuctionById(auctionId);
if (request.biddingPrice() < auction.getInitPrice()) {
throw new ValidationException(BiddingErrorCode.BIDDING_PRICE_LESS_THAN_INIT_PRICE);
}
int updatedRow = auctionRepository.updateAuctionBiddingPriceAndCount(auctionId, request.biddingPrice());
...
BiddingCount 업데이트 추가
Auction에 BiddingCount 필드가 존재한다.
비즈니스 코드에 있던 입찰 수 증가 로직을 쿼리에 포함시켜 값을 원자적으로 증가시킴으로써 lost update를 방지하였다.
@Modifying(clearAutomatically = true)
@Query("update Auction a set "
+ "a.currentBiddingPrice=:biddingPrice, "
+ "a.biddingCount = a.biddingCount+1 " +
"where a.id = :id "
+ "and (a.currentBiddingPrice is null or :biddingPrice >= a.currentBiddingPrice+1000)"
)
int updateAuctionBiddingPriceAndCount(Long id, int biddingPrice);