From feec12c5d968c296f16ab5da3cc9be49096c5409 Mon Sep 17 00:00:00 2001 From: seminchoi Date: Thu, 15 Aug 2024 10:57:40 +0900 Subject: [PATCH 01/23] =?UTF-8?q?feat:=20=EB=82=99=EA=B4=80=EC=A0=81=20?= =?UTF-8?q?=EB=9D=BD=EC=9D=84=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EC=A2=8C=EC=84=9D=20=EC=84=A0=ED=83=9D=20=EA=B8=B0=EB=8A=A5=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 --- .../ticketing/domain/common/ErrorCode.java | 6 ++ .../ticketing/domain/seat/Seat.java | 13 +++++ .../ticket/dto/SeatSelectionRequest.java | 2 +- .../service/PersistenceTicketService.java | 55 ++++++++++++++++++- .../domain/ticket/service/TicketService.java | 13 ++--- 5 files changed, 79 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/thirdparty/ticketing/domain/common/ErrorCode.java b/src/main/java/com/thirdparty/ticketing/domain/common/ErrorCode.java index d3bcb61f..a99ba3f5 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/common/ErrorCode.java +++ b/src/main/java/com/thirdparty/ticketing/domain/common/ErrorCode.java @@ -42,6 +42,12 @@ public enum ErrorCode { */ NOT_FOUND_SEAT_GRADE(HttpStatus.NOT_FOUND, "SG404-1", "존재하지 않는 구역입니다."), + /* + Seat Error + */ + NOT_FOUND_SEAT(HttpStatus.NOT_FOUND, "S404-1", "존재하지 않는 좌석입니다."), + NOT_SELECTABLE_SEAT(HttpStatus.FORBIDDEN, "S403-1", "이미 선택된 좌석입니다."); + /* Payment Error */ diff --git a/src/main/java/com/thirdparty/ticketing/domain/seat/Seat.java b/src/main/java/com/thirdparty/ticketing/domain/seat/Seat.java index 67f1ae66..781eb166 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/seat/Seat.java +++ b/src/main/java/com/thirdparty/ticketing/domain/seat/Seat.java @@ -3,6 +3,8 @@ import jakarta.persistence.*; import com.thirdparty.ticketing.domain.BaseEntity; +import com.thirdparty.ticketing.domain.common.ErrorCode; +import com.thirdparty.ticketing.domain.common.TicketingException; import com.thirdparty.ticketing.domain.member.Member; import com.thirdparty.ticketing.domain.zone.Zone; @@ -43,6 +45,9 @@ public class Seat extends BaseEntity { @Column(length = 16, nullable = false) private SeatStatus seatStatus = SeatStatus.SELECTABLE; + @Version + private Long version; + public Seat(String seatCode, SeatStatus seatStatus) { this.seatCode = seatCode; this.seatStatus = seatStatus; @@ -51,4 +56,12 @@ public Seat(String seatCode, SeatStatus seatStatus) { public boolean isSelectable() { return seatStatus.isSelectable(); } + + public void assignByMember(Member member) { + if (!isSelectable()) { + throw new TicketingException(ErrorCode.NOT_SELECTABLE_SEAT); + } + this.member = member; + this.seatStatus = SeatStatus.SELECTED; + } } diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/dto/SeatSelectionRequest.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/dto/SeatSelectionRequest.java index c4f6e2ce..91ca7d1d 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/ticket/dto/SeatSelectionRequest.java +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/dto/SeatSelectionRequest.java @@ -9,5 +9,5 @@ public class SeatSelectionRequest { @NotNull(message = "좌석 ID를 요청하지 않았습니다.") @Min(value = 1, message = "좌석 ID는 1 이상이어야 합니다.") - private Long seatId; + private final Long seatId; } diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/PersistenceTicketService.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/PersistenceTicketService.java index 820bfec6..d45b0afd 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/PersistenceTicketService.java +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/PersistenceTicketService.java @@ -1,16 +1,29 @@ package com.thirdparty.ticketing.domain.ticket.service; +import jakarta.persistence.OptimisticLockException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; +import com.thirdparty.ticketing.domain.common.ErrorCode; +import com.thirdparty.ticketing.domain.common.TicketingException; +import com.thirdparty.ticketing.domain.member.Member; import com.thirdparty.ticketing.domain.member.repository.MemberRepository; +import com.thirdparty.ticketing.domain.seat.Seat; import com.thirdparty.ticketing.domain.payment.PaymentProcessor; import com.thirdparty.ticketing.domain.seat.repository.SeatRepository; import com.thirdparty.ticketing.domain.ticket.dto.SeatSelectionRequest; import com.thirdparty.ticketing.domain.ticket.dto.TicketPaymentRequest; import com.thirdparty.ticketing.domain.ticket.repository.TicketRepository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; +import java.util.logging.Logger; @Service public class PersistenceTicketService extends TicketService { + private static final Logger log = LoggerFactory.getLogger(PersistenceTicketService.class); + public PersistenceTicketService( MemberRepository memberRepository, TicketRepository ticketRepository, @@ -20,8 +33,46 @@ public PersistenceTicketService( } @Override - public void selectSeat(SeatSelectionRequest seatSelectionRequest) {} + @Transactional + public void selectSeat(SeatSelectionRequest seatSelectionRequest) { + try { + Long seatId = seatSelectionRequest.getSeatId(); + Optional byId = seatRepository.findById(seatId); + Seat seat = byId.orElseThrow(() -> new TicketingException(ErrorCode.NOT_FOUND_SEAT)); + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = (String) authentication.getPrincipal(); + + Member member = + memberRepository + .findByEmail(email) + .orElseThrow(() -> new TicketingException(ErrorCode.NOT_FOUND_MEMBER)); + + seat.assignByMember(member); + } catch (OptimisticLockException e) { + log.error("optimistic lock exception", e); + } + } @Override - public void reservationTicket(TicketPaymentRequest ticketPaymentRequest) {} + @Transactional + public void reservationTicket(TicketPaymentRequest ticketPaymentRequest) { + Long seatId = ticketPaymentRequest.getSeatId(); + Seat seat = + seatRepository + .findById(seatId) + .orElseThrow(() -> new TicketingException(ErrorCode.NOT_FOUND_SEAT)); + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = (String) authentication.getPrincipal(); + + Member loginMember = + memberRepository + .findByEmail(email) + .orElseThrow(() -> new TicketingException(ErrorCode.NOT_FOUND_MEMBER)); + + if (!seat.getMember().getMemberId().equals(loginMember.getMemberId())) { + throw new TicketingException(ErrorCode.NOT_SELECTABLE_SEAT); + } + } } diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/TicketService.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/TicketService.java index 9db29be7..2c8393c0 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/TicketService.java +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/TicketService.java @@ -1,7 +1,5 @@ package com.thirdparty.ticketing.domain.ticket.service; -import java.util.List; - import com.thirdparty.ticketing.domain.ItemResult; import com.thirdparty.ticketing.domain.common.ErrorCode; import com.thirdparty.ticketing.domain.common.TicketingException; @@ -13,15 +11,16 @@ import com.thirdparty.ticketing.domain.ticket.dto.TicketElement; import com.thirdparty.ticketing.domain.ticket.dto.TicketPaymentRequest; import com.thirdparty.ticketing.domain.ticket.repository.TicketRepository; - import lombok.RequiredArgsConstructor; +import java.util.List; + @RequiredArgsConstructor public abstract class TicketService { - private final MemberRepository memberRepository; - private final TicketRepository ticketRepository; - private final SeatRepository seatRepository; - private final PaymentProcessor paymentProcessor; + protected final MemberRepository memberRepository; + protected final TicketRepository ticketRepository; + protected final SeatRepository seatRepository; + protected final PaymentProcessor paymentProcessor; public ItemResult selectMyTicket(String memberEmail) { Member member = From 9d4a03c7f31046edc5fcf1b70b7da45f38bb605d Mon Sep 17 00:00:00 2001 From: seminchoi Date: Thu, 15 Aug 2024 21:21:04 +0900 Subject: [PATCH 02/23] =?UTF-8?q?refactor:=20=ED=8B=B0=EC=BC=93=20?= =?UTF-8?q?=EC=98=88=EB=A7=A4=20=EA=B4=80=EB=A0=A8=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자 검증이 필요하므로 LoginMember 정보를 메소드 인자에 추가 --- .../domain/ticket/controller/TicketController.java | 6 ++++-- .../domain/ticket/service/CacheTicketService.java | 6 ++++-- .../ticketing/domain/ticket/service/TicketService.java | 10 ++++++---- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/controller/TicketController.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/controller/TicketController.java index 37b40eca..6eeff078 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/ticket/controller/TicketController.java +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/controller/TicketController.java @@ -31,15 +31,17 @@ public ResponseEntity> selectMyTickets( @PostMapping("/seats/select") public ResponseEntity selectSeat( + @LoginMember String memberEmail, @RequestBody @Valid SeatSelectionRequest seatSelectionRequest) { - ticketService.selectSeat(seatSelectionRequest); + ticketService.selectSeat(memberEmail, seatSelectionRequest); return ResponseEntity.ok().build(); } @PostMapping("/tickets") public ResponseEntity payTicket( + @LoginMember String memberEmail, @RequestBody @Valid TicketPaymentRequest ticketPaymentRequest) { - ticketService.reservationTicket(ticketPaymentRequest); + ticketService.reservationTicket(memberEmail, ticketPaymentRequest); return ResponseEntity.ok().build(); } } diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/CacheTicketService.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/CacheTicketService.java index 01b721be..262069f0 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/CacheTicketService.java +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/CacheTicketService.java @@ -17,8 +17,10 @@ public CacheTicketService( } @Override - public void selectSeat(SeatSelectionRequest seatSelectionRequest) {} + public void selectSeat(String memberEmail, SeatSelectionRequest seatSelectionRequest) { + } @Override - public void reservationTicket(TicketPaymentRequest ticketPaymentRequest) {} + public void reservationTicket(String memberEmail, TicketPaymentRequest ticketPaymentRequest) { + } } diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/TicketService.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/TicketService.java index 2c8393c0..ddf7d1c4 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/TicketService.java +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/TicketService.java @@ -1,5 +1,7 @@ package com.thirdparty.ticketing.domain.ticket.service; +import java.util.List; + import com.thirdparty.ticketing.domain.ItemResult; import com.thirdparty.ticketing.domain.common.ErrorCode; import com.thirdparty.ticketing.domain.common.TicketingException; @@ -11,9 +13,8 @@ import com.thirdparty.ticketing.domain.ticket.dto.TicketElement; import com.thirdparty.ticketing.domain.ticket.dto.TicketPaymentRequest; import com.thirdparty.ticketing.domain.ticket.repository.TicketRepository; -import lombok.RequiredArgsConstructor; -import java.util.List; +import lombok.RequiredArgsConstructor; @RequiredArgsConstructor public abstract class TicketService { @@ -34,7 +35,8 @@ public ItemResult selectMyTicket(String memberEmail) { return ItemResult.of(tickets); } - public abstract void selectSeat(SeatSelectionRequest seatSelectionRequest); + public abstract void selectSeat(String memberEmail, SeatSelectionRequest seatSelectionRequest); - public abstract void reservationTicket(TicketPaymentRequest ticketPaymentRequest); + public abstract void reservationTicket( + String memberEmail, TicketPaymentRequest ticketPaymentRequest); } From 246af136f09f08887e6aef5ce6f5acff35dbba2f Mon Sep 17 00:00:00 2001 From: seminchoi Date: Thu, 15 Aug 2024 21:23:56 +0900 Subject: [PATCH 03/23] =?UTF-8?q?feat:=20=EC=A2=8C=EC=84=9D=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EB=A5=BC=20=EC=A1=B0=ED=9A=8C=ED=95=A0=20=EB=95=8C=20?= =?UTF-8?q?=EC=A0=84=EB=9E=B5=EC=9D=84=20=EC=84=A0=ED=83=9D=ED=95=A0=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8A=94=20=EC=A0=84=EB=9E=B5=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=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 --- .../ticketing/domain/ticket/policy/LockSeatStrategy.java | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/main/java/com/thirdparty/ticketing/domain/ticket/policy/LockSeatStrategy.java diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/policy/LockSeatStrategy.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/policy/LockSeatStrategy.java new file mode 100644 index 00000000..1f3914be --- /dev/null +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/policy/LockSeatStrategy.java @@ -0,0 +1,9 @@ +package com.thirdparty.ticketing.domain.ticket.policy; + +import java.util.Optional; + +import com.thirdparty.ticketing.domain.seat.Seat; + +public interface LockSeatStrategy { + Optional getSeatWithLock(Long seatId); +} From 6adf6f14fff5847e363f2603bc725f639613603b Mon Sep 17 00:00:00 2001 From: seminchoi Date: Thu, 15 Aug 2024 21:24:15 +0900 Subject: [PATCH 04/23] =?UTF-8?q?feat:=20=EC=A2=8C=EC=84=9D=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EB=A5=BC=20=EC=A1=B0=ED=9A=8C=ED=95=A0=20=EB=95=8C=20?= =?UTF-8?q?=EB=82=99=EA=B4=80=EB=9D=BD,=20=EB=B9=84=EA=B4=80=EB=9D=BD?= =?UTF-8?q?=EC=9D=84=20=EC=82=AC=EC=9A=A9=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20=EC=A0=84=EB=9E=B5=20=ED=8C=A8=ED=84=B4=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ticketing/domain/seat/Seat.java | 4 ++++ .../seat/repository/SeatRepository.java | 13 +++++++++++ .../policy/OptimisticLockSeatStrategy.java | 23 +++++++++++++++++++ .../policy/PessimisticLockSeatStrategy.java | 20 ++++++++++++++++ 4 files changed, 60 insertions(+) create mode 100644 src/main/java/com/thirdparty/ticketing/domain/ticket/policy/OptimisticLockSeatStrategy.java create mode 100644 src/main/java/com/thirdparty/ticketing/domain/ticket/policy/PessimisticLockSeatStrategy.java diff --git a/src/main/java/com/thirdparty/ticketing/domain/seat/Seat.java b/src/main/java/com/thirdparty/ticketing/domain/seat/Seat.java index 781eb166..c59fe761 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/seat/Seat.java +++ b/src/main/java/com/thirdparty/ticketing/domain/seat/Seat.java @@ -64,4 +64,8 @@ public void assignByMember(Member member) { this.member = member; this.seatStatus = SeatStatus.SELECTED; } + + public void updateStatus(SeatStatus seatStatus) { + this.seatStatus = seatStatus; + } } diff --git a/src/main/java/com/thirdparty/ticketing/domain/seat/repository/SeatRepository.java b/src/main/java/com/thirdparty/ticketing/domain/seat/repository/SeatRepository.java index 5a24b9dc..3ca6969c 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/seat/repository/SeatRepository.java +++ b/src/main/java/com/thirdparty/ticketing/domain/seat/repository/SeatRepository.java @@ -1,8 +1,13 @@ package com.thirdparty.ticketing.domain.seat.repository; import java.util.List; +import java.util.Optional; + +import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import com.thirdparty.ticketing.domain.seat.Seat; @@ -11,4 +16,12 @@ @Repository public interface SeatRepository extends JpaRepository { List findByZone(Zone zone); + + @Query("SELECT s FROM Seat as s WHERE s.id = :seatId") + @Lock(LockModeType.OPTIMISTIC) + Optional findByIdWithOptimistic(Long seatId); + + @Query("SELECT s FROM Seat as s WHERE s.id = :seatId") + @Lock(LockModeType.PESSIMISTIC_WRITE) + Optional findByIdWithPessimistic(Long seatId); } diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/policy/OptimisticLockSeatStrategy.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/policy/OptimisticLockSeatStrategy.java new file mode 100644 index 00000000..0ef98b8d --- /dev/null +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/policy/OptimisticLockSeatStrategy.java @@ -0,0 +1,23 @@ +package com.thirdparty.ticketing.domain.ticket.policy; + +import java.util.Optional; + +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +import com.thirdparty.ticketing.domain.seat.Seat; +import com.thirdparty.ticketing.domain.seat.repository.SeatRepository; + +import lombok.RequiredArgsConstructor; + +@Primary +@Component +@RequiredArgsConstructor +public class OptimisticLockSeatStrategy implements LockSeatStrategy { + private final SeatRepository seatRepository; + + @Override + public Optional getSeatWithLock(Long seatId) { + return seatRepository.findByIdWithOptimistic(seatId); + } +} diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/policy/PessimisticLockSeatStrategy.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/policy/PessimisticLockSeatStrategy.java new file mode 100644 index 00000000..4c9827a8 --- /dev/null +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/policy/PessimisticLockSeatStrategy.java @@ -0,0 +1,20 @@ +package com.thirdparty.ticketing.domain.ticket.policy; + +import java.util.Optional; + +import com.thirdparty.ticketing.domain.seat.Seat; +import com.thirdparty.ticketing.domain.seat.repository.SeatRepository; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class PessimisticLockSeatStrategy implements LockSeatStrategy { + private final SeatRepository seatRepository; + + @Override + public Optional getSeatWithLock(Long seatId) { + return seatRepository.findByIdWithPessimistic(seatId); + } +} From 5c72253201b6a88589358170dde20cb4eadd749c Mon Sep 17 00:00:00 2001 From: seminchoi Date: Thu, 15 Aug 2024 21:26:33 +0900 Subject: [PATCH 05/23] =?UTF-8?q?fix:=20ErrorCode=20=EC=BB=B4=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/thirdparty/ticketing/domain/common/ErrorCode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/thirdparty/ticketing/domain/common/ErrorCode.java b/src/main/java/com/thirdparty/ticketing/domain/common/ErrorCode.java index a99ba3f5..c00edc04 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/common/ErrorCode.java +++ b/src/main/java/com/thirdparty/ticketing/domain/common/ErrorCode.java @@ -46,7 +46,7 @@ public enum ErrorCode { Seat Error */ NOT_FOUND_SEAT(HttpStatus.NOT_FOUND, "S404-1", "존재하지 않는 좌석입니다."), - NOT_SELECTABLE_SEAT(HttpStatus.FORBIDDEN, "S403-1", "이미 선택된 좌석입니다."); + NOT_SELECTABLE_SEAT(HttpStatus.FORBIDDEN, "S403-1", "이미 선택된 좌석입니다."), /* Payment Error From 20a320d1e66985e3c4a8189f0229c53f2ec11d5d Mon Sep 17 00:00:00 2001 From: seminchoi Date: Thu, 15 Aug 2024 21:28:27 +0900 Subject: [PATCH 06/23] =?UTF-8?q?feat:=20=ED=8B=B0=EC=BC=93=20=EC=98=88?= =?UTF-8?q?=EB=A7=A4=EC=8B=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A0=95?= =?UTF-8?q?=ED=95=A9=EC=84=B1=EC=9D=B4=20=EC=9C=A0=EC=A7=80=EB=90=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20DB=20Locking=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/PersistenceTicketService.java | 54 +++++++++++-------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/PersistenceTicketService.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/PersistenceTicketService.java index d45b0afd..f209345d 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/PersistenceTicketService.java +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/PersistenceTicketService.java @@ -1,76 +1,84 @@ package com.thirdparty.ticketing.domain.ticket.service; import jakarta.persistence.OptimisticLockException; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; + import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import com.thirdparty.ticketing.domain.common.ErrorCode; import com.thirdparty.ticketing.domain.common.TicketingException; import com.thirdparty.ticketing.domain.member.Member; import com.thirdparty.ticketing.domain.member.repository.MemberRepository; -import com.thirdparty.ticketing.domain.seat.Seat; import com.thirdparty.ticketing.domain.payment.PaymentProcessor; +import com.thirdparty.ticketing.domain.payment.dto.PaymentRequest; +import com.thirdparty.ticketing.domain.seat.Seat; +import com.thirdparty.ticketing.domain.seat.SeatStatus; import com.thirdparty.ticketing.domain.seat.repository.SeatRepository; import com.thirdparty.ticketing.domain.ticket.dto.SeatSelectionRequest; import com.thirdparty.ticketing.domain.ticket.dto.TicketPaymentRequest; +import com.thirdparty.ticketing.domain.ticket.policy.LockSeatStrategy; import com.thirdparty.ticketing.domain.ticket.repository.TicketRepository; -import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; -import java.util.logging.Logger; +import lombok.extern.slf4j.Slf4j; @Service +@Slf4j public class PersistenceTicketService extends TicketService { - private static final Logger log = LoggerFactory.getLogger(PersistenceTicketService.class); + private final LockSeatStrategy lockSeatStrategy; public PersistenceTicketService( MemberRepository memberRepository, TicketRepository ticketRepository, SeatRepository seatRepository, - PaymentProcessor paymentProcessor) { + PaymentProcessor paymentProcessor, + LockSeatStrategy lockSeatStrategy) { super(memberRepository, ticketRepository, seatRepository, paymentProcessor); + this.lockSeatStrategy = lockSeatStrategy; } @Override @Transactional - public void selectSeat(SeatSelectionRequest seatSelectionRequest) { + public void selectSeat(String memberEmail, SeatSelectionRequest seatSelectionRequest) { try { Long seatId = seatSelectionRequest.getSeatId(); - Optional byId = seatRepository.findById(seatId); - Seat seat = byId.orElseThrow(() -> new TicketingException(ErrorCode.NOT_FOUND_SEAT)); - - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String email = (String) authentication.getPrincipal(); + Seat seat = + lockSeatStrategy + .getSeatWithLock(seatId) + .orElseThrow(() -> new TicketingException(ErrorCode.NOT_FOUND_SEAT)); Member member = memberRepository - .findByEmail(email) + .findByEmail(memberEmail) .orElseThrow(() -> new TicketingException(ErrorCode.NOT_FOUND_MEMBER)); seat.assignByMember(member); } catch (OptimisticLockException e) { - log.error("optimistic lock exception", e); + throw new TicketingException(ErrorCode.NOT_SELECTABLE_SEAT); } } @Override @Transactional - public void reservationTicket(TicketPaymentRequest ticketPaymentRequest) { + public void reservationTicket(String memberEmail, TicketPaymentRequest ticketPaymentRequest) { Long seatId = ticketPaymentRequest.getSeatId(); Seat seat = - seatRepository - .findById(seatId) + lockSeatStrategy + .getSeatWithLock(seatId) .orElseThrow(() -> new TicketingException(ErrorCode.NOT_FOUND_SEAT)); - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String email = (String) authentication.getPrincipal(); - Member loginMember = memberRepository - .findByEmail(email) + .findByEmail(memberEmail) .orElseThrow(() -> new TicketingException(ErrorCode.NOT_FOUND_MEMBER)); + if (!seat.getSeatStatus().equals(SeatStatus.SELECTED)) { + seat.updateStatus(SeatStatus.PENDING_PAYMENT); + } + paymentProcessor.processPayment(new PaymentRequest()); + if (!seat.getSeatStatus().equals(SeatStatus.PAID)) { + seat.updateStatus(SeatStatus.PAID); + } + if (!seat.getMember().getMemberId().equals(loginMember.getMemberId())) { throw new TicketingException(ErrorCode.NOT_SELECTABLE_SEAT); } From 57c8720ed7fa27634d01a3244d3cc9a6487f2af4 Mon Sep 17 00:00:00 2001 From: seminchoi Date: Thu, 15 Aug 2024 21:28:42 +0900 Subject: [PATCH 07/23] =?UTF-8?q?refactor:=20DTO=20final=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ticketing/domain/ticket/dto/TicketPaymentRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/dto/TicketPaymentRequest.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/dto/TicketPaymentRequest.java index b812c0e0..1ca75a61 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/ticket/dto/TicketPaymentRequest.java +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/dto/TicketPaymentRequest.java @@ -9,5 +9,5 @@ public class TicketPaymentRequest { @NotNull(message = "좌석 ID를 요청하지 않았습니다.") @Min(value = 1, message = "좌석 ID는 1 이상이어야 합니다.") - private Long seatId; + private final Long seatId; } From d7ef2c3a8fbc3c1738b25a7813b487ad62e8cf58 Mon Sep 17 00:00:00 2001 From: seminchoi Date: Thu, 15 Aug 2024 21:29:03 +0900 Subject: [PATCH 08/23] =?UTF-8?q?feat:=20=ED=8B=B0=EC=BC=93=20=EC=98=88?= =?UTF-8?q?=EB=A7=A4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20sql=20=ED=8C=8C=EC=9D=BC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/resources/db/reservation-test.sql | 31 ++++++++++++++++++++++ src/test/resources/db/select-seat-test.sql | 31 ++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 src/test/resources/db/reservation-test.sql create mode 100644 src/test/resources/db/select-seat-test.sql diff --git a/src/test/resources/db/reservation-test.sql b/src/test/resources/db/reservation-test.sql new file mode 100644 index 00000000..b0cf148d --- /dev/null +++ b/src/test/resources/db/reservation-test.sql @@ -0,0 +1,31 @@ +DELETE +FROM seat; +DELETE +FROM seat_grade; +DELETE +FROM zone; +DELETE +FROM performance; +DELETE +FROM member; + +-- Member 테이블에 데이터 삽입 +INSERT INTO member (member_id, email, password, member_role, created_at, updated_at) +VALUES (1, 'test@gmail.com', 'testpassword', 'USER', NOW(), NOW()); + +-- Performance 테이블에 데이터 삽입 +INSERT INTO performance (performance_id, performance_name, performance_place, performance_showtime, created_at, + updated_at) +VALUES (1, '공연', '장소', '2024-08-23 14:30:00', NOW(), NOW()); + +-- Zone 테이블에 데이터 삽입 +INSERT INTO zone (zone_id, zone_name, performance_id, created_at, updated_at) +VALUES (1, 'VIP', 1, NOW(), NOW()); + +-- SeatGrade 테이블에 데이터 삽입 +INSERT INTO seat_grade (seat_grade_id, grade_name, price, performance_id, created_at, updated_at) +VALUES (1, 'Grade1', 10000, 1, NOW(), NOW()); + +-- Seat 테이블에 데이터 삽입 +INSERT INTO seat (seat_id, seat_code, seat_status, member_id, zone_id, seat_grade_id, version, created_at, updated_at) +VALUES (1, 'A01', 'SELECTABLE', 1, 1, 1, 0, NOW(), NOW()); diff --git a/src/test/resources/db/select-seat-test.sql b/src/test/resources/db/select-seat-test.sql new file mode 100644 index 00000000..b75ac6b1 --- /dev/null +++ b/src/test/resources/db/select-seat-test.sql @@ -0,0 +1,31 @@ +DELETE +FROM seat; +DELETE +FROM seat_grade; +DELETE +FROM zone; +DELETE +FROM performance; +DELETE +FROM member; + +-- Member 테이블에 데이터 삽입 +INSERT INTO member (member_id, email, password, member_role, created_at, updated_at) +VALUES (1, 'test@gmail.com', 'testpassword', 'USER', NOW(), NOW()); + +-- Performance 테이블에 데이터 삽입 +INSERT INTO performance (performance_id, performance_name, performance_place, performance_showtime, created_at, + updated_at) +VALUES (1, '공연', '장소', '2024-08-23 14:30:00', NOW(), NOW()); + +-- Zone 테이블에 데이터 삽입 +INSERT INTO zone (zone_id, zone_name, performance_id, created_at, updated_at) +VALUES (1, 'VIP', 1, NOW(), NOW()); + +-- SeatGrade 테이블에 데이터 삽입 +INSERT INTO seat_grade (seat_grade_id, grade_name, price, performance_id, created_at, updated_at) +VALUES (1, 'Grade1', 10000, 1, NOW(), NOW()); + +-- Seat 테이블에 데이터 삽입 +INSERT INTO seat (seat_id, seat_code, seat_status, zone_id, seat_grade_id, version, created_at, updated_at) +VALUES (1, 'A01', 'SELECTABLE', 1, 1, 0, NOW(), NOW()); From 8b00e9f6f07d3be921bd3986c50ec735b9a52512 Mon Sep 17 00:00:00 2001 From: seminchoi Date: Thu, 15 Aug 2024 21:29:33 +0900 Subject: [PATCH 09/23] =?UTF-8?q?test:=20=ED=8B=B0=EC=BC=93=20=EC=98=88?= =?UTF-8?q?=EB=A7=A4=20=EC=8B=9C=20=EC=A2=8C=EC=84=9D=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=ED=95=A0=20=EB=95=8C=20=EB=8F=99=EC=8B=9C=EC=84=B1=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ticket/PersistenceTicketServiceTest.java | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 src/test/java/com/thirdparty/ticketing/domain/ticket/PersistenceTicketServiceTest.java diff --git a/src/test/java/com/thirdparty/ticketing/domain/ticket/PersistenceTicketServiceTest.java b/src/test/java/com/thirdparty/ticketing/domain/ticket/PersistenceTicketServiceTest.java new file mode 100644 index 00000000..470d6a38 --- /dev/null +++ b/src/test/java/com/thirdparty/ticketing/domain/ticket/PersistenceTicketServiceTest.java @@ -0,0 +1,151 @@ +package com.thirdparty.ticketing.domain.ticket; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.context.annotation.Import; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlConfig; + +import com.thirdparty.ticketing.domain.member.repository.MemberRepository; +import com.thirdparty.ticketing.domain.payment.SimulatedPaymentProcessor; +import com.thirdparty.ticketing.domain.seat.repository.SeatRepository; +import com.thirdparty.ticketing.domain.ticket.dto.SeatSelectionRequest; +import com.thirdparty.ticketing.domain.ticket.policy.OptimisticLockSeatStrategy; +import com.thirdparty.ticketing.domain.ticket.repository.TicketRepository; +import com.thirdparty.ticketing.domain.ticket.service.PersistenceTicketService; +import com.thirdparty.ticketing.domain.ticket.service.TicketService; + +@DataJpaTest +@Import({ + PersistenceTicketService.class, + SimulatedPaymentProcessor.class, + OptimisticLockSeatStrategy.class +}) +public class PersistenceTicketServiceTest { + private static final Logger log = LoggerFactory.getLogger(PersistenceTicketServiceTest.class); + @Autowired + private TestEntityManager entityManager; + @Autowired + private DataSource dataSource; + @Autowired + private SeatRepository seatRepository; + @Autowired + private TicketRepository ticketRepository; + @Autowired + private MemberRepository memberRepository; + + @Autowired + private TicketService ticketService; + + private String memberEmail = "test@gmail.com"; + private Long seatId = 1L; + + @Nested + @DisplayName("좌석을 한 번에 여러개 선택할 때") + @Sql( + scripts = "/db/select-seat-test.sql", + config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)) + class SelectSeatConcurrencyTest { + + @Test + @DisplayName("다른 스레드에서 테스트 데이터를 볼 수 있는지 확인한다") + void select_otherThread() throws InterruptedException { + Long seatId = 1L; + + ExecutorService executor = Executors.newFixedThreadPool(2); + + executor.execute( + () -> { + seatRepository.findById(seatId).orElseThrow(); + }); + + executor.shutdown(); + executor.awaitTermination(10, TimeUnit.SECONDS); + } + + @Test + @DisplayName("여러개의 동시 요청 중 한 명만 좌석을 성공적으로 선택해야 한다.") + void selectSeat_ConcurrencyTest() throws InterruptedException { + // Given + int numRequests = 2000; + CountDownLatch latch = new CountDownLatch(1); + ExecutorService executor = Executors.newFixedThreadPool(numRequests); + + AtomicInteger successfulSelections = new AtomicInteger(0); + AtomicInteger failedSelections = new AtomicInteger(0); + + // when + IntStream.range(0, numRequests) + .forEach( + i -> + executor.submit( + () -> + selectSeatTask( + latch, + seatId, + successfulSelections, + failedSelections))); + + latch.countDown(); // 모든 스레드가 동시에 실행되도록 설정 + + executor.shutdown(); + executor.awaitTermination(10, TimeUnit.SECONDS); + + // Then + assertThat(successfulSelections.get()).isEqualTo(1); + assertThat(failedSelections.get()).isEqualTo(numRequests - 1); + } + + private void selectSeatTask( + CountDownLatch latch, + Long seatId, + AtomicInteger successfulSelections, + AtomicInteger failedSelections) { + + setUpAuthentication(); + try { + latch.await(); + try { + ticketService.selectSeat(memberEmail, new SeatSelectionRequest(seatId)); + successfulSelections.incrementAndGet(); + } catch (Exception e) { + log.error(e.getMessage(), e); + failedSelections.incrementAndGet(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private void setUpAuthentication() { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + Authentication authentication = + new UsernamePasswordAuthenticationToken( + "test@gmail.com", "testpassword", List.of()); + context.setAuthentication(authentication); + SecurityContextHolder.setContext(context); + } + } +} From e8ad16031ffa137393f712c87a1e5952350ead05 Mon Sep 17 00:00:00 2001 From: seminchoi Date: Thu, 15 Aug 2024 21:56:58 +0900 Subject: [PATCH 10/23] =?UTF-8?q?test:=20=ED=8B=B0=EC=BC=93=20=EC=98=88?= =?UTF-8?q?=EB=A7=A4=20=EC=8B=9C=20=EA=B2=B0=EC=A0=9C=20=EC=8B=9C=EB=8F=84?= =?UTF-8?q?=ED=95=A0=20=EB=95=8C=20=EB=8F=99=EC=8B=9C=EC=84=B1=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ticket/PersistenceTicketServiceTest.java | 55 ++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/thirdparty/ticketing/domain/ticket/PersistenceTicketServiceTest.java b/src/test/java/com/thirdparty/ticketing/domain/ticket/PersistenceTicketServiceTest.java index 470d6a38..62882b30 100644 --- a/src/test/java/com/thirdparty/ticketing/domain/ticket/PersistenceTicketServiceTest.java +++ b/src/test/java/com/thirdparty/ticketing/domain/ticket/PersistenceTicketServiceTest.java @@ -12,6 +12,7 @@ import javax.sql.DataSource; +import com.thirdparty.ticketing.domain.ticket.dto.TicketPaymentRequest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -63,11 +64,11 @@ public class PersistenceTicketServiceTest { private Long seatId = 1L; @Nested - @DisplayName("좌석을 한 번에 여러개 선택할 때") + @DisplayName("티켓 예매를 위해 좌석을 선택할 때") @Sql( scripts = "/db/select-seat-test.sql", config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)) - class SelectSeatConcurrencyTest { + class SelectSeatTest { @Test @DisplayName("다른 스레드에서 테스트 데이터를 볼 수 있는지 확인한다") @@ -148,4 +149,54 @@ private void setUpAuthentication() { SecurityContextHolder.setContext(context); } } + + @Nested + @DisplayName("티켓 예매 할 때 결제 시도 시") + @Sql(scripts = "/db/reservation-test.sql", config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)) + class reservationTicketTest { + + @Test + @DisplayName("동시에 여러 요청이 오면 하나의 요청만 성공한다.") + void reservationTicket_ConcurrencyTest() throws InterruptedException { + // Given + int numRequests = 100; + CountDownLatch latch = new CountDownLatch(1); + ExecutorService executor = Executors.newFixedThreadPool(numRequests); + + AtomicInteger successfulReservations = new AtomicInteger(0); + AtomicInteger failedReservations = new AtomicInteger(0); + + // When + IntStream.range(0, numRequests) + .forEach(i -> executor.submit(() -> { + try { + latch.await(); // 동기화된 시작 + reservationTicketTask(memberEmail, seatId, successfulReservations, failedReservations); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + })); + + latch.countDown(); // 모든 스레드가 동시에 실행되도록 설정 + + executor.shutdown(); + executor.awaitTermination(10, TimeUnit.SECONDS); + + // Then + assertThat(successfulReservations.get()).isEqualTo(1); + assertThat(failedReservations.get()).isEqualTo(numRequests - 1); + } + + private void reservationTicketTask(String memberEmail, Long seatId, + AtomicInteger successfulReservations, + AtomicInteger failedReservations) { + try { + ticketService.reservationTicket(memberEmail, new TicketPaymentRequest(seatId)); + successfulReservations.incrementAndGet(); + } catch (Exception e) { + log.error(e.getMessage(), e); + failedReservations.incrementAndGet(); + } + } + } } From b337f710013a904b9b27ffc3ffbfc8209f58e3a1 Mon Sep 17 00:00:00 2001 From: seminchoi Date: Thu, 15 Aug 2024 21:57:22 +0900 Subject: [PATCH 11/23] =?UTF-8?q?refactor:=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=ED=8F=AC=EB=A7=B7=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ticketing/domain/seat/Seat.java | 3 +- .../policy/PessimisticLockSeatStrategy.java | 3 +- .../ticket/service/CacheTicketService.java | 6 +- .../ticket/PersistenceTicketServiceTest.java | 64 +++++++++---------- 4 files changed, 35 insertions(+), 41 deletions(-) diff --git a/src/main/java/com/thirdparty/ticketing/domain/seat/Seat.java b/src/main/java/com/thirdparty/ticketing/domain/seat/Seat.java index c59fe761..225ed4ee 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/seat/Seat.java +++ b/src/main/java/com/thirdparty/ticketing/domain/seat/Seat.java @@ -45,8 +45,7 @@ public class Seat extends BaseEntity { @Column(length = 16, nullable = false) private SeatStatus seatStatus = SeatStatus.SELECTABLE; - @Version - private Long version; + @Version private Long version; public Seat(String seatCode, SeatStatus seatStatus) { this.seatCode = seatCode; diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/policy/PessimisticLockSeatStrategy.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/policy/PessimisticLockSeatStrategy.java index 4c9827a8..3a259fc8 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/ticket/policy/PessimisticLockSeatStrategy.java +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/policy/PessimisticLockSeatStrategy.java @@ -2,11 +2,12 @@ import java.util.Optional; +import org.springframework.stereotype.Component; + import com.thirdparty.ticketing.domain.seat.Seat; import com.thirdparty.ticketing.domain.seat.repository.SeatRepository; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; @Component @RequiredArgsConstructor diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/CacheTicketService.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/CacheTicketService.java index 262069f0..a6500a7f 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/CacheTicketService.java +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/CacheTicketService.java @@ -17,10 +17,8 @@ public CacheTicketService( } @Override - public void selectSeat(String memberEmail, SeatSelectionRequest seatSelectionRequest) { - } + public void selectSeat(String memberEmail, SeatSelectionRequest seatSelectionRequest) {} @Override - public void reservationTicket(String memberEmail, TicketPaymentRequest ticketPaymentRequest) { - } + public void reservationTicket(String memberEmail, TicketPaymentRequest ticketPaymentRequest) {} } diff --git a/src/test/java/com/thirdparty/ticketing/domain/ticket/PersistenceTicketServiceTest.java b/src/test/java/com/thirdparty/ticketing/domain/ticket/PersistenceTicketServiceTest.java index 62882b30..85876570 100644 --- a/src/test/java/com/thirdparty/ticketing/domain/ticket/PersistenceTicketServiceTest.java +++ b/src/test/java/com/thirdparty/ticketing/domain/ticket/PersistenceTicketServiceTest.java @@ -10,9 +10,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.IntStream; -import javax.sql.DataSource; - -import com.thirdparty.ticketing.domain.ticket.dto.TicketPaymentRequest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -20,7 +17,6 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; import org.springframework.context.annotation.Import; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -29,36 +25,25 @@ import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.SqlConfig; -import com.thirdparty.ticketing.domain.member.repository.MemberRepository; import com.thirdparty.ticketing.domain.payment.SimulatedPaymentProcessor; import com.thirdparty.ticketing.domain.seat.repository.SeatRepository; import com.thirdparty.ticketing.domain.ticket.dto.SeatSelectionRequest; +import com.thirdparty.ticketing.domain.ticket.dto.TicketPaymentRequest; import com.thirdparty.ticketing.domain.ticket.policy.OptimisticLockSeatStrategy; -import com.thirdparty.ticketing.domain.ticket.repository.TicketRepository; import com.thirdparty.ticketing.domain.ticket.service.PersistenceTicketService; import com.thirdparty.ticketing.domain.ticket.service.TicketService; @DataJpaTest @Import({ - PersistenceTicketService.class, - SimulatedPaymentProcessor.class, - OptimisticLockSeatStrategy.class + PersistenceTicketService.class, + SimulatedPaymentProcessor.class, + OptimisticLockSeatStrategy.class }) public class PersistenceTicketServiceTest { private static final Logger log = LoggerFactory.getLogger(PersistenceTicketServiceTest.class); - @Autowired - private TestEntityManager entityManager; - @Autowired - private DataSource dataSource; - @Autowired - private SeatRepository seatRepository; - @Autowired - private TicketRepository ticketRepository; - @Autowired - private MemberRepository memberRepository; - - @Autowired - private TicketService ticketService; + @Autowired private SeatRepository seatRepository; + + @Autowired private TicketService ticketService; private String memberEmail = "test@gmail.com"; private Long seatId = 1L; @@ -152,7 +137,9 @@ private void setUpAuthentication() { @Nested @DisplayName("티켓 예매 할 때 결제 시도 시") - @Sql(scripts = "/db/reservation-test.sql", config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)) + @Sql( + scripts = "/db/reservation-test.sql", + config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED)) class reservationTicketTest { @Test @@ -168,14 +155,21 @@ void reservationTicket_ConcurrencyTest() throws InterruptedException { // When IntStream.range(0, numRequests) - .forEach(i -> executor.submit(() -> { - try { - latch.await(); // 동기화된 시작 - reservationTicketTask(memberEmail, seatId, successfulReservations, failedReservations); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - })); + .forEach( + i -> + executor.submit( + () -> { + try { + latch.await(); // 동기화된 시작 + reservationTicketTask( + memberEmail, + seatId, + successfulReservations, + failedReservations); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + })); latch.countDown(); // 모든 스레드가 동시에 실행되도록 설정 @@ -187,9 +181,11 @@ void reservationTicket_ConcurrencyTest() throws InterruptedException { assertThat(failedReservations.get()).isEqualTo(numRequests - 1); } - private void reservationTicketTask(String memberEmail, Long seatId, - AtomicInteger successfulReservations, - AtomicInteger failedReservations) { + private void reservationTicketTask( + String memberEmail, + Long seatId, + AtomicInteger successfulReservations, + AtomicInteger failedReservations) { try { ticketService.reservationTicket(memberEmail, new TicketPaymentRequest(seatId)); successfulReservations.incrementAndGet(); From a334e1257d4012e13e87dd3cf475a32f7fe60df1 Mon Sep 17 00:00:00 2001 From: seminchoi Date: Fri, 16 Aug 2024 10:37:19 +0900 Subject: [PATCH 12/23] =?UTF-8?q?feat:=20=EC=A2=8C=EC=84=9D=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EC=97=90=20=EA=B4=80=EB=A0=A8=EB=90=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/thirdparty/ticketing/domain/common/ErrorCode.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/thirdparty/ticketing/domain/common/ErrorCode.java b/src/main/java/com/thirdparty/ticketing/domain/common/ErrorCode.java index c00edc04..e721c2dd 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/common/ErrorCode.java +++ b/src/main/java/com/thirdparty/ticketing/domain/common/ErrorCode.java @@ -47,6 +47,7 @@ public enum ErrorCode { */ NOT_FOUND_SEAT(HttpStatus.NOT_FOUND, "S404-1", "존재하지 않는 좌석입니다."), NOT_SELECTABLE_SEAT(HttpStatus.FORBIDDEN, "S403-1", "이미 선택된 좌석입니다."), + INVALID_SEAT_STATUS(HttpStatus.FORBIDDEN, "S403-2", "요청을 수행할 수 없는 좌석의 상태입니다."), /* Payment Error From 446f0421bed824b54364d0ad4b5e3d8db3a48a38 Mon Sep 17 00:00:00 2001 From: seminchoi Date: Fri, 16 Aug 2024 10:38:54 +0900 Subject: [PATCH 13/23] =?UTF-8?q?refactor:=20=EC=A2=8C=EC=84=9D,=20?= =?UTF-8?q?=EC=A2=8C=EC=84=9D=20=EC=83=81=ED=83=9C,=20=EC=A2=8C=EC=84=9D?= =?UTF-8?q?=EC=97=90=20=ED=95=A0=EB=8B=B9=EB=90=9C=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=84=EC=97=90=20=EC=84=9C=EB=A1=9C=20=EB=A9=94=EC=84=B8?= =?UTF-8?q?=EC=A7=80=EB=A5=BC=20=ED=86=B5=ED=95=B4=EC=84=9C=20=ED=86=B5?= =?UTF-8?q?=EC=8B=A0=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ticketing/domain/member/Member.java | 14 ++++++++++++++ .../thirdparty/ticketing/domain/seat/Seat.java | 18 ++++++++++++++++-- .../ticketing/domain/seat/SeatStatus.java | 12 ++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/thirdparty/ticketing/domain/member/Member.java b/src/main/java/com/thirdparty/ticketing/domain/member/Member.java index 2e4b4075..034cf35b 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/member/Member.java +++ b/src/main/java/com/thirdparty/ticketing/domain/member/Member.java @@ -1,6 +1,7 @@ package com.thirdparty.ticketing.domain.member; import java.time.ZonedDateTime; +import java.util.Objects; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -53,4 +54,17 @@ public Member(String email, String password, MemberRole memberRole, ZonedDateTim this.password = password; this.memberRole = memberRole; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Member member = (Member) o; + return Objects.equals(memberId, member.memberId); + } + + @Override + public int hashCode() { + return Objects.hashCode(memberId); + } } diff --git a/src/main/java/com/thirdparty/ticketing/domain/seat/Seat.java b/src/main/java/com/thirdparty/ticketing/domain/seat/Seat.java index 225ed4ee..faeacbba 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/seat/Seat.java +++ b/src/main/java/com/thirdparty/ticketing/domain/seat/Seat.java @@ -64,7 +64,21 @@ public void assignByMember(Member member) { this.seatStatus = SeatStatus.SELECTED; } - public void updateStatus(SeatStatus seatStatus) { - this.seatStatus = seatStatus; + public void markAsPendingPayment() { + if (!seatStatus.isSelected()) { + throw new TicketingException(ErrorCode.INVALID_SEAT_STATUS); + } + this.seatStatus = SeatStatus.PENDING_PAYMENT; + } + + public void markAsPaid() { + if (!seatStatus.isPendingPayment()) { + throw new TicketingException(ErrorCode.INVALID_SEAT_STATUS); + } + this.seatStatus = SeatStatus.PAID; + } + + public boolean isAssignedByMember(Member loginMember) { + return loginMember.equals(member); } } diff --git a/src/main/java/com/thirdparty/ticketing/domain/seat/SeatStatus.java b/src/main/java/com/thirdparty/ticketing/domain/seat/SeatStatus.java index a0b26463..a3e9d0d3 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/seat/SeatStatus.java +++ b/src/main/java/com/thirdparty/ticketing/domain/seat/SeatStatus.java @@ -9,4 +9,16 @@ public enum SeatStatus { public boolean isSelectable() { return this == SELECTABLE; } + + public boolean isSelected() { + return this == SELECTED; + } + + public boolean isPendingPayment() { + return this == PENDING_PAYMENT; + } + + public boolean isPaid() { + return this == PAID; + } } From a5c0e7950a56b9327ede27fd6f618e78e4f36c79 Mon Sep 17 00:00:00 2001 From: seminchoi Date: Fri, 16 Aug 2024 10:39:25 +0900 Subject: [PATCH 14/23] =?UTF-8?q?refactor:=20=EA=B0=9D=EC=B2=B4=EA=B0=84?= =?UTF-8?q?=20=EB=A9=94=EC=84=B8=EC=A7=80=EB=A5=BC=20=EB=B3=B4=EB=82=B4?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=ED=95=9C=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ticket/service/PersistenceTicketService.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/PersistenceTicketService.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/PersistenceTicketService.java index f209345d..6aa11fc2 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/PersistenceTicketService.java +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/PersistenceTicketService.java @@ -12,7 +12,6 @@ import com.thirdparty.ticketing.domain.payment.PaymentProcessor; import com.thirdparty.ticketing.domain.payment.dto.PaymentRequest; import com.thirdparty.ticketing.domain.seat.Seat; -import com.thirdparty.ticketing.domain.seat.SeatStatus; import com.thirdparty.ticketing.domain.seat.repository.SeatRepository; import com.thirdparty.ticketing.domain.ticket.dto.SeatSelectionRequest; import com.thirdparty.ticketing.domain.ticket.dto.TicketPaymentRequest; @@ -71,15 +70,11 @@ public void reservationTicket(String memberEmail, TicketPaymentRequest ticketPay .findByEmail(memberEmail) .orElseThrow(() -> new TicketingException(ErrorCode.NOT_FOUND_MEMBER)); - if (!seat.getSeatStatus().equals(SeatStatus.SELECTED)) { - seat.updateStatus(SeatStatus.PENDING_PAYMENT); - } + seat.markAsPendingPayment(); paymentProcessor.processPayment(new PaymentRequest()); - if (!seat.getSeatStatus().equals(SeatStatus.PAID)) { - seat.updateStatus(SeatStatus.PAID); - } + seat.markAsPaid(); - if (!seat.getMember().getMemberId().equals(loginMember.getMemberId())) { + if (seat.isAssignedByMember(loginMember)) { throw new TicketingException(ErrorCode.NOT_SELECTABLE_SEAT); } } From 2e19429aba92b8e58899505d35e5fce370e043d1 Mon Sep 17 00:00:00 2001 From: seminchoi Date: Fri, 16 Aug 2024 10:50:54 +0900 Subject: [PATCH 15/23] =?UTF-8?q?feat:=20reservation-test.sql=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/resources/db/reservation-test.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/resources/db/reservation-test.sql b/src/test/resources/db/reservation-test.sql index b0cf148d..86bb902c 100644 --- a/src/test/resources/db/reservation-test.sql +++ b/src/test/resources/db/reservation-test.sql @@ -28,4 +28,4 @@ VALUES (1, 'Grade1', 10000, 1, NOW(), NOW()); -- Seat 테이블에 데이터 삽입 INSERT INTO seat (seat_id, seat_code, seat_status, member_id, zone_id, seat_grade_id, version, created_at, updated_at) -VALUES (1, 'A01', 'SELECTABLE', 1, 1, 1, 0, NOW(), NOW()); +VALUES (1, 'A01', 'SELECTED', 1, 1, 1, 0, NOW(), NOW()); From 1e01b876a51c36ac48d2878a830426ee0103455f Mon Sep 17 00:00:00 2001 From: mirageoasis Date: Thu, 15 Aug 2024 13:28:19 +0900 Subject: [PATCH 16/23] =?UTF-8?q?feat:=20CacheTicketService=20selectSeat?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/common/LettuceRepository.java | 20 +++++++++++ .../service/LettuceCacheTicketService.java | 33 +++++++++++++++++ .../service/RedissonCacheTicketService.java | 35 +++++++++++++++++++ .../global/config/RedissonConfig.java | 9 ----- 4 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/thirdparty/ticketing/domain/common/LettuceRepository.java create mode 100644 src/main/java/com/thirdparty/ticketing/domain/ticket/service/LettuceCacheTicketService.java create mode 100644 src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java diff --git a/src/main/java/com/thirdparty/ticketing/domain/common/LettuceRepository.java b/src/main/java/com/thirdparty/ticketing/domain/common/LettuceRepository.java new file mode 100644 index 00000000..e9434300 --- /dev/null +++ b/src/main/java/com/thirdparty/ticketing/domain/common/LettuceRepository.java @@ -0,0 +1,20 @@ +package com.thirdparty.ticketing.domain.common; + +import java.time.Duration; + +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class LettuceRepository { + private final StringRedisTemplate redisTemplate; + + // 1. lock을 생성 + // 2. 60초가 유지되는 key는 자리번호 value는 유저 id를 생성 + public Boolean seatLock(String key) { + return redisTemplate.opsForValue().setIfAbsent(key, "lock", Duration.ofSeconds(60)); + } +} diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/LettuceCacheTicketService.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/LettuceCacheTicketService.java new file mode 100644 index 00000000..e2cec47c --- /dev/null +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/LettuceCacheTicketService.java @@ -0,0 +1,33 @@ +package com.thirdparty.ticketing.domain.ticket.service; + +import org.springframework.stereotype.Service; + +import com.thirdparty.ticketing.domain.member.repository.MemberRepository; +import com.thirdparty.ticketing.domain.payment.PaymentProcessor; +import com.thirdparty.ticketing.domain.seat.repository.SeatRepository; +import com.thirdparty.ticketing.domain.ticket.dto.SeatSelectionRequest; +import com.thirdparty.ticketing.domain.ticket.dto.TicketPaymentRequest; +import com.thirdparty.ticketing.domain.ticket.repository.TicketRepository; + +@Service +public class LettuceCacheTicketService extends TicketService { + private final CacheTicketService cacheTicketService; + + public LettuceCacheTicketService( + MemberRepository memberRepository, + TicketRepository ticketRepository, + SeatRepository seatRepository, + PaymentProcessor paymentProcessor, + CacheTicketService cacheTicketService) { + super(memberRepository, ticketRepository, seatRepository, paymentProcessor); + this.cacheTicketService = cacheTicketService; + } + + @Override + public void selectSeat(String memberEmail, SeatSelectionRequest seatSelectionRequest) { + // TODO spin lock으로 일정 횟수 만큼 lock을 얻어오기 + } + + @Override + public void reservationTicket(String memberEmail, TicketPaymentRequest ticketPaymentRequest) {} +} diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java new file mode 100644 index 00000000..56f9a902 --- /dev/null +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java @@ -0,0 +1,35 @@ +package com.thirdparty.ticketing.domain.ticket.service; + +import org.springframework.stereotype.Service; + +import com.thirdparty.ticketing.domain.member.repository.MemberRepository; +import com.thirdparty.ticketing.domain.payment.PaymentProcessor; +import com.thirdparty.ticketing.domain.seat.repository.SeatRepository; +import com.thirdparty.ticketing.domain.ticket.dto.SeatSelectionRequest; +import com.thirdparty.ticketing.domain.ticket.dto.TicketPaymentRequest; +import com.thirdparty.ticketing.domain.ticket.repository.TicketRepository; + +@Service +public class RedissonCacheTicketService extends TicketService { + + public RedissonCacheTicketService( + MemberRepository memberRepository, + TicketRepository ticketRepository, + SeatRepository seatRepository, + PaymentProcessor paymentProcessor) { + super(memberRepository, ticketRepository, seatRepository, paymentProcessor); + } + + @Override + public void selectSeat(String memberEmail, SeatSelectionRequest seatSelectionRequest) { + // TODO + // try { + // + // }catch (){ + // + // } + } + + @Override + public void reservationTicket(String memberEmail, TicketPaymentRequest ticketPaymentRequest) {} +} diff --git a/src/main/java/com/thirdparty/ticketing/global/config/RedissonConfig.java b/src/main/java/com/thirdparty/ticketing/global/config/RedissonConfig.java index 7921301c..23ec79bb 100644 --- a/src/main/java/com/thirdparty/ticketing/global/config/RedissonConfig.java +++ b/src/main/java/com/thirdparty/ticketing/global/config/RedissonConfig.java @@ -4,11 +4,9 @@ import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.redisson.config.SingleServerConfig; -import org.redisson.spring.data.connection.RedissonConnectionFactory; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.core.StringRedisTemplate; import lombok.Setter; @@ -26,11 +24,4 @@ public RedissonClient redissonClient() { serverConfig.setAddress("redis://" + host + ":" + port); return Redisson.create(config); } - - @Bean - public StringRedisTemplate redissonRedisTemplate(RedissonClient redissonClient) { - StringRedisTemplate redisTemplate = new StringRedisTemplate(); - redisTemplate.setConnectionFactory(new RedissonConnectionFactory(redissonClient)); - return redisTemplate; - } } From 1045defe12bb0bde8988073ebe5978e931a82ff6 Mon Sep 17 00:00:00 2001 From: mirageoasis Date: Thu, 15 Aug 2024 16:06:47 +0900 Subject: [PATCH 17/23] =?UTF-8?q?feat:=20=EC=9E=90=EB=A6=AC=20=EC=84=A0?= =?UTF-8?q?=EC=A0=90=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/common/LettuceRepository.java | 4 +++ .../ticket/controller/TicketController.java | 6 ++++ .../service/LettuceCacheTicketService.java | 22 ++++++++++++- .../service/RedissonCacheTicketService.java | 31 +++++++++++++++---- 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/thirdparty/ticketing/domain/common/LettuceRepository.java b/src/main/java/com/thirdparty/ticketing/domain/common/LettuceRepository.java index e9434300..e5fc25f5 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/common/LettuceRepository.java +++ b/src/main/java/com/thirdparty/ticketing/domain/common/LettuceRepository.java @@ -17,4 +17,8 @@ public class LettuceRepository { public Boolean seatLock(String key) { return redisTemplate.opsForValue().setIfAbsent(key, "lock", Duration.ofSeconds(60)); } + + public void unlock(String string) { + redisTemplate.delete(string); + } } diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/controller/TicketController.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/controller/TicketController.java index 6eeff078..4347b61d 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/ticket/controller/TicketController.java +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/controller/TicketController.java @@ -2,6 +2,7 @@ import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -20,8 +21,13 @@ @RestController("/api") @RequiredArgsConstructor public class TicketController { + private final TicketService ticketService; + public TicketController(@Qualifier("lettuceCacheTicketService") TicketService ticketService) { + this.ticketService = ticketService; + } + @GetMapping("/members/tickets") public ResponseEntity> selectMyTickets( @LoginMember String memberEmail) { diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/LettuceCacheTicketService.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/LettuceCacheTicketService.java index e2cec47c..4f195025 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/LettuceCacheTicketService.java +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/LettuceCacheTicketService.java @@ -2,6 +2,7 @@ import org.springframework.stereotype.Service; +import com.thirdparty.ticketing.domain.common.LettuceRepository; import com.thirdparty.ticketing.domain.member.repository.MemberRepository; import com.thirdparty.ticketing.domain.payment.PaymentProcessor; import com.thirdparty.ticketing.domain.seat.repository.SeatRepository; @@ -12,20 +13,39 @@ @Service public class LettuceCacheTicketService extends TicketService { private final CacheTicketService cacheTicketService; + private final LettuceRepository lettuceRepository; public LettuceCacheTicketService( MemberRepository memberRepository, TicketRepository ticketRepository, SeatRepository seatRepository, PaymentProcessor paymentProcessor, - CacheTicketService cacheTicketService) { + CacheTicketService cacheTicketService, + LettuceRepository lettuceRepository) { super(memberRepository, ticketRepository, seatRepository, paymentProcessor); this.cacheTicketService = cacheTicketService; + this.lettuceRepository = lettuceRepository; } @Override public void selectSeat(String memberEmail, SeatSelectionRequest seatSelectionRequest) { // TODO spin lock으로 일정 횟수 만큼 lock을 얻어오기 + int limit = 5; + try { + while (limit > 0 + && lettuceRepository.seatLock(seatSelectionRequest.getSeatId().toString())) { + limit -= 1; + Thread.sleep(300); + } + + if (limit > 0) { + cacheTicketService.selectSeat(memberEmail, seatSelectionRequest); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + lettuceRepository.unlock(seatSelectionRequest.getSeatId().toString()); + } } @Override diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java index 56f9a902..fc8802c8 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java @@ -1,5 +1,9 @@ package com.thirdparty.ticketing.domain.ticket.service; +import java.util.concurrent.TimeUnit; + +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; import org.springframework.stereotype.Service; import com.thirdparty.ticketing.domain.member.repository.MemberRepository; @@ -12,22 +16,37 @@ @Service public class RedissonCacheTicketService extends TicketService { + private final CacheTicketService cacheTicketService; + private final RedissonClient redissonClient; + public RedissonCacheTicketService( MemberRepository memberRepository, TicketRepository ticketRepository, SeatRepository seatRepository, + CacheTicketService cacheTicketService, + RedissonClient redissonClient, PaymentProcessor paymentProcessor) { super(memberRepository, ticketRepository, seatRepository, paymentProcessor); + this.cacheTicketService = cacheTicketService; + this.redissonClient = redissonClient; } @Override public void selectSeat(String memberEmail, SeatSelectionRequest seatSelectionRequest) { - // TODO - // try { - // - // }catch (){ - // - // } + RLock lock = redissonClient.getLock(seatSelectionRequest.getSeatId().toString()); + + try { + boolean available = lock.tryLock(5, 300, TimeUnit.MILLISECONDS); + if (!available) { + return; + } + + cacheTicketService.selectSeat(memberEmail, seatSelectionRequest); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + lock.unlock(); + } } @Override From 7eb8c8aa4821b8a0e7fecec707d03f9489149aac Mon Sep 17 00:00:00 2001 From: mirageoasis Date: Thu, 15 Aug 2024 21:13:00 +0900 Subject: [PATCH 18/23] =?UTF-8?q?feat:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ticket/controller/TicketController.java | 3 - .../service/TicketServiceConcurrencyTest.java | 143 ++++++++++++++++++ 2 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/thirdparty/ticketing/domain/ticket/service/TicketServiceConcurrencyTest.java diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/controller/TicketController.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/controller/TicketController.java index 4347b61d..764cd53a 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/ticket/controller/TicketController.java +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/controller/TicketController.java @@ -16,10 +16,7 @@ import com.thirdparty.ticketing.domain.ticket.dto.TicketPaymentRequest; import com.thirdparty.ticketing.domain.ticket.service.TicketService; -import lombok.RequiredArgsConstructor; - @RestController("/api") -@RequiredArgsConstructor public class TicketController { private final TicketService ticketService; diff --git a/src/test/java/com/thirdparty/ticketing/domain/ticket/service/TicketServiceConcurrencyTest.java b/src/test/java/com/thirdparty/ticketing/domain/ticket/service/TicketServiceConcurrencyTest.java new file mode 100644 index 00000000..919162c8 --- /dev/null +++ b/src/test/java/com/thirdparty/ticketing/domain/ticket/service/TicketServiceConcurrencyTest.java @@ -0,0 +1,143 @@ +package com.thirdparty.ticketing.domain.ticket.service; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockitoAnnotations; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import com.thirdparty.ticketing.domain.common.LettuceRepository; +import com.thirdparty.ticketing.domain.member.Member; +import com.thirdparty.ticketing.domain.member.MemberRole; +import com.thirdparty.ticketing.domain.member.repository.MemberRepository; +import com.thirdparty.ticketing.domain.performance.Performance; +import com.thirdparty.ticketing.domain.seat.Seat; +import com.thirdparty.ticketing.domain.seat.SeatGrade; +import com.thirdparty.ticketing.domain.seat.SeatStatus; +import com.thirdparty.ticketing.domain.seat.repository.SeatRepository; +import com.thirdparty.ticketing.domain.ticket.dto.SeatSelectionRequest; +import com.thirdparty.ticketing.domain.zone.Zone; + +@ExtendWith(SpringExtension.class) +@SpringBootTest +public class TicketServiceConcurrencyTest { + + @MockBean private SeatRepository seatRepository; + + @Autowired private MemberRepository memberRepository; + + @Autowired private LettuceRepository lettuceRepository; + + @Autowired private RedissonClient redissonClient; + + @Autowired private LettuceCacheTicketService lettuceCacheTicketService; + + @Autowired private RedissonCacheTicketService redissonCacheTicketService; + + private List members; + private Seat seat; + private Zone zone; + private SeatGrade seatGrade; + private Performance performance; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + + members = + memberRepository.saveAllAndFlush( + List.of( + new Member("member1@example.com", "password", MemberRole.USER), + new Member("member2@example.com", "password", MemberRole.USER), + new Member("member3@example.com", "password", MemberRole.USER), + new Member("member4@example.com", "password", MemberRole.USER), + new Member("member5@example.com", "password", MemberRole.USER))); + + performance = + new Performance( + 1L, + "Phantom of the Opera", + "Broadway Theater", + ZonedDateTime.now().plusDays(10)); + seatGrade = new SeatGrade(1L, performance, 20000L, "Regular"); + zone = new Zone(1L, performance, "R"); + + seat = spy(new Seat(1L, zone, seatGrade, null, "R", SeatStatus.SELECTABLE)); + + // Repository 모킹 + when(seatRepository.findById(seat.getSeatId())).thenReturn(Optional.of(seat)); + } + + @AfterEach + void breakUp() { + memberRepository.deleteAll(); + } + + @Test + public void testConcurrentSeatSelectionWithLettuce() throws InterruptedException { + runConcurrentSeatSelectionTest(lettuceCacheTicketService); + } + + @Test + public void testConcurrentSeatSelectionWithRedisson() throws InterruptedException { + // runConcurrentSeatSelectionTest(redissonCacheTicketService); + } + + private void runConcurrentSeatSelectionTest(TicketService ticketService) + throws InterruptedException { + int threadCount = members.size(); + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + + for (Member member : members) { + // 각 멤버에 대해 작업을 스레드 풀에 제출 + executorService.submit( + () -> { + try { + // 스레드 풀에서 병렬로 실행되는 작업 + SeatSelectionRequest seatSelectionRequest = + new SeatSelectionRequest(seat.getSeatId()); + ticketService.selectSeat(member.getEmail(), seatSelectionRequest); + } catch (RuntimeException e) { + // 예외 발생 시 오류 로그 출력 + System.err.println( + "Exception occurred for member: " + + member.getEmail() + + " - " + + e.getMessage()); + } finally { + // latch 카운트 감소, 스레드 완료 시 호출 + latch.countDown(); + } + }); + } + + latch.await(); + + Seat reservedSeat = seatRepository.findById(seat.getSeatId()).orElseThrow(); + assertNotNull(reservedSeat.getMember(), "Seat should be reserved by one member"); + + // designateMember 메서드가 정확히 한 번 호출되었는지 확인 + verify(seat, times(5)).empty(); + verify(seat, times(1)).designateMember(any(Member.class)); + } +} From b141a46e80ce294c90d4c7f8b320164940394846 Mon Sep 17 00:00:00 2001 From: mirageoasis Date: Thu, 15 Aug 2024 22:27:19 +0900 Subject: [PATCH 19/23] =?UTF-8?q?feat:=20=EB=A0=88=EB=94=94=EC=8A=A4=20?= =?UTF-8?q?=EB=9D=BD=20=EC=9E=91=EC=84=B1=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ticketing/domain/common/LettuceRepository.java | 2 +- .../domain/ticket/service/LettuceCacheTicketService.java | 4 ++-- .../domain/ticket/service/RedissonCacheTicketService.java | 2 +- .../domain/ticket/service/TicketServiceConcurrencyTest.java | 5 ++--- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/thirdparty/ticketing/domain/common/LettuceRepository.java b/src/main/java/com/thirdparty/ticketing/domain/common/LettuceRepository.java index e5fc25f5..d1fe6d3a 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/common/LettuceRepository.java +++ b/src/main/java/com/thirdparty/ticketing/domain/common/LettuceRepository.java @@ -15,7 +15,7 @@ public class LettuceRepository { // 1. lock을 생성 // 2. 60초가 유지되는 key는 자리번호 value는 유저 id를 생성 public Boolean seatLock(String key) { - return redisTemplate.opsForValue().setIfAbsent(key, "lock", Duration.ofSeconds(60)); + return redisTemplate.opsForValue().setIfAbsent(key, "lock", Duration.ofMinutes(1)); } public void unlock(String string) { diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/LettuceCacheTicketService.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/LettuceCacheTicketService.java index 4f195025..fbb600c6 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/LettuceCacheTicketService.java +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/LettuceCacheTicketService.java @@ -33,9 +33,9 @@ public void selectSeat(String memberEmail, SeatSelectionRequest seatSelectionReq int limit = 5; try { while (limit > 0 - && lettuceRepository.seatLock(seatSelectionRequest.getSeatId().toString())) { + && !lettuceRepository.seatLock(seatSelectionRequest.getSeatId().toString())) { limit -= 1; - Thread.sleep(300); + Thread.sleep(1000); } if (limit > 0) { diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java index fc8802c8..861c4e10 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java @@ -36,7 +36,7 @@ public void selectSeat(String memberEmail, SeatSelectionRequest seatSelectionReq RLock lock = redissonClient.getLock(seatSelectionRequest.getSeatId().toString()); try { - boolean available = lock.tryLock(5, 300, TimeUnit.MILLISECONDS); + boolean available = lock.tryLock(5, 1, TimeUnit.SECONDS); if (!available) { return; } diff --git a/src/test/java/com/thirdparty/ticketing/domain/ticket/service/TicketServiceConcurrencyTest.java b/src/test/java/com/thirdparty/ticketing/domain/ticket/service/TicketServiceConcurrencyTest.java index 919162c8..6cc1c490 100644 --- a/src/test/java/com/thirdparty/ticketing/domain/ticket/service/TicketServiceConcurrencyTest.java +++ b/src/test/java/com/thirdparty/ticketing/domain/ticket/service/TicketServiceConcurrencyTest.java @@ -99,7 +99,7 @@ public void testConcurrentSeatSelectionWithLettuce() throws InterruptedException @Test public void testConcurrentSeatSelectionWithRedisson() throws InterruptedException { - // runConcurrentSeatSelectionTest(redissonCacheTicketService); + runConcurrentSeatSelectionTest(redissonCacheTicketService); } private void runConcurrentSeatSelectionTest(TicketService ticketService) @@ -135,9 +135,8 @@ private void runConcurrentSeatSelectionTest(TicketService ticketService) Seat reservedSeat = seatRepository.findById(seat.getSeatId()).orElseThrow(); assertNotNull(reservedSeat.getMember(), "Seat should be reserved by one member"); - + System.out.println(reservedSeat.getMember().getEmail()); // designateMember 메서드가 정확히 한 번 호출되었는지 확인 - verify(seat, times(5)).empty(); verify(seat, times(1)).designateMember(any(Member.class)); } } From f315001baa7d71af6ba0a357aae8ef27214501a5 Mon Sep 17 00:00:00 2001 From: mirageoasis Date: Thu, 15 Aug 2024 22:35:28 +0900 Subject: [PATCH 20/23] =?UTF-8?q?fix:=20=EC=8B=9C=EA=B0=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/ticket/service/LettuceCacheTicketService.java | 2 +- .../domain/ticket/service/RedissonCacheTicketService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/LettuceCacheTicketService.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/LettuceCacheTicketService.java index fbb600c6..90f358b5 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/LettuceCacheTicketService.java +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/LettuceCacheTicketService.java @@ -35,7 +35,7 @@ public void selectSeat(String memberEmail, SeatSelectionRequest seatSelectionReq while (limit > 0 && !lettuceRepository.seatLock(seatSelectionRequest.getSeatId().toString())) { limit -= 1; - Thread.sleep(1000); + Thread.sleep(300); } if (limit > 0) { diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java index 861c4e10..a07cacbd 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java @@ -36,7 +36,7 @@ public void selectSeat(String memberEmail, SeatSelectionRequest seatSelectionReq RLock lock = redissonClient.getLock(seatSelectionRequest.getSeatId().toString()); try { - boolean available = lock.tryLock(5, 1, TimeUnit.SECONDS); + boolean available = lock.tryLock(5, 300, TimeUnit.MICROSECONDS); if (!available) { return; } From 2be4877c7b74c755f73a948ad8cae5bae522d095 Mon Sep 17 00:00:00 2001 From: mirageoasis Date: Fri, 16 Aug 2024 00:12:03 +0900 Subject: [PATCH 21/23] =?UTF-8?q?feature:=20=ED=8B=B0=EC=BC=93=20=EA=B5=AC?= =?UTF-8?q?=EB=A7=A4=20=EA=B8=B0=EB=8A=A5=20=EC=9E=84=EC=8B=9C=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/LettuceCacheTicketService.java | 20 +++++++++++++++-- .../service/RedissonCacheTicketService.java | 22 ++++++++++++++++++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/LettuceCacheTicketService.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/LettuceCacheTicketService.java index 90f358b5..cbe986f1 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/LettuceCacheTicketService.java +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/LettuceCacheTicketService.java @@ -29,7 +29,6 @@ public LettuceCacheTicketService( @Override public void selectSeat(String memberEmail, SeatSelectionRequest seatSelectionRequest) { - // TODO spin lock으로 일정 횟수 만큼 lock을 얻어오기 int limit = 5; try { while (limit > 0 @@ -49,5 +48,22 @@ public void selectSeat(String memberEmail, SeatSelectionRequest seatSelectionReq } @Override - public void reservationTicket(String memberEmail, TicketPaymentRequest ticketPaymentRequest) {} + public void reservationTicket(String memberEmail, TicketPaymentRequest ticketPaymentRequest) { + int limit = 5; + try { + while (limit > 0 + && !lettuceRepository.seatLock(ticketPaymentRequest.getSeatId().toString())) { + limit -= 1; + Thread.sleep(300); + } + + if (limit > 0) { + cacheTicketService.reservationTicket(memberEmail, ticketPaymentRequest); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + lettuceRepository.unlock(ticketPaymentRequest.getSeatId().toString()); + } + } } diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java index a07cacbd..773ec899 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java @@ -1,13 +1,17 @@ package com.thirdparty.ticketing.domain.ticket.service; +import java.util.NoSuchElementException; import java.util.concurrent.TimeUnit; + import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import com.thirdparty.ticketing.domain.member.repository.MemberRepository; import com.thirdparty.ticketing.domain.payment.PaymentProcessor; +import com.thirdparty.ticketing.domain.seat.Seat; import com.thirdparty.ticketing.domain.seat.repository.SeatRepository; import com.thirdparty.ticketing.domain.ticket.dto.SeatSelectionRequest; import com.thirdparty.ticketing.domain.ticket.dto.TicketPaymentRequest; @@ -50,5 +54,21 @@ public void selectSeat(String memberEmail, SeatSelectionRequest seatSelectionReq } @Override - public void reservationTicket(String memberEmail, TicketPaymentRequest ticketPaymentRequest) {} + @Transactional + public void reservationTicket(String memberEmail, TicketPaymentRequest ticketPaymentRequest) { + RLock lock = redissonClient.getLock(ticketPaymentRequest.getSeatId().toString()); + + try { + boolean available = lock.tryLock(5, 300, TimeUnit.MICROSECONDS); + if (!available) { + return; + } + + cacheTicketService.reservationTicket(memberEmail, ticketPaymentRequest); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + lock.unlock(); + } + } } From f818ca3abd4fc2953cfee3248607a2d34cd87127 Mon Sep 17 00:00:00 2001 From: mirageoasis Date: Fri, 16 Aug 2024 00:12:14 +0900 Subject: [PATCH 22/23] =?UTF-8?q?feature:=20=ED=8B=B0=EC=BC=93=20=EA=B5=AC?= =?UTF-8?q?=EB=A7=A4=20=EA=B8=B0=EB=8A=A5=20=EC=9E=84=EC=8B=9C=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/ticket/service/RedissonCacheTicketService.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java index 773ec899..adb7deb8 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java +++ b/src/main/java/com/thirdparty/ticketing/domain/ticket/service/RedissonCacheTicketService.java @@ -1,9 +1,7 @@ package com.thirdparty.ticketing.domain.ticket.service; -import java.util.NoSuchElementException; import java.util.concurrent.TimeUnit; - import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.stereotype.Service; @@ -11,7 +9,6 @@ import com.thirdparty.ticketing.domain.member.repository.MemberRepository; import com.thirdparty.ticketing.domain.payment.PaymentProcessor; -import com.thirdparty.ticketing.domain.seat.Seat; import com.thirdparty.ticketing.domain.seat.repository.SeatRepository; import com.thirdparty.ticketing.domain.ticket.dto.SeatSelectionRequest; import com.thirdparty.ticketing.domain.ticket.dto.TicketPaymentRequest; From 1b5712922b436dfd52440a1f2a98c615e1cc7729 Mon Sep 17 00:00:00 2001 From: seminchoi Date: Sat, 17 Aug 2024 00:34:17 +0900 Subject: [PATCH 23/23] =?UTF-8?q?setting:=20=EC=84=9C=EB=B8=8C=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EB=B0=B0=ED=8F=AC=ED=99=98=EA=B2=BD=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=ED=8C=8C=EC=9D=BC=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend-config b/backend-config index 44d0d8d9..8d1e4757 160000 --- a/backend-config +++ b/backend-config @@ -1 +1 @@ -Subproject commit 44d0d8d9302141589cae777b567a3eab6fd9a68f +Subproject commit 8d1e4757aca6c549cde6585dbe6128d0b069913b