Project
티켓 발급 서비스에서 JPA 낙관적 락 구현
어떻게말이름이히힝
2025. 3. 27. 16:38
1. Entity에 @Version 필드 추가
2. 트랜잭션 설정
3. @Retry 어노테이션을 사용한 자동 재시도 구현
aop와 retry 의존성 추가
// Retry
implementation "org.springframework.retry:spring-retry"
// AOP
runtimeOnly 'org.springframework.boot:spring-boot-starter-aop'
concert.findbyId > 현재 version값을 1차캐시에 저장
트랜잭션 종료, 커밋 시점에 DB의 version값과 1차 캐시에 저장된 값을 비교함
버전 일치 > 버전 값 증가 / 버전 불일치 -> OptimisticLockException 발생 -> Retry
Example)
Thread 1 > 버전이 맞고 문제 없어서 version = 2 로 업데이트
Thread 2 > 커밋 시점에서 version이 1이어야되는데 version이 2인 걸 확인 -> Retry
-> 버전 값 새로 가져와서 캐시 저장
@Retryable(maxAttempts = 10, backoff = @Backoff(delay = 1000))
public TicketResponseDto create(TicketRequestDto ticketRequestDto) {
if (!ticketRequestDto.isValid()) {
throw new RuntimeException("Invalid ticket request");
}
return TicketResponseDto.from(LockCreate(ticketRequestDto));
}
@Transactional
public Ticket LockCreate(TicketRequestDto ticketRequestDto) {
Concert concert = concertRepository.findById(ticketRequestDto.concertId()).orElseThrow(
() -> new RuntimeException("Cannot find concert id: " + ticketRequestDto.concertId()));
if (concert.getAvailableAmount() <= 0) {
throw new RuntimeException("Cannot sell ticket. Available amount is less than 0.");
}
User user = userRepository.findById(ticketRequestDto.userId()).orElseThrow(
() -> new RuntimeException("Cannot find user id: " + ticketRequestDto.userId()));
concert.sellTicket();
concertRepository.save(concert);
Ticket ticket = new Ticket(user, concert);
ticket = ticketRepository.save(ticket);
return ticket;
}
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Concert extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String name;
private int totalAmount;
private int availableAmount;
@Version
private int version;
@Builder
public Concert(String name, int availableAmount) {
this.name = name;
this.availableAmount = availableAmount;
this.totalAmount = availableAmount;
}
public int refundTicket() {
this.availableAmount += 1;
return this.availableAmount;
}
public void sellTicket() throws ObjectOptimisticLockingFailureException {
if (this.availableAmount <= 0) {
throw new OptimisticLockException("No available tickets left!");
}
this.availableAmount -= 1;
}
}