Skip to content

[BUG]: 예약/결제 동시성 취약점 수정 (레이스 컨디션 및 오버부킹) #157

@zerochani

Description

@zerochani

🐛 버그 설명

예약 생성, 결제 확인, 예약 취소 로직에서 동시에 여러 요청이 들어올 때 레이스 컨디션이 발생할 수 있습니다. 이로 인해 오버부킹, 확정된 예약의 자동 취소, 중복 환불 API 호출 등의 심각한 데이터 정합성 문제가 발생할 수 있습니다.


🔍 원인 분석

1. createBooking() — 비관적 락이 BookingTable을 보호하지 않음

파일: BookingCommandServiceImpl.java:73-86

StoreTablePESSIMISTIC_WRITE 락을 걸지만, 중복 예약 확인 쿼리(findReservedTableIds)는 Booking/BookingTable 테이블을 조회하며 이 테이블에는 락이 없습니다.
MySQL REPEATABLE_READ 스냅샷 타이밍에 따라, 다른 스레드가 커밋한 예약이 보이지 않아 동일 테이블/시간대에 이중 예약이 발생할 수 있습니다.

2. BookingScheduler — 확정된 예약을 취소할 수 있음

파일: BookingScheduler.java:27-46

  • jakarta.transaction.Transactional 오용 (Spring의 @Transactional이 아님)
  • 스케줄러가 PENDING 목록 조회 후, 결제 확인 스레드가 동시에 CONFIRMED로 변경하면 스케줄러가 해당 예약을 CANCELED로 덮어씌울 수 있음

3. confirmPayment() / cancelBooking() — Booking 락 없음

파일: BookingCommandServiceImpl.java:156, BookingCommandServiceImpl.java:185
파일: PaymentService.java:118-122

Booking 엔티티에 @Version 필드가 없어 두 경로(BookingCommandServiceImplPaymentService)에서 동시에 booking.confirm()을 호출하면 둘 다 커밋될 수 있습니다.

cancelBooking()은 상태 확인과 Toss 환불 API 호출 사이에 갭이 있어 중복 환불 호출이 가능합니다.


✅ 작업할 내용

🔴 CRITICAL — 즉시 수정

  • DB 유니크 제약 추가: booking_table 테이블에 (store_id, booking_date, booking_time, table_id) 유니크 제약 추가 — 애플리케이션 락 우회 시 최후 방어선
  • BookingScheduler.java:27 jakarta.transaction.Transactionalorg.springframework.transaction.annotation.Transactional 변경
  • BookingScheduler.java:44 취소 처리 전 상태 재확인 로직 추가 (PENDING 여부 검증)
  • Booking 엔티티에 @Version 추가 — 낙관적 락으로 동시 상태 변경 감지

🔴 HIGH — 단기 수정

  • BookingRepository에 락 메서드 추가: findByIdWithLock(Long id) (@Lock(PESSIMISTIC_WRITE)) 구현
  • BookingCommandServiceImpl.confirmPayment():156 findByIdfindByIdWithLock 교체
  • BookingCommandServiceImpl.cancelBooking():185 findByIdfindByIdWithLock 교체
  • BookingCommandServiceImpl.cancelBookingByOwner():223 동일하게 락 적용

🟡 MEDIUM — 추가 개선

  • PaymentService.confirmPayment() Toss 외부 API 호출을 트랜잭션 바깥으로 분리 (커넥션 풀 고갈 방지)
  • PaymentService.cancelPayment() 취소 전 Payment 상태 검증 및 멱등성 체크 추가
  • 각 수정 사항에 대한 단위 테스트 작성

🙋🏻 참고 자료

  • 분석 대상 파일:
    • domain/booking/service/BookingCommandServiceImpl.java
    • domain/booking/service/BookingScheduler.java
    • domain/booking/repository/BookingRepository.java
    • domain/payment/service/PaymentService.java
    • domain/payment/entity/Payment.java
  • MySQL InnoDB MVCC 및 REPEATABLE_READ 격리 수준 문서
  • Spring Data JPA @Lock 어노테이션 공식 문서

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions