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;
    }


}