From cf38263801bfd02450a2fff949dcdac5f5e8c6ed Mon Sep 17 00:00:00 2001 From: yeGenieee Date: Mon, 30 Oct 2023 20:10:03 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=EB=B9=84=EA=B4=80=EC=A0=81=20=EB=9D=BD=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reservation/ReservationFactory.java | 5 ++ .../reservation/ReservationService.java | 61 +++++++++++++------ .../reservation/ReservationRepository.java | 16 +++++ 3 files changed, 63 insertions(+), 19 deletions(-) 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..fe5b565d 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,58 @@ public class ReservationService { private final ReservationFactory reservationFactory; + private final Logger logger = LoggerFactory.getLogger(ReservationService.class); + public List makeReservations(Show show, ReservationRequest request) { User user = userService.get(request.userId()); LocalDateTime showTime = request.showTime(); - List seats = request.seatGradeList(); + List seats = request.seatGradeList(); - 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..42bdcdb6 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 = "javax.persistence.lock.timeout", value = "1000") + }) + List findByShowSeatWithPessimisticLock( + @Param("scheduledShowSeat") ScheduledShowSeat scheduledShowSeat); } From 00ee0593523d93543a2482493a0f1b501db916fe Mon Sep 17 00:00:00 2001 From: yeGenieee Date: Wed, 1 Nov 2023 00:33:15 +0900 Subject: [PATCH 2/2] =?UTF-8?q?API=20=EB=B2=84=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(LocalDateTime=20field=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../www/business/service/reservation/ReservationService.java | 3 ++- .../repository/reservation/ReservationRepository.java | 2 +- .../www/presentation/controller/show/ShowController.java | 2 +- .../dto/request/reservation/ReservationRequest.java | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) 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 fe5b565d..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 @@ -44,13 +44,14 @@ public class ReservationService { 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); } 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 42bdcdb6..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 @@ -26,7 +26,7 @@ public interface ReservationRepository extends JpaRepository @Query(value = "select r from Reservation r where r.showSeat = :scheduledShowSeat") @Lock(LockModeType.PESSIMISTIC_WRITE) @QueryHints({ - @QueryHint(name = "javax.persistence.lock.timeout", value = "1000") + @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