diff --git a/app/src/main/java/com/picketing/www/business/domain/reservation/ReservationFactory.java b/app/src/main/java/com/picketing/www/business/domain/reservation/ReservationFactory.java index dd254c5f..73f0bfc1 100644 --- a/app/src/main/java/com/picketing/www/business/domain/reservation/ReservationFactory.java +++ b/app/src/main/java/com/picketing/www/business/domain/reservation/ReservationFactory.java @@ -8,6 +8,7 @@ import org.springframework.stereotype.Component; import com.picketing.www.business.domain.User; +import com.picketing.www.business.domain.show.seat.SeatGrade; import com.picketing.www.presentation.dto.response.reservation.MakeReservationResponse; @Component @@ -35,4 +36,8 @@ public Reservation convertSeatToReservation(User user, ScheduledShowSeat showSea .showSeat(showSeat) .build(); } + + public SeatGrade convertShowSeatGradeByReservation(Reservation reservation) { + return reservation.getShowSeat().getSeatGrade(); + } } diff --git a/app/src/main/java/com/picketing/www/business/service/reservation/ReservationService.java b/app/src/main/java/com/picketing/www/business/service/reservation/ReservationService.java index d62c2ae1..b3024065 100644 --- a/app/src/main/java/com/picketing/www/business/service/reservation/ReservationService.java +++ b/app/src/main/java/com/picketing/www/business/service/reservation/ReservationService.java @@ -4,10 +4,15 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import java.util.stream.IntStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import com.picketing.www.application.exception.CustomException; import com.picketing.www.application.exception.ErrorCode; @@ -21,9 +26,10 @@ import com.picketing.www.persistence.repository.reservation.ReservationRepository; import com.picketing.www.presentation.dto.request.reservation.ReservationRequest; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +@Slf4j @Service @RequiredArgsConstructor public class ReservationService { @@ -36,41 +42,59 @@ public class ReservationService { private final ReservationFactory reservationFactory; + private final Logger logger = LoggerFactory.getLogger(ReservationService.class); + + @Transactional public List makeReservations(Show show, ReservationRequest request) { User user = userService.get(request.userId()); LocalDateTime showTime = request.showTime(); - List seats = request.seatGradeList(); + List seats = request.reservationSeatRequests(); - return makeReservations(user, show, showTime, seats); + return makeReservations(user, show, showTime, seats); } @Transactional - public List makeReservations(User user, Show show, LocalDateTime showTime, List seats ) { - - if (!isBookable(show, showTime, seats)) { - throw new CustomException(ErrorCode.ALREADY_RESERVED); - } - - List reservations = seats - .stream() - .flatMap(seatRequest -> makeReservationPerCount(user, show, showTime, seatRequest).stream()) - .collect(Collectors.toList()); + public List makeReservations(User user, Show show, LocalDateTime showTime, + List seats) { + + // 예약 정보를 가지고 오고 -> 추가하는 과정에서 동시성 문제가 발생 + List> reservationList = seats.stream() + .map( + seat -> reservationRepository.findByShowSeatWithPessimisticLock( + scheduledShowSeatService.getScheduledShowSeat(show, showTime, seat.seatGrade())) + ).toList(); + + Map reservedSeatCountMap = new ConcurrentHashMap<>(); + reservationList.forEach((reservation -> { + Reservation current = reservation.get(0); + SeatGrade currentSeat = reservationFactory.convertShowSeatGradeByReservation(current); + int reservedCount = reservation.size(); + reservedSeatCountMap.put(currentSeat, reservedCount); + })); + + if (!isBookable(reservedSeatCountMap, seats)) { + throw new CustomException(ErrorCode.ALREADY_RESERVED); + } + + List reservations = seats + .stream() + .flatMap(seatRequest -> makeReservationPerCount(user, show, showTime, seatRequest).stream()) + .collect(Collectors.toList()); - return reservationRepository.saveAll(reservations); + return reservationRepository.saveAll(reservations); } - private boolean isBookable(Show show, LocalDateTime showTime, List seatRequestList) { + private boolean isBookable(Map map, List seatRequestList) { return seatRequestList.stream() .allMatch(seatRequest -> { SeatGrade currentSeatGrade = seatRequest.seatGrade(); - long reservedCount = countReservationsByShowSeat( - scheduledShowSeatService.getScheduledShowSeat(show, showTime, - currentSeatGrade)); + Integer purchaseCount = seatRequest.count(); + Integer reservedCount = map.get(currentSeatGrade); - return ((currentSeatGrade.getCount() - reservedCount) - seatRequest.count()) >= 0; + return ((currentSeatGrade.getCount() - reservedCount) - purchaseCount >= 0); }); } diff --git a/app/src/main/java/com/picketing/www/persistence/repository/reservation/ReservationRepository.java b/app/src/main/java/com/picketing/www/persistence/repository/reservation/ReservationRepository.java index 3b648ab2..1ea2bac1 100644 --- a/app/src/main/java/com/picketing/www/persistence/repository/reservation/ReservationRepository.java +++ b/app/src/main/java/com/picketing/www/persistence/repository/reservation/ReservationRepository.java @@ -3,15 +3,31 @@ import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.QueryHints; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import com.picketing.www.business.domain.reservation.Reservation; import com.picketing.www.business.domain.reservation.ScheduledShowSeat; +import jakarta.persistence.LockModeType; +import jakarta.persistence.QueryHint; + @Repository public interface ReservationRepository extends JpaRepository { Long countReservationsByShowSeat(ScheduledShowSeat scheduledShowSeat); List findAllByShowSeat(ScheduledShowSeat scheduledShowSeat); + + // 예약 정보 조회 시 베타락을 건다 + @Query(value = "select r from Reservation r where r.showSeat = :scheduledShowSeat") + @Lock(LockModeType.PESSIMISTIC_WRITE) + @QueryHints({ + @QueryHint(name = "jakarta.persistence.lock.timeout", value = "1000") + }) + List findByShowSeatWithPessimisticLock( + @Param("scheduledShowSeat") ScheduledShowSeat scheduledShowSeat); } diff --git a/app/src/main/java/com/picketing/www/presentation/controller/show/ShowController.java b/app/src/main/java/com/picketing/www/presentation/controller/show/ShowController.java index c7de6d86..d47a4fca 100644 --- a/app/src/main/java/com/picketing/www/presentation/controller/show/ShowController.java +++ b/app/src/main/java/com/picketing/www/presentation/controller/show/ShowController.java @@ -64,7 +64,7 @@ public Page getShowListWithPagination( public RemainingSeatsResponse getRemainingSeatCountsByShowAndTime( @PathVariable Long showId, @RequestParam(value = "showTime", required = true) - @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") LocalDateTime showTime + @DateTimeFormat(pattern = "yyyy-MM-ddTHH:mm") LocalDateTime showTime ) { return seatGradeFactory.convertRemainingSeats( showService.getRemainingSeats(showId, showTime) diff --git a/app/src/main/java/com/picketing/www/presentation/dto/request/reservation/ReservationRequest.java b/app/src/main/java/com/picketing/www/presentation/dto/request/reservation/ReservationRequest.java index 5b712c06..a46210ca 100644 --- a/app/src/main/java/com/picketing/www/presentation/dto/request/reservation/ReservationRequest.java +++ b/app/src/main/java/com/picketing/www/presentation/dto/request/reservation/ReservationRequest.java @@ -19,9 +19,9 @@ public record ReservationRequest( @Positive Long userId, - @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") + @DateTimeFormat(pattern = "yyyy-MM-ddTHH:mm") LocalDateTime showTime, - List seatGradeList + List reservationSeatRequests ) { @Builder