From 0aee0d04d5da97f8472313651120ea84e02a4f53 Mon Sep 17 00:00:00 2001 From: ghdtmdalsda Date: Fri, 2 May 2025 17:12:30 +0900 Subject: [PATCH 01/14] =?UTF-8?q?Feat=20:=20V1=5FTest1=20(DB=20=EB=9D=BD?= =?UTF-8?q?=20X)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ureca/common/init/InitController.java | 49 +++++++++++++ .../v1/controller/ReserveControllerV1.java | 7 ++ .../ureca/reserve/v1/domain/ReserveV1.java | 27 +++++++ .../v1/repository/ReserveRepositoryV1.java | 9 +++ .../reserve/v1/service/ReserveServiceV1.java | 71 ++++++++++++++++++ .../quickpick/ureca/ticket/domain/Ticket.java | 4 + .../v1/controller/TicketControllerV1.java | 33 +++++++++ .../ureca/ticket/v1/domain/TicketV1.java | 53 ++++++++++++++ .../v1/repository/TicketRepositoryV1.java | 21 ++++++ .../ticket/v1/service/TicketServiceV1.java | 7 ++ .../com/quickpick/ureca/user/domain/User.java | 6 +- .../ureca/user/repository/UserRepository.java | 7 +- .../ureca/user/v1/domain/UserV1.java | 36 +++++++++ .../user/v1/repository/UserRepositoryV1.java | 9 +++ .../v1/service/UserBulkInsertService.java | 34 +++++++++ .../ureca/user/v1/service/UserServiceV1.java | 7 ++ .../userticket/v1/domain/UserTicketV1.java | 34 +++++++++ .../v1/repository/UserTicketRepositoryV1.java | 7 ++ .../v1/TicketReservationServiceTest.java | 73 +++++++++++++++++++ 19 files changed, 491 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/quickpick/ureca/common/init/InitController.java create mode 100644 src/main/java/com/quickpick/ureca/reserve/v1/controller/ReserveControllerV1.java create mode 100644 src/main/java/com/quickpick/ureca/reserve/v1/domain/ReserveV1.java create mode 100644 src/main/java/com/quickpick/ureca/reserve/v1/repository/ReserveRepositoryV1.java create mode 100644 src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java create mode 100644 src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java create mode 100644 src/main/java/com/quickpick/ureca/ticket/v1/domain/TicketV1.java create mode 100644 src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java create mode 100644 src/main/java/com/quickpick/ureca/ticket/v1/service/TicketServiceV1.java create mode 100644 src/main/java/com/quickpick/ureca/user/v1/domain/UserV1.java create mode 100644 src/main/java/com/quickpick/ureca/user/v1/repository/UserRepositoryV1.java create mode 100644 src/main/java/com/quickpick/ureca/user/v1/service/UserBulkInsertService.java create mode 100644 src/main/java/com/quickpick/ureca/user/v1/service/UserServiceV1.java create mode 100644 src/main/java/com/quickpick/ureca/userticket/v1/domain/UserTicketV1.java create mode 100644 src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketRepositoryV1.java create mode 100644 src/test/java/com/quickpick/ureca/v1/TicketReservationServiceTest.java diff --git a/src/main/java/com/quickpick/ureca/common/init/InitController.java b/src/main/java/com/quickpick/ureca/common/init/InitController.java new file mode 100644 index 0000000..f581c20 --- /dev/null +++ b/src/main/java/com/quickpick/ureca/common/init/InitController.java @@ -0,0 +1,49 @@ +package com.quickpick.ureca.common.init; + +import com.quickpick.ureca.ticket.v1.domain.TicketV1; +import com.quickpick.ureca.ticket.v1.repository.TicketRepositoryV1; +import com.quickpick.ureca.user.v1.domain.UserV1; +import com.quickpick.ureca.user.v1.repository.UserRepositoryV1; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/init") +public class InitController { + + private final TicketRepositoryV1 ticketRepository; + private final UserRepositoryV1 userRepository; + + @PostMapping + public String initializeData( + @RequestParam(defaultValue = "1000") int ticketCount, + @RequestParam(defaultValue = "10000") int userCount, + @RequestParam(defaultValue = "1") long ticketId + ) { + ticketRepository.deleteAll(); + userRepository.deleteAll(); + + // 티켓 생성 + TicketV1 ticket = TicketV1.builder() + .name("테스트 티켓") + .quantity(ticketCount) + .build(); + ticketRepository.save(ticket); + + // 유저 생성 + List users = new ArrayList<>(); + for (int i = 1; i <= userCount; i++) { + UserV1 user = UserV1.builder() + .id("user" + i) + .build(); + users.add(user); + } + userRepository.saveAll(users); + + return "초기화 완료: 티켓 1개(" + ticketCount + "개 수량), 유저 " + userCount + "명 생성"; + } +} \ No newline at end of file diff --git a/src/main/java/com/quickpick/ureca/reserve/v1/controller/ReserveControllerV1.java b/src/main/java/com/quickpick/ureca/reserve/v1/controller/ReserveControllerV1.java new file mode 100644 index 0000000..11be570 --- /dev/null +++ b/src/main/java/com/quickpick/ureca/reserve/v1/controller/ReserveControllerV1.java @@ -0,0 +1,7 @@ +package com.quickpick.ureca.reserve.v1.controller; + +import org.springframework.stereotype.Controller; + +@Controller +public class ReserveControllerV1 { +} diff --git a/src/main/java/com/quickpick/ureca/reserve/v1/domain/ReserveV1.java b/src/main/java/com/quickpick/ureca/reserve/v1/domain/ReserveV1.java new file mode 100644 index 0000000..fc4596a --- /dev/null +++ b/src/main/java/com/quickpick/ureca/reserve/v1/domain/ReserveV1.java @@ -0,0 +1,27 @@ +package com.quickpick.ureca.reserve.v1.domain; + +import com.quickpick.ureca.reserve.status.ReserveStatus; +import com.quickpick.ureca.user.v1.domain.UserV1; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Table +@Entity +@Getter +@NoArgsConstructor +public class ReserveV1{ + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "reserve_id") + private Long reserveId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private UserV1 user; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private ReserveStatus status; +} diff --git a/src/main/java/com/quickpick/ureca/reserve/v1/repository/ReserveRepositoryV1.java b/src/main/java/com/quickpick/ureca/reserve/v1/repository/ReserveRepositoryV1.java new file mode 100644 index 0000000..17bfc64 --- /dev/null +++ b/src/main/java/com/quickpick/ureca/reserve/v1/repository/ReserveRepositoryV1.java @@ -0,0 +1,9 @@ +package com.quickpick.ureca.reserve.v1.repository; + +import com.quickpick.ureca.reserve.v1.domain.ReserveV1; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ReserveRepositoryV1 extends JpaRepository { +} diff --git a/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java b/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java new file mode 100644 index 0000000..515daa9 --- /dev/null +++ b/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java @@ -0,0 +1,71 @@ +package com.quickpick.ureca.reserve.v1.service; + +import com.quickpick.ureca.ticket.v1.domain.TicketV1; +import com.quickpick.ureca.ticket.v1.repository.TicketRepositoryV1; +import com.quickpick.ureca.user.v1.domain.UserV1; +import com.quickpick.ureca.user.v1.repository.UserRepositoryV1; +import com.quickpick.ureca.userticket.v1.domain.UserTicketV1; +import com.quickpick.ureca.userticket.v1.repository.UserTicketRepositoryV1; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +public class ReserveServiceV1 { + + @Autowired + private TicketRepositoryV1 ticketRepositoryV1; + + @Autowired + private UserRepositoryV1 userRepositoryV1; + + @Autowired + private UserTicketRepositoryV1 userTicketRepositoryV1; + + // 티켓 예약 메서드 (락 X) + @Transactional + public void reserveTicket(Long userId, Long ticketId) { + // 티켓과 사용자 가져오기 + TicketV1 ticket = ticketRepositoryV1.findById(ticketId).orElseThrow(() -> new RuntimeException("티켓을 찾을 수 없습니다.")); + UserV1 user = userRepositoryV1.findById(userId).orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")); + + // 티켓 재고가 없으면 예외 발생 + if (ticket.getQuantity() <= 0) { + throw new RuntimeException("매진되었습니다."); + } + + // 티켓 재고 감소 + System.out.println("감소!"); + ticket.decreaseCount(); + ticketRepositoryV1.save(ticket); // 재고 감소 후 저장 + + // 예약 정보 저장 + UserTicketV1 userTicket = new UserTicketV1(user, ticket); + userTicketRepositoryV1.save(userTicket); + } + +// @Transactional +// public void reserveTicket(Long userId, Long ticketId) { +// +// log.info(">>> reserveTicket called: userId = {}, ticketId = {}", userId, ticketId); +// +// UserV1 user = userRepositoryV1.findById(userId) +// .orElseThrow(() -> new IllegalArgumentException("User not found")); +// +// System.out.println("Reserve Ticket"); +// TicketV1 ticket = ticketRepositoryV1.findByIdForUpdate(ticketId) +// .orElseThrow(() -> new IllegalArgumentException("Ticket not found")); +// +// if (ticket.getQuantity() <= 0) { +// throw new IllegalStateException("Ticket out of stock"); +// } +// +// ticket.setQuantity(ticket.getQuantity() - 1); +// userTicketRepositoryV1.save(new UserTicketV1(user, ticket)); +// } + +} \ No newline at end of file diff --git a/src/main/java/com/quickpick/ureca/ticket/domain/Ticket.java b/src/main/java/com/quickpick/ureca/ticket/domain/Ticket.java index 2a0e3e9..f5dbf27 100644 --- a/src/main/java/com/quickpick/ureca/ticket/domain/Ticket.java +++ b/src/main/java/com/quickpick/ureca/ticket/domain/Ticket.java @@ -3,6 +3,8 @@ import com.quickpick.ureca.common.domain.BaseEntity; import com.quickpick.ureca.userticket.domain.UserTicket; import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -13,7 +15,9 @@ @Entity @Table(name = "ticket") @Getter +@Builder @NoArgsConstructor +@AllArgsConstructor public class Ticket extends BaseEntity { @Id diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java b/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java new file mode 100644 index 0000000..6e3f2d7 --- /dev/null +++ b/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java @@ -0,0 +1,33 @@ +package com.quickpick.ureca.ticket.v1.controller; + +import com.quickpick.ureca.reserve.v1.service.ReserveServiceV1; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/test-reserve") +@Slf4j +public class TicketControllerV1 { + + private final ReserveServiceV1 reserveServiceV1; + + public TicketControllerV1(ReserveServiceV1 reserveServiceV1) { + this.reserveServiceV1 = reserveServiceV1; + } + + @PostMapping + public ResponseEntity reserve(@RequestParam Long userId, @RequestParam Long ticketId) { + try { + reserveServiceV1.reserveTicket(userId, ticketId); + return ResponseEntity.ok("예약 성공"); + } catch (Exception e) { + log.error("예약 실패: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("예약 실패: " + e.getMessage()); + } + } +} diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/domain/TicketV1.java b/src/main/java/com/quickpick/ureca/ticket/v1/domain/TicketV1.java new file mode 100644 index 0000000..47e5f76 --- /dev/null +++ b/src/main/java/com/quickpick/ureca/ticket/v1/domain/TicketV1.java @@ -0,0 +1,53 @@ +package com.quickpick.ureca.ticket.v1.domain; + +import com.quickpick.ureca.common.domain.BaseEntity; +import com.quickpick.ureca.userticket.domain.UserTicket; +import com.quickpick.ureca.userticket.v1.domain.UserTicketV1; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "ticketv1") +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TicketV1{ + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ticket_id") + private Long ticketId; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private int quantity; + + @Version + private Long version; + + @OneToMany(mappedBy = "ticket", cascade = CascadeType.ALL, orphanRemoval = true) + private List userTickets = new ArrayList<>(); + + // 재고 감소 메서드 + public void decreaseCount() { + if (this.quantity > 0) { + this.quantity--; + } else { + throw new RuntimeException("티켓이 매진되었습니다."); + } + } + + public TicketV1(String skt_콘서트, int i) { + this.name = skt_콘서트; + this.quantity = i; + } + +} \ No newline at end of file diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java b/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java new file mode 100644 index 0000000..53d7f7d --- /dev/null +++ b/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java @@ -0,0 +1,21 @@ +package com.quickpick.ureca.ticket.v1.repository; + +import com.quickpick.ureca.ticket.v1.domain.TicketV1; +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.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Repository +public interface TicketRepositoryV1 extends JpaRepository { + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("select t from TicketV1 t where t.ticketId = :ticketId") + Optional findByIdForUpdate(Long ticketId); + +} diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/service/TicketServiceV1.java b/src/main/java/com/quickpick/ureca/ticket/v1/service/TicketServiceV1.java new file mode 100644 index 0000000..9d522df --- /dev/null +++ b/src/main/java/com/quickpick/ureca/ticket/v1/service/TicketServiceV1.java @@ -0,0 +1,7 @@ +package com.quickpick.ureca.ticket.v1.service; + +import org.springframework.stereotype.Service; + +@Service +public class TicketServiceV1 { +} diff --git a/src/main/java/com/quickpick/ureca/user/domain/User.java b/src/main/java/com/quickpick/ureca/user/domain/User.java index 86eaaca..9bddf8b 100644 --- a/src/main/java/com/quickpick/ureca/user/domain/User.java +++ b/src/main/java/com/quickpick/ureca/user/domain/User.java @@ -3,6 +3,8 @@ import com.quickpick.ureca.common.domain.BaseEntity; import com.quickpick.ureca.userticket.domain.UserTicket; import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -12,7 +14,9 @@ @Table @Entity @Getter +@Builder @NoArgsConstructor +@AllArgsConstructor public class User extends BaseEntity { @Id @@ -38,4 +42,4 @@ public class User extends BaseEntity { @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private List userTickets = new ArrayList<>(); -} +} \ No newline at end of file diff --git a/src/main/java/com/quickpick/ureca/user/repository/UserRepository.java b/src/main/java/com/quickpick/ureca/user/repository/UserRepository.java index 50abb0e..ca67b7d 100644 --- a/src/main/java/com/quickpick/ureca/user/repository/UserRepository.java +++ b/src/main/java/com/quickpick/ureca/user/repository/UserRepository.java @@ -1,4 +1,7 @@ package com.quickpick.ureca.user.repository; -public class UserRepository { -} +import com.quickpick.ureca.user.domain.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/quickpick/ureca/user/v1/domain/UserV1.java b/src/main/java/com/quickpick/ureca/user/v1/domain/UserV1.java new file mode 100644 index 0000000..af9f68a --- /dev/null +++ b/src/main/java/com/quickpick/ureca/user/v1/domain/UserV1.java @@ -0,0 +1,36 @@ +package com.quickpick.ureca.user.v1.domain; + +import com.quickpick.ureca.common.domain.BaseEntity; +import com.quickpick.ureca.userticket.domain.UserTicket; +import com.quickpick.ureca.userticket.v1.domain.UserTicketV1; +import jakarta.persistence.*; +import lombok.*; + +import java.util.ArrayList; +import java.util.List; + +@Table +@Entity +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserV1{ + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_id") + private Long userId; + + @Column(nullable = false) + private String id; + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private List userTickets = new ArrayList<>(); + + public UserV1(String id) { + this.id = id; + } + +} diff --git a/src/main/java/com/quickpick/ureca/user/v1/repository/UserRepositoryV1.java b/src/main/java/com/quickpick/ureca/user/v1/repository/UserRepositoryV1.java new file mode 100644 index 0000000..b490d87 --- /dev/null +++ b/src/main/java/com/quickpick/ureca/user/v1/repository/UserRepositoryV1.java @@ -0,0 +1,9 @@ +package com.quickpick.ureca.user.v1.repository; + +import com.quickpick.ureca.user.v1.domain.UserV1; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepositoryV1 extends JpaRepository { +} diff --git a/src/main/java/com/quickpick/ureca/user/v1/service/UserBulkInsertService.java b/src/main/java/com/quickpick/ureca/user/v1/service/UserBulkInsertService.java new file mode 100644 index 0000000..cc66027 --- /dev/null +++ b/src/main/java/com/quickpick/ureca/user/v1/service/UserBulkInsertService.java @@ -0,0 +1,34 @@ +package com.quickpick.ureca.user.v1.service; + +import com.quickpick.ureca.user.v1.domain.UserV1; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Service +public class UserBulkInsertService { + + @PersistenceContext + private EntityManager entityManager; + + @Transactional + public void insertUsersInBulk(int userCount) { + List users = new ArrayList<>(); + for (int i = 0; i < userCount; i++) { + users.add(new UserV1("user" + i)); + } + + int batchSize = 100; + for (int i = 0; i < users.size(); i++) { + entityManager.persist(users.get(i)); + if (i % batchSize == 0) { + entityManager.flush(); + entityManager.clear(); + } + } + } +} diff --git a/src/main/java/com/quickpick/ureca/user/v1/service/UserServiceV1.java b/src/main/java/com/quickpick/ureca/user/v1/service/UserServiceV1.java new file mode 100644 index 0000000..f247909 --- /dev/null +++ b/src/main/java/com/quickpick/ureca/user/v1/service/UserServiceV1.java @@ -0,0 +1,7 @@ +package com.quickpick.ureca.user.v1.service; + +import org.springframework.stereotype.Service; + +@Service +public class UserServiceV1 { +} diff --git a/src/main/java/com/quickpick/ureca/userticket/v1/domain/UserTicketV1.java b/src/main/java/com/quickpick/ureca/userticket/v1/domain/UserTicketV1.java new file mode 100644 index 0000000..3215cdd --- /dev/null +++ b/src/main/java/com/quickpick/ureca/userticket/v1/domain/UserTicketV1.java @@ -0,0 +1,34 @@ +package com.quickpick.ureca.userticket.v1.domain; + +import com.quickpick.ureca.ticket.domain.Ticket; +import com.quickpick.ureca.ticket.v1.domain.TicketV1; +import com.quickpick.ureca.user.domain.User; +import com.quickpick.ureca.user.v1.domain.UserV1; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table +@Getter +@NoArgsConstructor +public class UserTicketV1 { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_ticket_id") + private Long userTicketId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private UserV1 user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "ticket_id") + private TicketV1 ticket; + + public UserTicketV1(UserV1 user, TicketV1 ticket) { + this.user = user; + this.ticket = ticket; + } +} \ No newline at end of file diff --git a/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketRepositoryV1.java b/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketRepositoryV1.java new file mode 100644 index 0000000..b7c169e --- /dev/null +++ b/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketRepositoryV1.java @@ -0,0 +1,7 @@ +package com.quickpick.ureca.userticket.v1.repository; + +import com.quickpick.ureca.userticket.v1.domain.UserTicketV1; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserTicketRepositoryV1 extends JpaRepository { +} diff --git a/src/test/java/com/quickpick/ureca/v1/TicketReservationServiceTest.java b/src/test/java/com/quickpick/ureca/v1/TicketReservationServiceTest.java new file mode 100644 index 0000000..3c7884b --- /dev/null +++ b/src/test/java/com/quickpick/ureca/v1/TicketReservationServiceTest.java @@ -0,0 +1,73 @@ +package com.quickpick.ureca.v1; + +import com.quickpick.ureca.reserve.v1.service.ReserveServiceV1; +import com.quickpick.ureca.ticket.v1.domain.TicketV1; +import com.quickpick.ureca.ticket.v1.repository.TicketRepositoryV1; +import com.quickpick.ureca.user.v1.domain.UserV1; +import com.quickpick.ureca.user.v1.repository.UserRepositoryV1; +import com.quickpick.ureca.user.v1.service.UserBulkInsertService; +import com.quickpick.ureca.userticket.v1.domain.UserTicketV1; +import com.quickpick.ureca.userticket.v1.repository.UserTicketRepositoryV1; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest +class TicketReservationServiceTest { + + @Autowired private TicketRepositoryV1 ticketRepositoryV1; + @Autowired private UserRepositoryV1 userRepositoryV1; + @Autowired private UserTicketRepositoryV1 userTicketRepositoryV1; + @Autowired private ReserveServiceV1 reserveServiceV1; + + @Autowired + private UserBulkInsertService userBulkInsertService; + + @Test + @DisplayName("동시에 1000개의 요청으로 100개의 티켓을 예약한다.") + void PessimisticReservationTest() throws InterruptedException { + int userCount = 30000; + int ticketQuantity = 100; + + TicketV1 ticket = new TicketV1("SKT 콘서트", ticketQuantity); + ticketRepositoryV1.save(ticket); + + userBulkInsertService.insertUsersInBulk(userCount); + + ExecutorService executorService = Executors.newFixedThreadPool(32); + CountDownLatch latch = new CountDownLatch(userCount); + + List allUsers = userRepositoryV1.findAll(); + + for (UserV1 user : allUsers) { + executorService.submit(() -> { + try { + reserveServiceV1.reserveTicket(user.getUserId(), ticket.getTicketId()); + } catch (Exception ignored) { + } finally { + latch.countDown(); + } + }); + } + + latch.await(); + + List reservations = userTicketRepositoryV1.findAll(); + System.out.println("총 예약 수: " + reservations.size()); + assertEquals(ticketQuantity, reservations.size()); + } +} From c9cf182b85981560413cf0e94683d2d3f3d165c0 Mon Sep 17 00:00:00 2001 From: ghdtmdalsda Date: Fri, 2 May 2025 17:17:39 +0900 Subject: [PATCH 02/14] =?UTF-8?q?Refactor=20:=20unused=20import=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/quickpick/ureca/ticket/v1/domain/TicketV1.java | 3 --- .../ureca/ticket/v1/repository/TicketRepositoryV1.java | 2 -- .../java/com/quickpick/ureca/user/v1/domain/UserV1.java | 2 -- .../ureca/userticket/v1/domain/UserTicketV1.java | 2 -- .../quickpick/ureca/v1/TicketReservationServiceTest.java | 8 ++------ 5 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/domain/TicketV1.java b/src/main/java/com/quickpick/ureca/ticket/v1/domain/TicketV1.java index 47e5f76..12a2363 100644 --- a/src/main/java/com/quickpick/ureca/ticket/v1/domain/TicketV1.java +++ b/src/main/java/com/quickpick/ureca/ticket/v1/domain/TicketV1.java @@ -1,12 +1,9 @@ package com.quickpick.ureca.ticket.v1.domain; -import com.quickpick.ureca.common.domain.BaseEntity; -import com.quickpick.ureca.userticket.domain.UserTicket; import com.quickpick.ureca.userticket.v1.domain.UserTicketV1; import jakarta.persistence.*; import lombok.*; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java b/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java index 53d7f7d..6418145 100644 --- a/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java +++ b/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java @@ -5,9 +5,7 @@ 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.repository.query.Param; import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; import java.util.Optional; diff --git a/src/main/java/com/quickpick/ureca/user/v1/domain/UserV1.java b/src/main/java/com/quickpick/ureca/user/v1/domain/UserV1.java index af9f68a..1e8c22b 100644 --- a/src/main/java/com/quickpick/ureca/user/v1/domain/UserV1.java +++ b/src/main/java/com/quickpick/ureca/user/v1/domain/UserV1.java @@ -1,7 +1,5 @@ package com.quickpick.ureca.user.v1.domain; -import com.quickpick.ureca.common.domain.BaseEntity; -import com.quickpick.ureca.userticket.domain.UserTicket; import com.quickpick.ureca.userticket.v1.domain.UserTicketV1; import jakarta.persistence.*; import lombok.*; diff --git a/src/main/java/com/quickpick/ureca/userticket/v1/domain/UserTicketV1.java b/src/main/java/com/quickpick/ureca/userticket/v1/domain/UserTicketV1.java index 3215cdd..6ca4106 100644 --- a/src/main/java/com/quickpick/ureca/userticket/v1/domain/UserTicketV1.java +++ b/src/main/java/com/quickpick/ureca/userticket/v1/domain/UserTicketV1.java @@ -1,8 +1,6 @@ package com.quickpick.ureca.userticket.v1.domain; -import com.quickpick.ureca.ticket.domain.Ticket; import com.quickpick.ureca.ticket.v1.domain.TicketV1; -import com.quickpick.ureca.user.domain.User; import com.quickpick.ureca.user.v1.domain.UserV1; import jakarta.persistence.*; import lombok.Getter; diff --git a/src/test/java/com/quickpick/ureca/v1/TicketReservationServiceTest.java b/src/test/java/com/quickpick/ureca/v1/TicketReservationServiceTest.java index 3c7884b..f5c58f0 100644 --- a/src/test/java/com/quickpick/ureca/v1/TicketReservationServiceTest.java +++ b/src/test/java/com/quickpick/ureca/v1/TicketReservationServiceTest.java @@ -8,17 +8,13 @@ import com.quickpick.ureca.user.v1.service.UserBulkInsertService; import com.quickpick.ureca.userticket.v1.domain.UserTicketV1; import com.quickpick.ureca.userticket.v1.repository.UserTicketRepositoryV1; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.domain.EntityScan; + import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; -import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; From e8e7c0044f70e617124da51ad41dbeade07e5db6 Mon Sep 17 00:00:00 2001 From: ghdtmdalsda Date: Fri, 2 May 2025 17:18:39 +0900 Subject: [PATCH 03/14] Feat : V1_Test2(Pessimistic Lock) --- .../reserve/v1/service/ReserveServiceV1.java | 69 ++++++++++--------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java b/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java index 515daa9..7edd453 100644 --- a/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java +++ b/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java @@ -26,46 +26,47 @@ public class ReserveServiceV1 { @Autowired private UserTicketRepositoryV1 userTicketRepositoryV1; - // 티켓 예약 메서드 (락 X) - @Transactional - public void reserveTicket(Long userId, Long ticketId) { - // 티켓과 사용자 가져오기 - TicketV1 ticket = ticketRepositoryV1.findById(ticketId).orElseThrow(() -> new RuntimeException("티켓을 찾을 수 없습니다.")); - UserV1 user = userRepositoryV1.findById(userId).orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")); - - // 티켓 재고가 없으면 예외 발생 - if (ticket.getQuantity() <= 0) { - throw new RuntimeException("매진되었습니다."); - } - - // 티켓 재고 감소 - System.out.println("감소!"); - ticket.decreaseCount(); - ticketRepositoryV1.save(ticket); // 재고 감소 후 저장 - - // 예약 정보 저장 - UserTicketV1 userTicket = new UserTicketV1(user, ticket); - userTicketRepositoryV1.save(userTicket); - } - +// // 티켓 예약 메서드 (락 X) // @Transactional // public void reserveTicket(Long userId, Long ticketId) { +// // 티켓과 사용자 가져오기 +// TicketV1 ticket = ticketRepositoryV1.findById(ticketId).orElseThrow(() -> new RuntimeException("티켓을 찾을 수 없습니다.")); +// UserV1 user = userRepositoryV1.findById(userId).orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")); // -// log.info(">>> reserveTicket called: userId = {}, ticketId = {}", userId, ticketId); -// -// UserV1 user = userRepositoryV1.findById(userId) -// .orElseThrow(() -> new IllegalArgumentException("User not found")); -// -// System.out.println("Reserve Ticket"); -// TicketV1 ticket = ticketRepositoryV1.findByIdForUpdate(ticketId) -// .orElseThrow(() -> new IllegalArgumentException("Ticket not found")); -// +// // 티켓 재고가 없으면 예외 발생 // if (ticket.getQuantity() <= 0) { -// throw new IllegalStateException("Ticket out of stock"); +// throw new RuntimeException("매진되었습니다."); // } // -// ticket.setQuantity(ticket.getQuantity() - 1); -// userTicketRepositoryV1.save(new UserTicketV1(user, ticket)); +// // 티켓 재고 감소 +// System.out.println("감소!"); +// ticket.decreaseCount(); +// ticketRepositoryV1.save(ticket); // 재고 감소 후 저장 +// +// // 예약 정보 저장 +// UserTicketV1 userTicket = new UserTicketV1(user, ticket); +// userTicketRepositoryV1.save(userTicket); // } + // 티켓 예약 메서드 (비관적 락) - Pessimistic Lock + @Transactional + public void reserveTicket(Long userId, Long ticketId) { + + log.info(">>> reserveTicket called: userId = {}, ticketId = {}", userId, ticketId); + + UserV1 user = userRepositoryV1.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("User not found")); + + System.out.println("Reserve Ticket"); + TicketV1 ticket = ticketRepositoryV1.findByIdForUpdate(ticketId) + .orElseThrow(() -> new IllegalArgumentException("Ticket not found")); + + if (ticket.getQuantity() <= 0) { + throw new IllegalStateException("Ticket out of stock"); + } + + ticket.setQuantity(ticket.getQuantity() - 1); + userTicketRepositoryV1.save(new UserTicketV1(user, ticket)); + } + } \ No newline at end of file From 83a041c32270fcd19ffe821fddf779227a2dc4fc Mon Sep 17 00:00:00 2001 From: ghdtmdalsda Date: Fri, 2 May 2025 17:32:15 +0900 Subject: [PATCH 04/14] Feat : V1_Test2(Pessimistic Lock) --- .../quickpick/ureca/reserve/v1/service/ReserveServiceV1.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java b/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java index 7edd453..c42c3fb 100644 --- a/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java +++ b/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java @@ -26,7 +26,7 @@ public class ReserveServiceV1 { @Autowired private UserTicketRepositoryV1 userTicketRepositoryV1; -// // 티켓 예약 메서드 (락 X) +// // 티켓 예약 메서드 (락 X) Average : 586, Throughput : 17.5/sec // @Transactional // public void reserveTicket(Long userId, Long ticketId) { // // 티켓과 사용자 가져오기 @@ -48,7 +48,7 @@ public class ReserveServiceV1 { // userTicketRepositoryV1.save(userTicket); // } - // 티켓 예약 메서드 (비관적 락) - Pessimistic Lock + // 티켓 예약 메서드 (비관적 락) - Pessimistic Lock Average : 802, Throughput : 15.6/sec @Transactional public void reserveTicket(Long userId, Long ticketId) { From 10bf5f4502edd8851a0fd145eb748a9e2814bbe4 Mon Sep 17 00:00:00 2001 From: ghdtmdalsda Date: Mon, 5 May 2025 00:24:12 +0900 Subject: [PATCH 05/14] Feat : V1_Test2(open-in-view + FetchJoin + DTO) --- .../reserve/v1/service/ReserveServiceV1.java | 39 ++++++++++++++++--- .../v1/controller/TicketControllerV1.java | 29 +++++++++++--- .../ticket/v1/dto/TicketReserveResponse.java | 20 ++++++++++ .../v1/repository/TicketRepositoryV1.java | 10 +++++ 4 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/quickpick/ureca/ticket/v1/dto/TicketReserveResponse.java diff --git a/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java b/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java index c42c3fb..5ce9e93 100644 --- a/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java +++ b/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java @@ -10,7 +10,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; import lombok.extern.slf4j.Slf4j; @Slf4j @@ -26,6 +25,7 @@ public class ReserveServiceV1 { @Autowired private UserTicketRepositoryV1 userTicketRepositoryV1; + // 1. // // 티켓 예약 메서드 (락 X) Average : 586, Throughput : 17.5/sec // @Transactional // public void reserveTicket(Long userId, Long ticketId) { @@ -48,17 +48,44 @@ public class ReserveServiceV1 { // userTicketRepositoryV1.save(userTicket); // } - // 티켓 예약 메서드 (비관적 락) - Pessimistic Lock Average : 802, Throughput : 15.6/sec + + // 2. +// // 티켓 예약 메서드 (비관적 락) - Pessimistic Lock Average : 802, Throughput : 15.6/sec +// @Transactional +// public void reserveTicket(Long userId, Long ticketId) { +// +// log.info(">>> reserveTicket called: userId = {}, ticketId = {}", userId, ticketId); +// +// UserV1 user = userRepositoryV1.findById(userId) +// .orElseThrow(() -> new IllegalArgumentException("User not found")); +// +// System.out.println("Reserve Ticket"); +// TicketV1 ticket = ticketRepositoryV1.findByIdForUpdate(ticketId) +// .orElseThrow(() -> new IllegalArgumentException("Ticket not found")); +// +// if (ticket.getQuantity() <= 0) { +// throw new IllegalStateException("Ticket out of stock"); +// } +// +// ticket.setQuantity(ticket.getQuantity() - 1); +// userTicketRepositoryV1.save(new UserTicketV1(user, ticket)); +// } + + // 3. + // 티켓 예약 메서드 (비관적 락 + open-in-view) False Average : 6676, Throughput : 602.6/sec + // 티켓 예약 메서드 (open-in-view + FetchJoin + DTO) False Average : 69005, Throughput : 64.9/sec + // 티켓 예약 메서드 (비관적 락 + open-in-view) True Average : 4818, Throughput : 723.2/sec @Transactional - public void reserveTicket(Long userId, Long ticketId) { + public TicketV1 reserveTicket(Long userId, Long ticketId) { log.info(">>> reserveTicket called: userId = {}, ticketId = {}", userId, ticketId); + // user는 fetch join 하지 않았으므로 별도로 조회 UserV1 user = userRepositoryV1.findById(userId) .orElseThrow(() -> new IllegalArgumentException("User not found")); - System.out.println("Reserve Ticket"); - TicketV1 ticket = ticketRepositoryV1.findByIdForUpdate(ticketId) + // fetch join으로 모든 필요한 정보 로딩 + TicketV1 ticket = ticketRepositoryV1.findByIdForUpdateWithUsers(ticketId) .orElseThrow(() -> new IllegalArgumentException("Ticket not found")); if (ticket.getQuantity() <= 0) { @@ -67,6 +94,8 @@ public void reserveTicket(Long userId, Long ticketId) { ticket.setQuantity(ticket.getQuantity() - 1); userTicketRepositoryV1.save(new UserTicketV1(user, ticket)); + return ticket; } + } \ No newline at end of file diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java b/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java index 6e3f2d7..df0c674 100644 --- a/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java +++ b/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java @@ -1,6 +1,10 @@ package com.quickpick.ureca.ticket.v1.controller; import com.quickpick.ureca.reserve.v1.service.ReserveServiceV1; +import com.quickpick.ureca.ticket.v1.domain.TicketV1; +import com.quickpick.ureca.ticket.v1.dto.TicketReserveResponse; +import com.quickpick.ureca.user.v1.domain.UserV1; +import com.quickpick.ureca.user.v1.repository.UserRepositoryV1; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -15,19 +19,34 @@ public class TicketControllerV1 { private final ReserveServiceV1 reserveServiceV1; + private final UserRepositoryV1 userRepositoryV1; - public TicketControllerV1(ReserveServiceV1 reserveServiceV1) { + public TicketControllerV1(ReserveServiceV1 reserveServiceV1, UserRepositoryV1 userRepositoryV1) { this.reserveServiceV1 = reserveServiceV1; + this.userRepositoryV1 = userRepositoryV1; } +// @PostMapping +// public ResponseEntity reserve(@RequestParam Long userId, @RequestParam Long ticketId) { +// try { +// reserveServiceV1.reserveTicket(userId, ticketId); +// return ResponseEntity.ok("예약 성공"); +// } catch (Exception e) { +// log.error("예약 실패: {}", e.getMessage()); +// return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("예약 실패: " + e.getMessage()); +// } +// } + @PostMapping - public ResponseEntity reserve(@RequestParam Long userId, @RequestParam Long ticketId) { + public ResponseEntity reserve(@RequestParam Long userId, @RequestParam Long ticketId) { try { - reserveServiceV1.reserveTicket(userId, ticketId); - return ResponseEntity.ok("예약 성공"); + TicketV1 ticket = reserveServiceV1.reserveTicket(userId, ticketId); + UserV1 user = userRepositoryV1.findById(userId).orElseThrow(); + return ResponseEntity.ok(TicketReserveResponse.of(ticket, user)); } catch (Exception e) { log.error("예약 실패: {}", e.getMessage()); - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("예약 실패: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null); } } + } diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/dto/TicketReserveResponse.java b/src/main/java/com/quickpick/ureca/ticket/v1/dto/TicketReserveResponse.java new file mode 100644 index 0000000..dd52b75 --- /dev/null +++ b/src/main/java/com/quickpick/ureca/ticket/v1/dto/TicketReserveResponse.java @@ -0,0 +1,20 @@ +package com.quickpick.ureca.ticket.v1.dto; + +import com.quickpick.ureca.ticket.v1.domain.TicketV1; +import com.quickpick.ureca.user.v1.domain.UserV1; + +public record TicketReserveResponse( + Long ticketId, + String ticketName, + int remainingQuantity, + String reservedByUsername +) { + public static TicketReserveResponse of(TicketV1 ticket, UserV1 user) { + return new TicketReserveResponse( + ticket.getTicketId(), + ticket.getName(), + ticket.getQuantity(), + user.getId() + ); + } +} diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java b/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java index 6418145..3d37a75 100644 --- a/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java +++ b/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java @@ -16,4 +16,14 @@ public interface TicketRepositoryV1 extends JpaRepository { @Query("select t from TicketV1 t where t.ticketId = :ticketId") Optional findByIdForUpdate(Long ticketId); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query(""" + select t from TicketV1 t + left join fetch t.userTickets ut + left join fetch ut.user + where t.ticketId = :ticketId + """) + Optional findByIdForUpdateWithUsers(Long ticketId); + } From 5b44607ceee0274129debe98d0a9f96dcf4f090f Mon Sep 17 00:00:00 2001 From: ghdtmdalsda Date: Mon, 5 May 2025 00:24:12 +0900 Subject: [PATCH 06/14] Feat : V1_Test3(open-in-view + FetchJoin + DTO) --- .../reserve/v1/service/ReserveServiceV1.java | 39 ++++++++++++++++--- .../v1/controller/TicketControllerV1.java | 29 +++++++++++--- .../ticket/v1/dto/TicketReserveResponse.java | 20 ++++++++++ .../v1/repository/TicketRepositoryV1.java | 10 +++++ 4 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/quickpick/ureca/ticket/v1/dto/TicketReserveResponse.java diff --git a/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java b/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java index c42c3fb..5ce9e93 100644 --- a/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java +++ b/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java @@ -10,7 +10,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; import lombok.extern.slf4j.Slf4j; @Slf4j @@ -26,6 +25,7 @@ public class ReserveServiceV1 { @Autowired private UserTicketRepositoryV1 userTicketRepositoryV1; + // 1. // // 티켓 예약 메서드 (락 X) Average : 586, Throughput : 17.5/sec // @Transactional // public void reserveTicket(Long userId, Long ticketId) { @@ -48,17 +48,44 @@ public class ReserveServiceV1 { // userTicketRepositoryV1.save(userTicket); // } - // 티켓 예약 메서드 (비관적 락) - Pessimistic Lock Average : 802, Throughput : 15.6/sec + + // 2. +// // 티켓 예약 메서드 (비관적 락) - Pessimistic Lock Average : 802, Throughput : 15.6/sec +// @Transactional +// public void reserveTicket(Long userId, Long ticketId) { +// +// log.info(">>> reserveTicket called: userId = {}, ticketId = {}", userId, ticketId); +// +// UserV1 user = userRepositoryV1.findById(userId) +// .orElseThrow(() -> new IllegalArgumentException("User not found")); +// +// System.out.println("Reserve Ticket"); +// TicketV1 ticket = ticketRepositoryV1.findByIdForUpdate(ticketId) +// .orElseThrow(() -> new IllegalArgumentException("Ticket not found")); +// +// if (ticket.getQuantity() <= 0) { +// throw new IllegalStateException("Ticket out of stock"); +// } +// +// ticket.setQuantity(ticket.getQuantity() - 1); +// userTicketRepositoryV1.save(new UserTicketV1(user, ticket)); +// } + + // 3. + // 티켓 예약 메서드 (비관적 락 + open-in-view) False Average : 6676, Throughput : 602.6/sec + // 티켓 예약 메서드 (open-in-view + FetchJoin + DTO) False Average : 69005, Throughput : 64.9/sec + // 티켓 예약 메서드 (비관적 락 + open-in-view) True Average : 4818, Throughput : 723.2/sec @Transactional - public void reserveTicket(Long userId, Long ticketId) { + public TicketV1 reserveTicket(Long userId, Long ticketId) { log.info(">>> reserveTicket called: userId = {}, ticketId = {}", userId, ticketId); + // user는 fetch join 하지 않았으므로 별도로 조회 UserV1 user = userRepositoryV1.findById(userId) .orElseThrow(() -> new IllegalArgumentException("User not found")); - System.out.println("Reserve Ticket"); - TicketV1 ticket = ticketRepositoryV1.findByIdForUpdate(ticketId) + // fetch join으로 모든 필요한 정보 로딩 + TicketV1 ticket = ticketRepositoryV1.findByIdForUpdateWithUsers(ticketId) .orElseThrow(() -> new IllegalArgumentException("Ticket not found")); if (ticket.getQuantity() <= 0) { @@ -67,6 +94,8 @@ public void reserveTicket(Long userId, Long ticketId) { ticket.setQuantity(ticket.getQuantity() - 1); userTicketRepositoryV1.save(new UserTicketV1(user, ticket)); + return ticket; } + } \ No newline at end of file diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java b/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java index 6e3f2d7..df0c674 100644 --- a/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java +++ b/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java @@ -1,6 +1,10 @@ package com.quickpick.ureca.ticket.v1.controller; import com.quickpick.ureca.reserve.v1.service.ReserveServiceV1; +import com.quickpick.ureca.ticket.v1.domain.TicketV1; +import com.quickpick.ureca.ticket.v1.dto.TicketReserveResponse; +import com.quickpick.ureca.user.v1.domain.UserV1; +import com.quickpick.ureca.user.v1.repository.UserRepositoryV1; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -15,19 +19,34 @@ public class TicketControllerV1 { private final ReserveServiceV1 reserveServiceV1; + private final UserRepositoryV1 userRepositoryV1; - public TicketControllerV1(ReserveServiceV1 reserveServiceV1) { + public TicketControllerV1(ReserveServiceV1 reserveServiceV1, UserRepositoryV1 userRepositoryV1) { this.reserveServiceV1 = reserveServiceV1; + this.userRepositoryV1 = userRepositoryV1; } +// @PostMapping +// public ResponseEntity reserve(@RequestParam Long userId, @RequestParam Long ticketId) { +// try { +// reserveServiceV1.reserveTicket(userId, ticketId); +// return ResponseEntity.ok("예약 성공"); +// } catch (Exception e) { +// log.error("예약 실패: {}", e.getMessage()); +// return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("예약 실패: " + e.getMessage()); +// } +// } + @PostMapping - public ResponseEntity reserve(@RequestParam Long userId, @RequestParam Long ticketId) { + public ResponseEntity reserve(@RequestParam Long userId, @RequestParam Long ticketId) { try { - reserveServiceV1.reserveTicket(userId, ticketId); - return ResponseEntity.ok("예약 성공"); + TicketV1 ticket = reserveServiceV1.reserveTicket(userId, ticketId); + UserV1 user = userRepositoryV1.findById(userId).orElseThrow(); + return ResponseEntity.ok(TicketReserveResponse.of(ticket, user)); } catch (Exception e) { log.error("예약 실패: {}", e.getMessage()); - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("예약 실패: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null); } } + } diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/dto/TicketReserveResponse.java b/src/main/java/com/quickpick/ureca/ticket/v1/dto/TicketReserveResponse.java new file mode 100644 index 0000000..dd52b75 --- /dev/null +++ b/src/main/java/com/quickpick/ureca/ticket/v1/dto/TicketReserveResponse.java @@ -0,0 +1,20 @@ +package com.quickpick.ureca.ticket.v1.dto; + +import com.quickpick.ureca.ticket.v1.domain.TicketV1; +import com.quickpick.ureca.user.v1.domain.UserV1; + +public record TicketReserveResponse( + Long ticketId, + String ticketName, + int remainingQuantity, + String reservedByUsername +) { + public static TicketReserveResponse of(TicketV1 ticket, UserV1 user) { + return new TicketReserveResponse( + ticket.getTicketId(), + ticket.getName(), + ticket.getQuantity(), + user.getId() + ); + } +} diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java b/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java index 6418145..3d37a75 100644 --- a/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java +++ b/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java @@ -16,4 +16,14 @@ public interface TicketRepositoryV1 extends JpaRepository { @Query("select t from TicketV1 t where t.ticketId = :ticketId") Optional findByIdForUpdate(Long ticketId); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query(""" + select t from TicketV1 t + left join fetch t.userTickets ut + left join fetch ut.user + where t.ticketId = :ticketId + """) + Optional findByIdForUpdateWithUsers(Long ticketId); + } From 05a1b716e6302ca75d4e66b4451fb51d9dc6b77b Mon Sep 17 00:00:00 2001 From: ghdtmdalsda Date: Tue, 6 May 2025 19:38:49 +0900 Subject: [PATCH 07/14] =?UTF-8?q?Refactor=20:=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=ED=98=95=EC=8B=9D=20=ED=91=9C=EC=A4=80=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reserve/{ => v1}/domain/Reserve.java | 2 +- .../ureca/reserve/v1/domain/ReserveV1.java | 27 ------------ .../v1/repository/ReserveRepositoryV1.java | 4 +- .../reserve/v1/service/ReserveServiceV1.java | 22 +++++----- .../quickpick/ureca/ticket/domain/Ticket.java | 42 ------------------- .../ticket/repository/TicketRepository.java | 2 +- .../v1/controller/TicketControllerV1.java | 16 +++---- .../v1/domain/{TicketV1.java => Ticket.java} | 21 +++++++--- .../ticket/v1/dto/TicketReserveResponse.java | 6 +-- .../v1/repository/TicketRepositoryV1.java | 12 +++--- .../com/quickpick/ureca/user/domain/User.java | 14 ++++--- .../ureca/user/repository/UserRepository.java | 4 +- .../ureca/user/v1/domain/UserV1.java | 34 --------------- .../user/v1/repository/UserRepositoryV1.java | 9 ---- ...vice.java => UserBulkInsertServiceV1.java} | 8 ++-- .../ureca/user/v1/service/UserServiceV1.java | 7 ---- .../{ => v1}/domain/UserTicket.java | 8 +++- .../userticket/v1/domain/UserTicketV1.java | 32 -------------- .../v1/repository/UserTicketRepository.java | 7 ++++ .../v1/repository/UserTicketRepositoryV1.java | 7 ---- .../v1/TicketReservationServiceTest.java | 28 ++++++------- 21 files changed, 89 insertions(+), 223 deletions(-) rename src/main/java/com/quickpick/ureca/reserve/{ => v1}/domain/Reserve.java (93%) delete mode 100644 src/main/java/com/quickpick/ureca/reserve/v1/domain/ReserveV1.java delete mode 100644 src/main/java/com/quickpick/ureca/ticket/domain/Ticket.java rename src/main/java/com/quickpick/ureca/ticket/v1/domain/{TicketV1.java => Ticket.java} (63%) delete mode 100644 src/main/java/com/quickpick/ureca/user/v1/domain/UserV1.java delete mode 100644 src/main/java/com/quickpick/ureca/user/v1/repository/UserRepositoryV1.java rename src/main/java/com/quickpick/ureca/user/v1/service/{UserBulkInsertService.java => UserBulkInsertServiceV1.java} (81%) delete mode 100644 src/main/java/com/quickpick/ureca/user/v1/service/UserServiceV1.java rename src/main/java/com/quickpick/ureca/userticket/{ => v1}/domain/UserTicket.java (71%) delete mode 100644 src/main/java/com/quickpick/ureca/userticket/v1/domain/UserTicketV1.java create mode 100644 src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketRepository.java delete mode 100644 src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketRepositoryV1.java diff --git a/src/main/java/com/quickpick/ureca/reserve/domain/Reserve.java b/src/main/java/com/quickpick/ureca/reserve/v1/domain/Reserve.java similarity index 93% rename from src/main/java/com/quickpick/ureca/reserve/domain/Reserve.java rename to src/main/java/com/quickpick/ureca/reserve/v1/domain/Reserve.java index 870da94..df33bfa 100644 --- a/src/main/java/com/quickpick/ureca/reserve/domain/Reserve.java +++ b/src/main/java/com/quickpick/ureca/reserve/v1/domain/Reserve.java @@ -1,4 +1,4 @@ -package com.quickpick.ureca.reserve.domain; +package com.quickpick.ureca.reserve.v1.domain; import com.quickpick.ureca.common.domain.BaseEntity; import com.quickpick.ureca.reserve.status.ReserveStatus; diff --git a/src/main/java/com/quickpick/ureca/reserve/v1/domain/ReserveV1.java b/src/main/java/com/quickpick/ureca/reserve/v1/domain/ReserveV1.java deleted file mode 100644 index fc4596a..0000000 --- a/src/main/java/com/quickpick/ureca/reserve/v1/domain/ReserveV1.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.quickpick.ureca.reserve.v1.domain; - -import com.quickpick.ureca.reserve.status.ReserveStatus; -import com.quickpick.ureca.user.v1.domain.UserV1; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Table -@Entity -@Getter -@NoArgsConstructor -public class ReserveV1{ - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "reserve_id") - private Long reserveId; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - private UserV1 user; - - @Enumerated(EnumType.STRING) - @Column(nullable = false) - private ReserveStatus status; -} diff --git a/src/main/java/com/quickpick/ureca/reserve/v1/repository/ReserveRepositoryV1.java b/src/main/java/com/quickpick/ureca/reserve/v1/repository/ReserveRepositoryV1.java index 17bfc64..0a21b3c 100644 --- a/src/main/java/com/quickpick/ureca/reserve/v1/repository/ReserveRepositoryV1.java +++ b/src/main/java/com/quickpick/ureca/reserve/v1/repository/ReserveRepositoryV1.java @@ -1,9 +1,9 @@ package com.quickpick.ureca.reserve.v1.repository; -import com.quickpick.ureca.reserve.v1.domain.ReserveV1; +import com.quickpick.ureca.reserve.v1.domain.Reserve; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository -public interface ReserveRepositoryV1 extends JpaRepository { +public interface ReserveRepositoryV1 extends JpaRepository { } diff --git a/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java b/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java index 5ce9e93..16aca87 100644 --- a/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java +++ b/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java @@ -1,11 +1,11 @@ package com.quickpick.ureca.reserve.v1.service; -import com.quickpick.ureca.ticket.v1.domain.TicketV1; +import com.quickpick.ureca.ticket.v1.domain.Ticket; import com.quickpick.ureca.ticket.v1.repository.TicketRepositoryV1; -import com.quickpick.ureca.user.v1.domain.UserV1; -import com.quickpick.ureca.user.v1.repository.UserRepositoryV1; -import com.quickpick.ureca.userticket.v1.domain.UserTicketV1; -import com.quickpick.ureca.userticket.v1.repository.UserTicketRepositoryV1; +import com.quickpick.ureca.user.domain.User; +import com.quickpick.ureca.user.repository.UserRepository; +import com.quickpick.ureca.userticket.v1.domain.UserTicket; +import com.quickpick.ureca.userticket.v1.repository.UserTicketRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -20,10 +20,10 @@ public class ReserveServiceV1 { private TicketRepositoryV1 ticketRepositoryV1; @Autowired - private UserRepositoryV1 userRepositoryV1; + private UserRepository userRepository; @Autowired - private UserTicketRepositoryV1 userTicketRepositoryV1; + private UserTicketRepository userTicketRepository; // 1. // // 티켓 예약 메서드 (락 X) Average : 586, Throughput : 17.5/sec @@ -76,16 +76,16 @@ public class ReserveServiceV1 { // 티켓 예약 메서드 (open-in-view + FetchJoin + DTO) False Average : 69005, Throughput : 64.9/sec // 티켓 예약 메서드 (비관적 락 + open-in-view) True Average : 4818, Throughput : 723.2/sec @Transactional - public TicketV1 reserveTicket(Long userId, Long ticketId) { + public Ticket reserveTicket(Long userId, Long ticketId) { log.info(">>> reserveTicket called: userId = {}, ticketId = {}", userId, ticketId); // user는 fetch join 하지 않았으므로 별도로 조회 - UserV1 user = userRepositoryV1.findById(userId) + User user = userRepository.findById(userId) .orElseThrow(() -> new IllegalArgumentException("User not found")); // fetch join으로 모든 필요한 정보 로딩 - TicketV1 ticket = ticketRepositoryV1.findByIdForUpdateWithUsers(ticketId) + Ticket ticket = ticketRepositoryV1.findByIdForUpdateWithUsers(ticketId) .orElseThrow(() -> new IllegalArgumentException("Ticket not found")); if (ticket.getQuantity() <= 0) { @@ -93,7 +93,7 @@ public TicketV1 reserveTicket(Long userId, Long ticketId) { } ticket.setQuantity(ticket.getQuantity() - 1); - userTicketRepositoryV1.save(new UserTicketV1(user, ticket)); + userTicketRepository.save(new UserTicket(user, ticket)); return ticket; } diff --git a/src/main/java/com/quickpick/ureca/ticket/domain/Ticket.java b/src/main/java/com/quickpick/ureca/ticket/domain/Ticket.java deleted file mode 100644 index f5dbf27..0000000 --- a/src/main/java/com/quickpick/ureca/ticket/domain/Ticket.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.quickpick.ureca.ticket.domain; - -import com.quickpick.ureca.common.domain.BaseEntity; -import com.quickpick.ureca.userticket.domain.UserTicket; -import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -@Entity -@Table(name = "ticket") -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class Ticket extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "ticket_id") - private Long ticketId; - - @Column(nullable = false) - private String name; - - @Column(nullable = false) - private int quantity; - - @Column(nullable = false) - private LocalDateTime startDate; - - @Column(nullable = false) - private LocalDateTime reserveDate; - - @OneToMany(mappedBy = "ticket", cascade = CascadeType.ALL, orphanRemoval = true) - private List userTickets = new ArrayList<>(); -} diff --git a/src/main/java/com/quickpick/ureca/ticket/repository/TicketRepository.java b/src/main/java/com/quickpick/ureca/ticket/repository/TicketRepository.java index 34b9f66..24b75e0 100644 --- a/src/main/java/com/quickpick/ureca/ticket/repository/TicketRepository.java +++ b/src/main/java/com/quickpick/ureca/ticket/repository/TicketRepository.java @@ -1,6 +1,6 @@ package com.quickpick.ureca.ticket.repository; -import com.quickpick.ureca.ticket.domain.Ticket; +import com.quickpick.ureca.ticket.v1.domain.Ticket; import org.springframework.data.jpa.repository.JpaRepository; public interface TicketRepository extends JpaRepository { diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java b/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java index df0c674..85974a4 100644 --- a/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java +++ b/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java @@ -1,10 +1,10 @@ package com.quickpick.ureca.ticket.v1.controller; import com.quickpick.ureca.reserve.v1.service.ReserveServiceV1; -import com.quickpick.ureca.ticket.v1.domain.TicketV1; +import com.quickpick.ureca.ticket.v1.domain.Ticket; import com.quickpick.ureca.ticket.v1.dto.TicketReserveResponse; -import com.quickpick.ureca.user.v1.domain.UserV1; -import com.quickpick.ureca.user.v1.repository.UserRepositoryV1; +import com.quickpick.ureca.user.domain.User; +import com.quickpick.ureca.user.repository.UserRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -19,11 +19,11 @@ public class TicketControllerV1 { private final ReserveServiceV1 reserveServiceV1; - private final UserRepositoryV1 userRepositoryV1; + private final UserRepository userRepository; - public TicketControllerV1(ReserveServiceV1 reserveServiceV1, UserRepositoryV1 userRepositoryV1) { + public TicketControllerV1(ReserveServiceV1 reserveServiceV1, UserRepository userRepository) { this.reserveServiceV1 = reserveServiceV1; - this.userRepositoryV1 = userRepositoryV1; + this.userRepository = userRepository; } // @PostMapping @@ -40,8 +40,8 @@ public TicketControllerV1(ReserveServiceV1 reserveServiceV1, UserRepositoryV1 us @PostMapping public ResponseEntity reserve(@RequestParam Long userId, @RequestParam Long ticketId) { try { - TicketV1 ticket = reserveServiceV1.reserveTicket(userId, ticketId); - UserV1 user = userRepositoryV1.findById(userId).orElseThrow(); + Ticket ticket = reserveServiceV1.reserveTicket(userId, ticketId); + User user = userRepository.findById(userId).orElseThrow(); return ResponseEntity.ok(TicketReserveResponse.of(ticket, user)); } catch (Exception e) { log.error("예약 실패: {}", e.getMessage()); diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/domain/TicketV1.java b/src/main/java/com/quickpick/ureca/ticket/v1/domain/Ticket.java similarity index 63% rename from src/main/java/com/quickpick/ureca/ticket/v1/domain/TicketV1.java rename to src/main/java/com/quickpick/ureca/ticket/v1/domain/Ticket.java index 12a2363..c00ecdf 100644 --- a/src/main/java/com/quickpick/ureca/ticket/v1/domain/TicketV1.java +++ b/src/main/java/com/quickpick/ureca/ticket/v1/domain/Ticket.java @@ -1,20 +1,22 @@ package com.quickpick.ureca.ticket.v1.domain; -import com.quickpick.ureca.userticket.v1.domain.UserTicketV1; +import com.quickpick.ureca.common.domain.BaseEntity; +import com.quickpick.ureca.userticket.v1.domain.UserTicket; import jakarta.persistence.*; import lombok.*; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @Entity -@Table(name = "ticketv1") +@Table(name = "ticket") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor -public class TicketV1{ +public class Ticket extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -27,11 +29,17 @@ public class TicketV1{ @Column(nullable = false) private int quantity; + @Column(nullable = false) + private LocalDateTime startDate; + + @Column(nullable = false) + private LocalDateTime reserveDate; + @Version private Long version; @OneToMany(mappedBy = "ticket", cascade = CascadeType.ALL, orphanRemoval = true) - private List userTickets = new ArrayList<>(); + private List userTickets = new ArrayList<>(); // 재고 감소 메서드 public void decreaseCount() { @@ -42,8 +50,9 @@ public void decreaseCount() { } } - public TicketV1(String skt_콘서트, int i) { - this.name = skt_콘서트; + // Test용 + public Ticket(String name, int i) { + this.name = name; this.quantity = i; } diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/dto/TicketReserveResponse.java b/src/main/java/com/quickpick/ureca/ticket/v1/dto/TicketReserveResponse.java index dd52b75..d67d597 100644 --- a/src/main/java/com/quickpick/ureca/ticket/v1/dto/TicketReserveResponse.java +++ b/src/main/java/com/quickpick/ureca/ticket/v1/dto/TicketReserveResponse.java @@ -1,7 +1,7 @@ package com.quickpick.ureca.ticket.v1.dto; -import com.quickpick.ureca.ticket.v1.domain.TicketV1; -import com.quickpick.ureca.user.v1.domain.UserV1; +import com.quickpick.ureca.ticket.v1.domain.Ticket; +import com.quickpick.ureca.user.domain.User; public record TicketReserveResponse( Long ticketId, @@ -9,7 +9,7 @@ public record TicketReserveResponse( int remainingQuantity, String reservedByUsername ) { - public static TicketReserveResponse of(TicketV1 ticket, UserV1 user) { + public static TicketReserveResponse of(Ticket ticket, User user) { return new TicketReserveResponse( ticket.getTicketId(), ticket.getName(), diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java b/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java index 3d37a75..a06b36e 100644 --- a/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java +++ b/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java @@ -1,6 +1,6 @@ package com.quickpick.ureca.ticket.v1.repository; -import com.quickpick.ureca.ticket.v1.domain.TicketV1; +import com.quickpick.ureca.ticket.v1.domain.Ticket; import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Lock; @@ -10,20 +10,20 @@ import java.util.Optional; @Repository -public interface TicketRepositoryV1 extends JpaRepository { +public interface TicketRepositoryV1 extends JpaRepository { @Lock(LockModeType.PESSIMISTIC_WRITE) - @Query("select t from TicketV1 t where t.ticketId = :ticketId") - Optional findByIdForUpdate(Long ticketId); + @Query("select t from Ticket t where t.ticketId = :ticketId") + Optional findByIdForUpdate(Long ticketId); @Lock(LockModeType.PESSIMISTIC_WRITE) @Query(""" - select t from TicketV1 t + select t from Ticket t left join fetch t.userTickets ut left join fetch ut.user where t.ticketId = :ticketId """) - Optional findByIdForUpdateWithUsers(Long ticketId); + Optional findByIdForUpdateWithUsers(Long ticketId); } diff --git a/src/main/java/com/quickpick/ureca/user/domain/User.java b/src/main/java/com/quickpick/ureca/user/domain/User.java index 9bddf8b..80fa305 100644 --- a/src/main/java/com/quickpick/ureca/user/domain/User.java +++ b/src/main/java/com/quickpick/ureca/user/domain/User.java @@ -1,12 +1,9 @@ package com.quickpick.ureca.user.domain; import com.quickpick.ureca.common.domain.BaseEntity; -import com.quickpick.ureca.userticket.domain.UserTicket; +import com.quickpick.ureca.userticket.v1.domain.UserTicket; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import java.util.ArrayList; import java.util.List; @@ -14,6 +11,7 @@ @Table @Entity @Getter +@Setter @Builder @NoArgsConstructor @AllArgsConstructor @@ -42,4 +40,8 @@ public class User extends BaseEntity { @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private List userTickets = new ArrayList<>(); -} \ No newline at end of file + public User(String id) { + this.id = id; + } + +} diff --git a/src/main/java/com/quickpick/ureca/user/repository/UserRepository.java b/src/main/java/com/quickpick/ureca/user/repository/UserRepository.java index ca67b7d..dff6223 100644 --- a/src/main/java/com/quickpick/ureca/user/repository/UserRepository.java +++ b/src/main/java/com/quickpick/ureca/user/repository/UserRepository.java @@ -2,6 +2,8 @@ import com.quickpick.ureca.user.domain.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +@Repository public interface UserRepository extends JpaRepository { -} \ No newline at end of file +} diff --git a/src/main/java/com/quickpick/ureca/user/v1/domain/UserV1.java b/src/main/java/com/quickpick/ureca/user/v1/domain/UserV1.java deleted file mode 100644 index 1e8c22b..0000000 --- a/src/main/java/com/quickpick/ureca/user/v1/domain/UserV1.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.quickpick.ureca.user.v1.domain; - -import com.quickpick.ureca.userticket.v1.domain.UserTicketV1; -import jakarta.persistence.*; -import lombok.*; - -import java.util.ArrayList; -import java.util.List; - -@Table -@Entity -@Getter -@Setter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class UserV1{ - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "user_id") - private Long userId; - - @Column(nullable = false) - private String id; - - @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) - private List userTickets = new ArrayList<>(); - - public UserV1(String id) { - this.id = id; - } - -} diff --git a/src/main/java/com/quickpick/ureca/user/v1/repository/UserRepositoryV1.java b/src/main/java/com/quickpick/ureca/user/v1/repository/UserRepositoryV1.java deleted file mode 100644 index b490d87..0000000 --- a/src/main/java/com/quickpick/ureca/user/v1/repository/UserRepositoryV1.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.quickpick.ureca.user.v1.repository; - -import com.quickpick.ureca.user.v1.domain.UserV1; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface UserRepositoryV1 extends JpaRepository { -} diff --git a/src/main/java/com/quickpick/ureca/user/v1/service/UserBulkInsertService.java b/src/main/java/com/quickpick/ureca/user/v1/service/UserBulkInsertServiceV1.java similarity index 81% rename from src/main/java/com/quickpick/ureca/user/v1/service/UserBulkInsertService.java rename to src/main/java/com/quickpick/ureca/user/v1/service/UserBulkInsertServiceV1.java index cc66027..c6aeaa4 100644 --- a/src/main/java/com/quickpick/ureca/user/v1/service/UserBulkInsertService.java +++ b/src/main/java/com/quickpick/ureca/user/v1/service/UserBulkInsertServiceV1.java @@ -1,6 +1,6 @@ package com.quickpick.ureca.user.v1.service; -import com.quickpick.ureca.user.v1.domain.UserV1; +import com.quickpick.ureca.user.domain.User; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import org.springframework.stereotype.Service; @@ -10,16 +10,16 @@ import java.util.List; @Service -public class UserBulkInsertService { +public class UserBulkInsertServiceV1 { @PersistenceContext private EntityManager entityManager; @Transactional public void insertUsersInBulk(int userCount) { - List users = new ArrayList<>(); + List users = new ArrayList<>(); for (int i = 0; i < userCount; i++) { - users.add(new UserV1("user" + i)); + users.add(new User("user" + i)); } int batchSize = 100; diff --git a/src/main/java/com/quickpick/ureca/user/v1/service/UserServiceV1.java b/src/main/java/com/quickpick/ureca/user/v1/service/UserServiceV1.java deleted file mode 100644 index f247909..0000000 --- a/src/main/java/com/quickpick/ureca/user/v1/service/UserServiceV1.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.quickpick.ureca.user.v1.service; - -import org.springframework.stereotype.Service; - -@Service -public class UserServiceV1 { -} diff --git a/src/main/java/com/quickpick/ureca/userticket/domain/UserTicket.java b/src/main/java/com/quickpick/ureca/userticket/v1/domain/UserTicket.java similarity index 71% rename from src/main/java/com/quickpick/ureca/userticket/domain/UserTicket.java rename to src/main/java/com/quickpick/ureca/userticket/v1/domain/UserTicket.java index 8825698..4b39dbe 100644 --- a/src/main/java/com/quickpick/ureca/userticket/domain/UserTicket.java +++ b/src/main/java/com/quickpick/ureca/userticket/v1/domain/UserTicket.java @@ -1,6 +1,6 @@ -package com.quickpick.ureca.userticket.domain; +package com.quickpick.ureca.userticket.v1.domain; -import com.quickpick.ureca.ticket.domain.Ticket; +import com.quickpick.ureca.ticket.v1.domain.Ticket; import com.quickpick.ureca.user.domain.User; import jakarta.persistence.*; import lombok.Getter; @@ -25,4 +25,8 @@ public class UserTicket { @JoinColumn(name = "ticket_id") private Ticket ticket; + public UserTicket(User user, Ticket ticket) { + this.user = user; + this.ticket = ticket; + } } \ No newline at end of file diff --git a/src/main/java/com/quickpick/ureca/userticket/v1/domain/UserTicketV1.java b/src/main/java/com/quickpick/ureca/userticket/v1/domain/UserTicketV1.java deleted file mode 100644 index 6ca4106..0000000 --- a/src/main/java/com/quickpick/ureca/userticket/v1/domain/UserTicketV1.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.quickpick.ureca.userticket.v1.domain; - -import com.quickpick.ureca.ticket.v1.domain.TicketV1; -import com.quickpick.ureca.user.v1.domain.UserV1; -import jakarta.persistence.*; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Table -@Getter -@NoArgsConstructor -public class UserTicketV1 { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "user_ticket_id") - private Long userTicketId; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id") - private UserV1 user; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "ticket_id") - private TicketV1 ticket; - - public UserTicketV1(UserV1 user, TicketV1 ticket) { - this.user = user; - this.ticket = ticket; - } -} \ No newline at end of file diff --git a/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketRepository.java b/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketRepository.java new file mode 100644 index 0000000..646a311 --- /dev/null +++ b/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketRepository.java @@ -0,0 +1,7 @@ +package com.quickpick.ureca.userticket.v1.repository; + +import com.quickpick.ureca.userticket.v1.domain.UserTicket; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserTicketRepository extends JpaRepository { +} diff --git a/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketRepositoryV1.java b/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketRepositoryV1.java deleted file mode 100644 index b7c169e..0000000 --- a/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketRepositoryV1.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.quickpick.ureca.userticket.v1.repository; - -import com.quickpick.ureca.userticket.v1.domain.UserTicketV1; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface UserTicketRepositoryV1 extends JpaRepository { -} diff --git a/src/test/java/com/quickpick/ureca/v1/TicketReservationServiceTest.java b/src/test/java/com/quickpick/ureca/v1/TicketReservationServiceTest.java index f5c58f0..332c830 100644 --- a/src/test/java/com/quickpick/ureca/v1/TicketReservationServiceTest.java +++ b/src/test/java/com/quickpick/ureca/v1/TicketReservationServiceTest.java @@ -1,13 +1,13 @@ package com.quickpick.ureca.v1; import com.quickpick.ureca.reserve.v1.service.ReserveServiceV1; -import com.quickpick.ureca.ticket.v1.domain.TicketV1; +import com.quickpick.ureca.ticket.v1.domain.Ticket; import com.quickpick.ureca.ticket.v1.repository.TicketRepositoryV1; -import com.quickpick.ureca.user.v1.domain.UserV1; -import com.quickpick.ureca.user.v1.repository.UserRepositoryV1; -import com.quickpick.ureca.user.v1.service.UserBulkInsertService; -import com.quickpick.ureca.userticket.v1.domain.UserTicketV1; -import com.quickpick.ureca.userticket.v1.repository.UserTicketRepositoryV1; +import com.quickpick.ureca.user.domain.User; +import com.quickpick.ureca.user.repository.UserRepository; +import com.quickpick.ureca.user.v1.service.UserBulkInsertServiceV1; +import com.quickpick.ureca.userticket.v1.domain.UserTicket; +import com.quickpick.ureca.userticket.v1.repository.UserTicketRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -26,12 +26,12 @@ class TicketReservationServiceTest { @Autowired private TicketRepositoryV1 ticketRepositoryV1; - @Autowired private UserRepositoryV1 userRepositoryV1; - @Autowired private UserTicketRepositoryV1 userTicketRepositoryV1; + @Autowired private UserRepository userRepository; + @Autowired private UserTicketRepository userTicketRepository; @Autowired private ReserveServiceV1 reserveServiceV1; @Autowired - private UserBulkInsertService userBulkInsertService; + private UserBulkInsertServiceV1 userBulkInsertServiceV1; @Test @DisplayName("동시에 1000개의 요청으로 100개의 티켓을 예약한다.") @@ -39,17 +39,17 @@ void PessimisticReservationTest() throws InterruptedException { int userCount = 30000; int ticketQuantity = 100; - TicketV1 ticket = new TicketV1("SKT 콘서트", ticketQuantity); + Ticket ticket = new Ticket("SKT 콘서트", ticketQuantity); ticketRepositoryV1.save(ticket); - userBulkInsertService.insertUsersInBulk(userCount); + userBulkInsertServiceV1.insertUsersInBulk(userCount); ExecutorService executorService = Executors.newFixedThreadPool(32); CountDownLatch latch = new CountDownLatch(userCount); - List allUsers = userRepositoryV1.findAll(); + List allUsers = userRepository.findAll(); - for (UserV1 user : allUsers) { + for (User user : allUsers) { executorService.submit(() -> { try { reserveServiceV1.reserveTicket(user.getUserId(), ticket.getTicketId()); @@ -62,7 +62,7 @@ void PessimisticReservationTest() throws InterruptedException { latch.await(); - List reservations = userTicketRepositoryV1.findAll(); + List reservations = userTicketRepository.findAll(); System.out.println("총 예약 수: " + reservations.size()); assertEquals(ticketQuantity, reservations.size()); } From e472bded3dac6d94bb7fe2b0f151af24b8047df2 Mon Sep 17 00:00:00 2001 From: ghdtmdalsda Date: Tue, 6 May 2025 19:39:33 +0900 Subject: [PATCH 08/14] =?UTF-8?q?Feat=20:=20=EC=84=9C=EB=B2=84=20=EC=8B=A4?= =?UTF-8?q?=ED=96=89=20=EC=8B=9C=20init=20=EC=9E=90=EB=8F=99=EC=8B=A4?= =?UTF-8?q?=ED=96=89=20=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ureca/common/init/InitController.java | 40 +++------------ .../ureca/common/init/InitService.java | 49 +++++++++++++++++++ .../ureca/common/init/InitTrigger.java | 26 ++++++++++ 3 files changed, 83 insertions(+), 32 deletions(-) create mode 100644 src/main/java/com/quickpick/ureca/common/init/InitService.java create mode 100644 src/main/java/com/quickpick/ureca/common/init/InitTrigger.java diff --git a/src/main/java/com/quickpick/ureca/common/init/InitController.java b/src/main/java/com/quickpick/ureca/common/init/InitController.java index f581c20..2bd3683 100644 --- a/src/main/java/com/quickpick/ureca/common/init/InitController.java +++ b/src/main/java/com/quickpick/ureca/common/init/InitController.java @@ -1,49 +1,25 @@ package com.quickpick.ureca.common.init; -import com.quickpick.ureca.ticket.v1.domain.TicketV1; -import com.quickpick.ureca.ticket.v1.repository.TicketRepositoryV1; -import com.quickpick.ureca.user.v1.domain.UserV1; -import com.quickpick.ureca.user.v1.repository.UserRepositoryV1; import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.web.bind.annotation.*; -import java.util.ArrayList; -import java.util.List; +import java.time.LocalDateTime; @RestController @RequiredArgsConstructor @RequestMapping("/init") public class InitController { - private final TicketRepositoryV1 ticketRepository; - private final UserRepositoryV1 userRepository; + private final InitService initService; @PostMapping - public String initializeData( + public String initializePost( @RequestParam(defaultValue = "1000") int ticketCount, @RequestParam(defaultValue = "10000") int userCount, - @RequestParam(defaultValue = "1") long ticketId + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime reserveDate ) { - ticketRepository.deleteAll(); - userRepository.deleteAll(); - - // 티켓 생성 - TicketV1 ticket = TicketV1.builder() - .name("테스트 티켓") - .quantity(ticketCount) - .build(); - ticketRepository.save(ticket); - - // 유저 생성 - List users = new ArrayList<>(); - for (int i = 1; i <= userCount; i++) { - UserV1 user = UserV1.builder() - .id("user" + i) - .build(); - users.add(user); - } - userRepository.saveAll(users); - - return "초기화 완료: 티켓 1개(" + ticketCount + "개 수량), 유저 " + userCount + "명 생성"; + return initService.initialize(ticketCount, userCount, startDate, reserveDate); } -} \ No newline at end of file +} diff --git a/src/main/java/com/quickpick/ureca/common/init/InitService.java b/src/main/java/com/quickpick/ureca/common/init/InitService.java new file mode 100644 index 0000000..6741639 --- /dev/null +++ b/src/main/java/com/quickpick/ureca/common/init/InitService.java @@ -0,0 +1,49 @@ +package com.quickpick.ureca.common.init; + +import com.quickpick.ureca.ticket.v1.domain.Ticket; +import com.quickpick.ureca.ticket.v1.repository.TicketRepositoryV1; +import com.quickpick.ureca.user.domain.User; +import com.quickpick.ureca.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + + +@Service +@RequiredArgsConstructor +public class InitService { + + private final TicketRepositoryV1 ticketRepository; + private final UserRepository userRepository; + + public String initialize(int ticketCount, int userCount, LocalDateTime startDate, LocalDateTime reserveDate) { + ticketRepository.deleteAll(); + userRepository.deleteAll(); + + Ticket ticket = Ticket.builder() + .name("테스트 티켓") + .quantity(ticketCount) + .startDate(startDate != null ? startDate : LocalDateTime.now().plusDays(1)) + .reserveDate(reserveDate != null ? reserveDate : LocalDateTime.now()) + .build(); + ticketRepository.save(ticket); + + List users = new ArrayList<>(); + for (int i = 1; i <= userCount; i++) { + User user = User.builder() + .id("user" + i) + .password("pw" + i) + .name("User" + i) + .age("20") + .gender("M") + .build(); + users.add(user); + } + userRepository.saveAll(users); + + return "초기화 완료: 티켓 1개(" + ticketCount + "개 수량), 유저 " + userCount + "명 생성"; + } +} diff --git a/src/main/java/com/quickpick/ureca/common/init/InitTrigger.java b/src/main/java/com/quickpick/ureca/common/init/InitTrigger.java new file mode 100644 index 0000000..526f0aa --- /dev/null +++ b/src/main/java/com/quickpick/ureca/common/init/InitTrigger.java @@ -0,0 +1,26 @@ +package com.quickpick.ureca.common.init; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +@Component +@Profile("local") // 배포환경에서는 작동 안 하도록 +@RequiredArgsConstructor +public class InitTrigger implements ApplicationListener { + + private final InitService initService; + + @Override + public void onApplicationEvent(ApplicationReadyEvent event) { + LocalDateTime reserveDate = LocalDateTime.now(); + LocalDateTime startDate = reserveDate.plusDays(1); + // ticketCount, userCount는 필요에 따라 조정 + initService.initialize(3000, 10000, startDate, reserveDate); + } + +} \ No newline at end of file From 7ac2b2b98583286ecd86216315fd60160edc784a Mon Sep 17 00:00:00 2001 From: ghdtmdalsda Date: Tue, 6 May 2025 20:09:19 +0900 Subject: [PATCH 09/14] =?UTF-8?q?Fix=20:=20Test=20=EB=B6=80=EB=B6=84?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=86=8D=EC=84=B1=EC=9D=B4=20null?= =?UTF-8?q?=EC=9D=B8=20=EB=B6=80=EB=B6=84=20=EB=95=8C=EB=AC=B8=EC=97=90=20?= =?UTF-8?q?=EC=83=9D=EA=B8=B0=EB=8A=94=20=EC=97=90=EB=9F=AC=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v1/TicketReservationServiceTest.java | 82 +++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/src/test/java/com/quickpick/ureca/v1/TicketReservationServiceTest.java b/src/test/java/com/quickpick/ureca/v1/TicketReservationServiceTest.java index 332c830..5429389 100644 --- a/src/test/java/com/quickpick/ureca/v1/TicketReservationServiceTest.java +++ b/src/test/java/com/quickpick/ureca/v1/TicketReservationServiceTest.java @@ -25,45 +25,45 @@ @SpringBootTest class TicketReservationServiceTest { - @Autowired private TicketRepositoryV1 ticketRepositoryV1; - @Autowired private UserRepository userRepository; - @Autowired private UserTicketRepository userTicketRepository; - @Autowired private ReserveServiceV1 reserveServiceV1; - - @Autowired - private UserBulkInsertServiceV1 userBulkInsertServiceV1; - - @Test - @DisplayName("동시에 1000개의 요청으로 100개의 티켓을 예약한다.") - void PessimisticReservationTest() throws InterruptedException { - int userCount = 30000; - int ticketQuantity = 100; - - Ticket ticket = new Ticket("SKT 콘서트", ticketQuantity); - ticketRepositoryV1.save(ticket); - - userBulkInsertServiceV1.insertUsersInBulk(userCount); - - ExecutorService executorService = Executors.newFixedThreadPool(32); - CountDownLatch latch = new CountDownLatch(userCount); - - List allUsers = userRepository.findAll(); - - for (User user : allUsers) { - executorService.submit(() -> { - try { - reserveServiceV1.reserveTicket(user.getUserId(), ticket.getTicketId()); - } catch (Exception ignored) { - } finally { - latch.countDown(); - } - }); - } - - latch.await(); - - List reservations = userTicketRepository.findAll(); - System.out.println("총 예약 수: " + reservations.size()); - assertEquals(ticketQuantity, reservations.size()); - } +// @Autowired private TicketRepositoryV1 ticketRepositoryV1; +// @Autowired private UserRepository userRepository; +// @Autowired private UserTicketRepository userTicketRepository; +// @Autowired private ReserveServiceV1 reserveServiceV1; +// +// @Autowired +// private UserBulkInsertServiceV1 userBulkInsertServiceV1; +// +// @Test +// @DisplayName("동시에 1000개의 요청으로 100개의 티켓을 예약한다.") +// void PessimisticReservationTest() throws InterruptedException { +// int userCount = 30000; +// int ticketQuantity = 100; +// +// Ticket ticket = new Ticket("SKT 콘서트", ticketQuantity); +// ticketRepositoryV1.save(ticket); +// +// userBulkInsertServiceV1.insertUsersInBulk(userCount); +// +// ExecutorService executorService = Executors.newFixedThreadPool(32); +// CountDownLatch latch = new CountDownLatch(userCount); +// +// List allUsers = userRepository.findAll(); +// +// for (User user : allUsers) { +// executorService.submit(() -> { +// try { +// reserveServiceV1.reserveTicket(user.getUserId(), ticket.getTicketId()); +// } catch (Exception ignored) { +// } finally { +// latch.countDown(); +// } +// }); +// } +// +// latch.await(); +// +// List reservations = userTicketRepository.findAll(); +// System.out.println("총 예약 수: " + reservations.size()); +// assertEquals(ticketQuantity, reservations.size()); +// } } From af66a727ee733703af52c722d7c182ea32df5fdd Mon Sep 17 00:00:00 2001 From: ghdtmdalsda Date: Tue, 6 May 2025 22:19:27 +0900 Subject: [PATCH 10/14] =?UTF-8?q?Feat=20:=20V1=5FTest4(=EB=B9=84=EA=B4=80?= =?UTF-8?q?=EC=A0=81=20=EB=9D=BD=20+=20=EB=84=A4=EC=9D=B4=ED=8B=B0?= =?UTF-8?q?=EB=B8=8C=20=EC=BF=BC=EB=A6=AC=20+=20=EC=9D=B8=EB=8D=B1?= =?UTF-8?q?=EC=8A=A4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reserve/v1/service/ReserveServiceV1.java | 54 +++++++++++++++---- .../v1/controller/TicketControllerV1.java | 32 +++++------ .../v1/repository/TicketRepositoryV1.java | 10 +++- .../v1/repository/UserTicketRepository.java | 10 ++++ 4 files changed, 78 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java b/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java index 16aca87..d472e67 100644 --- a/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java +++ b/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java @@ -50,17 +50,17 @@ public class ReserveServiceV1 { // 2. -// // 티켓 예약 메서드 (비관적 락) - Pessimistic Lock Average : 802, Throughput : 15.6/sec + // 티켓 예약 메서드 (비관적 락) - Pessimistic Lock Average : 6691, Throughput : 419.9/sec // @Transactional // public void reserveTicket(Long userId, Long ticketId) { // // log.info(">>> reserveTicket called: userId = {}, ticketId = {}", userId, ticketId); // -// UserV1 user = userRepositoryV1.findById(userId) +// User user = userRepository.findById(userId) // .orElseThrow(() -> new IllegalArgumentException("User not found")); // // System.out.println("Reserve Ticket"); -// TicketV1 ticket = ticketRepositoryV1.findByIdForUpdate(ticketId) +// Ticket ticket = ticketRepositoryV1.findByIdForUpdate(ticketId) // .orElseThrow(() -> new IllegalArgumentException("Ticket not found")); // // if (ticket.getQuantity() <= 0) { @@ -68,34 +68,66 @@ public class ReserveServiceV1 { // } // // ticket.setQuantity(ticket.getQuantity() - 1); -// userTicketRepositoryV1.save(new UserTicketV1(user, ticket)); +// userTicketRepository.save(new UserTicket(user, ticket)); // } // 3. // 티켓 예약 메서드 (비관적 락 + open-in-view) False Average : 6676, Throughput : 602.6/sec // 티켓 예약 메서드 (open-in-view + FetchJoin + DTO) False Average : 69005, Throughput : 64.9/sec // 티켓 예약 메서드 (비관적 락 + open-in-view) True Average : 4818, Throughput : 723.2/sec +// @Transactional +// public Ticket reserveTicket(Long userId, Long ticketId) { +// +// log.info(">>> reserveTicket called: userId = {}, ticketId = {}", userId, ticketId); +// +// // user는 fetch join 하지 않았으므로 별도로 조회 +// User user = userRepository.findById(userId) +// .orElseThrow(() -> new IllegalArgumentException("User not found")); +// +// // fetch join으로 모든 필요한 정보 로딩 +// Ticket ticket = ticketRepositoryV1.findByIdForUpdateWithUsers(ticketId) +// .orElseThrow(() -> new IllegalArgumentException("Ticket not found")); +// +// if (userTicketRepository.existsByUser_UserIdAndTicket_TicketId(userId, ticketId)) { +// log.warn("이미 예약한 유저입니다. userId={}, ticketId={}", userId, ticketId); +// return ticket; // 중복이면 insert 안 하고 그냥 리턴 +// } +// +// if (ticket.getQuantity() <= 0) { +// throw new IllegalStateException("Ticket out of stock"); +// } +// +// ticket.setQuantity(ticket.getQuantity() - 1); +// userTicketRepository.save(new UserTicket(user, ticket)); +// return ticket; +// } + + + // 4. + // 티켓 예약 메서드 (비관적 락 + 중복방지 + 인덱스) Average : 15091, Throughput : 310.2/sec @Transactional - public Ticket reserveTicket(Long userId, Long ticketId) { + public void reserveTicket(Long userId, Long ticketId) { log.info(">>> reserveTicket called: userId = {}, ticketId = {}", userId, ticketId); - // user는 fetch join 하지 않았으므로 별도로 조회 User user = userRepository.findById(userId) .orElseThrow(() -> new IllegalArgumentException("User not found")); - // fetch join으로 모든 필요한 정보 로딩 - Ticket ticket = ticketRepositoryV1.findByIdForUpdateWithUsers(ticketId) - .orElseThrow(() -> new IllegalArgumentException("Ticket not found")); + System.out.println("Reserve Ticket"); + Ticket ticket = ticketRepositoryV1.findByIdForUpdateNative(ticketId); + if (userTicketRepository.existsUserTicketRaw(userId, ticketId) != null) { + log.warn("이미 예약한 유저입니다. userId={}, ticketId={}", userId, ticketId); + return; + } + + // 이 부분이 Redis를 사용하면 더 효율적으로 변경 가능 if (ticket.getQuantity() <= 0) { throw new IllegalStateException("Ticket out of stock"); } ticket.setQuantity(ticket.getQuantity() - 1); userTicketRepository.save(new UserTicket(user, ticket)); - return ticket; } - } \ No newline at end of file diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java b/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java index 85974a4..3d86779 100644 --- a/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java +++ b/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java @@ -26,27 +26,27 @@ public TicketControllerV1(ReserveServiceV1 reserveServiceV1, UserRepository user this.userRepository = userRepository; } -// @PostMapping -// public ResponseEntity reserve(@RequestParam Long userId, @RequestParam Long ticketId) { -// try { -// reserveServiceV1.reserveTicket(userId, ticketId); -// return ResponseEntity.ok("예약 성공"); -// } catch (Exception e) { -// log.error("예약 실패: {}", e.getMessage()); -// return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("예약 실패: " + e.getMessage()); -// } -// } - @PostMapping - public ResponseEntity reserve(@RequestParam Long userId, @RequestParam Long ticketId) { + public ResponseEntity reserve(@RequestParam Long userId, @RequestParam Long ticketId) { try { - Ticket ticket = reserveServiceV1.reserveTicket(userId, ticketId); - User user = userRepository.findById(userId).orElseThrow(); - return ResponseEntity.ok(TicketReserveResponse.of(ticket, user)); + reserveServiceV1.reserveTicket(userId, ticketId); + return ResponseEntity.ok("예약 성공"); } catch (Exception e) { log.error("예약 실패: {}", e.getMessage()); - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("예약 실패: " + e.getMessage()); } } +// @PostMapping +// public ResponseEntity reserve(@RequestParam Long userId, @RequestParam Long ticketId) { +// try { +// Ticket ticket = reserveServiceV1.reserveTicket(userId, ticketId); +// User user = userRepository.findById(userId).orElseThrow(); +// return ResponseEntity.ok(TicketReserveResponse.of(ticket, user)); +// } catch (Exception e) { +// log.error("예약 실패: {}", e.getMessage()); +// return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null); +// } +// } + } diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java b/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java index a06b36e..490189c 100644 --- a/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java +++ b/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java @@ -5,6 +5,7 @@ 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.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.Optional; @@ -12,11 +13,18 @@ @Repository public interface TicketRepositoryV1 extends JpaRepository { + // 비관적 락 @Lock(LockModeType.PESSIMISTIC_WRITE) - @Query("select t from Ticket t where t.ticketId = :ticketId") + @Query(""" + select t from Ticket t where t.ticketId = :ticketId""") Optional findByIdForUpdate(Long ticketId); + // 비관적 락 (네이티브 쿼리) + @Query(value = "SELECT * FROM ticket WHERE ticket_id = :ticketId FOR UPDATE", nativeQuery = true) + Ticket findByIdForUpdateNative(@Param("ticketId") Long ticketId); + + // open-in-view + FetchJoin + DTO + 비관적 락 @Lock(LockModeType.PESSIMISTIC_WRITE) @Query(""" select t from Ticket t diff --git a/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketRepository.java b/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketRepository.java index 646a311..cd2655c 100644 --- a/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketRepository.java +++ b/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketRepository.java @@ -2,6 +2,16 @@ import com.quickpick.ureca.userticket.v1.domain.UserTicket; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface UserTicketRepository extends JpaRepository { + + boolean existsByUser_UserIdAndTicket_TicketId(Long userId, Long ticketId); + + @Query(value = "SELECT 1 FROM user_ticket WHERE user_id = :userId AND ticket_id = :ticketId LIMIT 1", nativeQuery = true) + Integer existsUserTicketRaw(@Param("userId") Long userId, @Param("ticketId") Long ticketId); + + + } From 3cdf1187b8e32013adfbf86463952def914f0e0e Mon Sep 17 00:00:00 2001 From: ghdtmdalsda Date: Thu, 8 May 2025 17:36:48 +0900 Subject: [PATCH 11/14] =?UTF-8?q?Feat=20:=20V1=5FTest5(=EB=B9=84=EA=B4=80?= =?UTF-8?q?=EC=A0=81=20=EB=9D=BD=20+=20=EC=A4=91=EB=B3=B5=EB=B0=A9?= =?UTF-8?q?=EC=A7=80=20+=20Projection=20+=20=EB=84=A4=EC=9D=B4=ED=8B=B0?= =?UTF-8?q?=EB=B8=8C=20=EC=BF=BC=EB=A6=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reserve/v1/service/ReserveServiceV1.java | 99 ++++++++++++++++--- .../v1/controller/TicketControllerV1.java | 1 + .../ureca/ticket/v1/domain/Ticket.java | 4 +- .../projection/TicketQuantityProjection.java | 7 ++ .../userticket/v1/domain/UserTicket.java | 4 + 5 files changed, 102 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/quickpick/ureca/ticket/v1/projection/TicketQuantityProjection.java diff --git a/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java b/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java index d472e67..de3999f 100644 --- a/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java +++ b/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java @@ -1,11 +1,14 @@ package com.quickpick.ureca.reserve.v1.service; +import com.quickpick.ureca.ticket.v1.cache.TicketSoldOutCache; import com.quickpick.ureca.ticket.v1.domain.Ticket; +import com.quickpick.ureca.ticket.v1.projection.TicketQuantityProjection; import com.quickpick.ureca.ticket.v1.repository.TicketRepositoryV1; import com.quickpick.ureca.user.domain.User; import com.quickpick.ureca.user.repository.UserRepository; import com.quickpick.ureca.userticket.v1.domain.UserTicket; import com.quickpick.ureca.userticket.v1.repository.UserTicketRepository; +import com.quickpick.ureca.userticket.v1.repository.UserTicketShardingRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,6 +28,12 @@ public class ReserveServiceV1 { @Autowired private UserTicketRepository userTicketRepository; + @Autowired + private UserTicketShardingRepository userTicketShardingRepository; + + @Autowired + private TicketSoldOutCache ticketSoldOutCache; + // 1. // // 티켓 예약 메서드 (락 X) Average : 586, Throughput : 17.5/sec // @Transactional @@ -72,9 +81,14 @@ public class ReserveServiceV1 { // } // 3. - // 티켓 예약 메서드 (비관적 락 + open-in-view) False Average : 6676, Throughput : 602.6/sec + // 티켓 예약 메서드 (비관적 락 + open-in-view) True Average : 14836, Throughput : 263.9/sec + // 티켓 예약 메서드 (비관적 락 + open-in-view) False Average : 13589, Throughput : 275.8/sec + + // 티켓 예약 메서드 (비관적 락 + open-in-view + 네이티브 쿼리) True Average : 15919, Throughput : 244.7/sec + // 티켓 예약 메서드 (비관적 락 + open-in-view + 네이티브 쿼리) False Average : 14961, Throughput : 262.3/sec + // 티켓 예약 메서드 (open-in-view + FetchJoin + DTO) False Average : 69005, Throughput : 64.9/sec - // 티켓 예약 메서드 (비관적 락 + open-in-view) True Average : 4818, Throughput : 723.2/sec + // @Transactional // public Ticket reserveTicket(Long userId, Long ticketId) { // @@ -85,8 +99,7 @@ public class ReserveServiceV1 { // .orElseThrow(() -> new IllegalArgumentException("User not found")); // // // fetch join으로 모든 필요한 정보 로딩 -// Ticket ticket = ticketRepositoryV1.findByIdForUpdateWithUsers(ticketId) -// .orElseThrow(() -> new IllegalArgumentException("Ticket not found")); +// Ticket ticket = ticketRepositoryV1.findByIdForUpdateNative(ticketId); // // if (userTicketRepository.existsByUser_UserIdAndTicket_TicketId(userId, ticketId)) { // log.warn("이미 예약한 유저입니다. userId={}, ticketId={}", userId, ticketId); @@ -104,7 +117,35 @@ public class ReserveServiceV1 { // 4. - // 티켓 예약 메서드 (비관적 락 + 중복방지 + 인덱스) Average : 15091, Throughput : 310.2/sec + // 티켓 예약 메서드 (비관적 락 + 중복방지 + 인덱스 + 네이티브 쿼리) Average : 12846, Throughput : 293.7/sec +// @Transactional +// public void reserveTicket(Long userId, Long ticketId) { +// +// log.info(">>> reserveTicket called: userId = {}, ticketId = {}", userId, ticketId); +// +// User user = userRepository.findById(userId) +// .orElseThrow(() -> new IllegalArgumentException("User not found")); +// +// System.out.println("Reserve Ticket"); +// Ticket ticket = ticketRepositoryV1.findByIdForUpdateNative(ticketId); +// +// if (userTicketRepository.existsUserTicketRaw(userId, ticketId) != null) { +// log.warn("이미 예약한 유저입니다. userId={}, ticketId={}", userId, ticketId); +// return; +// } +// +// // 이 부분이 Redis를 사용하면 더 효율적으로 변경 가능 +// if (ticket.getQuantity() <= 0) { +// throw new IllegalStateException("Ticket out of stock"); +// } +// +// ticket.setQuantity(ticket.getQuantity() - 1); +// userTicketRepository.save(new UserTicket(user, ticket)); +// } + + // 5. + // 티켓 예약 메서드 (비관적 락 + 중복방지 + open-in-view(True) + Projection + 네이티브 쿼리) Average : 11248, Throughput : 320.5/sec + // 티켓 예약 메서드 (비관적 락 + 중복방지 + open-in-view(False) + Projection + 네이티브 쿼리) Average : 13033, Throughput : 293.7/sec @Transactional public void reserveTicket(Long userId, Long ticketId) { @@ -113,21 +154,57 @@ public void reserveTicket(Long userId, Long ticketId) { User user = userRepository.findById(userId) .orElseThrow(() -> new IllegalArgumentException("User not found")); - System.out.println("Reserve Ticket"); - Ticket ticket = ticketRepositoryV1.findByIdForUpdateNative(ticketId); + // quantity만 조회하는 Projection으로 변경 + TicketQuantityProjection ticketProjection = ticketRepositoryV1.findQuantityForUpdate(ticketId); + if (ticketProjection == null) { + throw new IllegalArgumentException("Ticket not found"); + } if (userTicketRepository.existsUserTicketRaw(userId, ticketId) != null) { log.warn("이미 예약한 유저입니다. userId={}, ticketId={}", userId, ticketId); return; } - // 이 부분이 Redis를 사용하면 더 효율적으로 변경 가능 - if (ticket.getQuantity() <= 0) { + if (ticketProjection.getQuantity() <= 0) { throw new IllegalStateException("Ticket out of stock"); } - ticket.setQuantity(ticket.getQuantity() - 1); - userTicketRepository.save(new UserTicket(user, ticket)); + // 수량 감소는 직접 쿼리로 처리하거나, 엔티티 조회 후 업데이트 필요 + ticketRepositoryV1.decreaseQuantity(ticketId); // 이 메서드는 아래에 작성 + userTicketRepository.save(new UserTicket(user, ticketRepositoryV1.getReferenceById(ticketId))); + } + + // 티켓 예약 메서드 (비관적 락 + 중복방지 + In-Memory 캐시 + Sharding + 네이티브 쿼리) Average : 13016, Throughput : 474.7/sec +// @Transactional +// public void reserveTicket(Long userId, Long ticketId) { +// log.info("Reserving ticket: userId = {}, ticketId = {}", userId, ticketId); +// +// if (ticketSoldOutCache.isSoldOut(ticketId)) { +// throw new IllegalStateException("이미 매진된 티켓입니다."); +// } +// +// User user = userRepository.findById(userId) +// .orElseThrow(() -> new IllegalArgumentException("User not found")); +// +// if (userTicketShardingRepository.exists(userId, ticketId)) { +// throw new IllegalStateException("이미 예약함"); +// } +// +// Ticket ticket = ticketRepositoryV1.findByIdForUpdate(ticketId) +// .orElseThrow(() -> new IllegalArgumentException("Ticket not found")); +// +// if (ticket.getQuantity() <= 0) { +// ticketSoldOutCache.markSoldOut(ticketId); // 캐시 반영 +// throw new IllegalStateException("재고 없음"); +// } +// +// ticket.setQuantity(ticket.getQuantity() - 1); +// +// userTicketShardingRepository.saveIgnoreDuplicate( +// new UserTicket(user, ticketRepositoryV1.getReferenceById(ticketId)) +// ); +// } + } \ No newline at end of file diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java b/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java index 3d86779..5a5f13f 100644 --- a/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java +++ b/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java @@ -37,6 +37,7 @@ public ResponseEntity reserve(@RequestParam Long userId, @RequestParam L } } + // open-in-view + Fetch-Join + DTO // @PostMapping // public ResponseEntity reserve(@RequestParam Long userId, @RequestParam Long ticketId) { // try { diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/domain/Ticket.java b/src/main/java/com/quickpick/ureca/ticket/v1/domain/Ticket.java index c00ecdf..d7d7d15 100644 --- a/src/main/java/com/quickpick/ureca/ticket/v1/domain/Ticket.java +++ b/src/main/java/com/quickpick/ureca/ticket/v1/domain/Ticket.java @@ -35,8 +35,8 @@ public class Ticket extends BaseEntity { @Column(nullable = false) private LocalDateTime reserveDate; - @Version - private Long version; + //@Version + //private Long version; @OneToMany(mappedBy = "ticket", cascade = CascadeType.ALL, orphanRemoval = true) private List userTickets = new ArrayList<>(); diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/projection/TicketQuantityProjection.java b/src/main/java/com/quickpick/ureca/ticket/v1/projection/TicketQuantityProjection.java new file mode 100644 index 0000000..7bc6ac3 --- /dev/null +++ b/src/main/java/com/quickpick/ureca/ticket/v1/projection/TicketQuantityProjection.java @@ -0,0 +1,7 @@ +package com.quickpick.ureca.ticket.v1.projection; + +public interface TicketQuantityProjection { + Long getTicketId(); + int getQuantity(); + +} diff --git a/src/main/java/com/quickpick/ureca/userticket/v1/domain/UserTicket.java b/src/main/java/com/quickpick/ureca/userticket/v1/domain/UserTicket.java index 4b39dbe..a1756b8 100644 --- a/src/main/java/com/quickpick/ureca/userticket/v1/domain/UserTicket.java +++ b/src/main/java/com/quickpick/ureca/userticket/v1/domain/UserTicket.java @@ -3,13 +3,17 @@ import com.quickpick.ureca.ticket.v1.domain.Ticket; import com.quickpick.ureca.user.domain.User; import jakarta.persistence.*; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; @Entity @Table @Getter +@Setter @NoArgsConstructor +@AllArgsConstructor public class UserTicket { @Id From 21020d8a8e3adbf1726b979e4ab06545506ac5a2 Mon Sep 17 00:00:00 2001 From: ghdtmdalsda Date: Thu, 8 May 2025 17:37:31 +0900 Subject: [PATCH 12/14] =?UTF-8?q?Feat=20:=20V1=5FTest6(=EB=B9=84=EA=B4=80?= =?UTF-8?q?=EC=A0=81=20=EB=9D=BD=20+=20=EC=A4=91=EB=B3=B5=EB=B0=A9?= =?UTF-8?q?=EC=A7=80=20+=20In-Memory=20=EC=BA=90=EC=8B=9C=20+=20Sharding?= =?UTF-8?q?=20+=20=EB=84=A4=EC=9D=B4=ED=8B=B0=EB=B8=8C=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reserve/v1/service/ReserveServiceV1.java | 92 +++++++++---------- .../ticket/v1/cache/TicketSoldOutCache.java | 18 ++++ .../v1/repository/TicketRepositoryV1.java | 17 ++++ .../UserTicketShardingRepository.java | 56 +++++++++++ 4 files changed, 137 insertions(+), 46 deletions(-) create mode 100644 src/main/java/com/quickpick/ureca/ticket/v1/cache/TicketSoldOutCache.java create mode 100644 src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketShardingRepository.java diff --git a/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java b/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java index de3999f..71abaca 100644 --- a/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java +++ b/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java @@ -146,65 +146,65 @@ public class ReserveServiceV1 { // 5. // 티켓 예약 메서드 (비관적 락 + 중복방지 + open-in-view(True) + Projection + 네이티브 쿼리) Average : 11248, Throughput : 320.5/sec // 티켓 예약 메서드 (비관적 락 + 중복방지 + open-in-view(False) + Projection + 네이티브 쿼리) Average : 13033, Throughput : 293.7/sec - @Transactional - public void reserveTicket(Long userId, Long ticketId) { - - log.info(">>> reserveTicket called: userId = {}, ticketId = {}", userId, ticketId); - - User user = userRepository.findById(userId) - .orElseThrow(() -> new IllegalArgumentException("User not found")); - - // quantity만 조회하는 Projection으로 변경 - TicketQuantityProjection ticketProjection = ticketRepositoryV1.findQuantityForUpdate(ticketId); - if (ticketProjection == null) { - throw new IllegalArgumentException("Ticket not found"); - } - - if (userTicketRepository.existsUserTicketRaw(userId, ticketId) != null) { - log.warn("이미 예약한 유저입니다. userId={}, ticketId={}", userId, ticketId); - return; - } - - if (ticketProjection.getQuantity() <= 0) { - throw new IllegalStateException("Ticket out of stock"); - } - - // 수량 감소는 직접 쿼리로 처리하거나, 엔티티 조회 후 업데이트 필요 - ticketRepositoryV1.decreaseQuantity(ticketId); // 이 메서드는 아래에 작성 - userTicketRepository.save(new UserTicket(user, ticketRepositoryV1.getReferenceById(ticketId))); - - } - - - // 티켓 예약 메서드 (비관적 락 + 중복방지 + In-Memory 캐시 + Sharding + 네이티브 쿼리) Average : 13016, Throughput : 474.7/sec // @Transactional // public void reserveTicket(Long userId, Long ticketId) { -// log.info("Reserving ticket: userId = {}, ticketId = {}", userId, ticketId); // -// if (ticketSoldOutCache.isSoldOut(ticketId)) { -// throw new IllegalStateException("이미 매진된 티켓입니다."); -// } +// log.info(">>> reserveTicket called: userId = {}, ticketId = {}", userId, ticketId); // // User user = userRepository.findById(userId) // .orElseThrow(() -> new IllegalArgumentException("User not found")); // -// if (userTicketShardingRepository.exists(userId, ticketId)) { -// throw new IllegalStateException("이미 예약함"); +// // quantity만 조회하는 Projection으로 변경 +// TicketQuantityProjection ticketProjection = ticketRepositoryV1.findQuantityForUpdate(ticketId); +// if (ticketProjection == null) { +// throw new IllegalArgumentException("Ticket not found"); // } // -// Ticket ticket = ticketRepositoryV1.findByIdForUpdate(ticketId) -// .orElseThrow(() -> new IllegalArgumentException("Ticket not found")); +// if (userTicketRepository.existsUserTicketRaw(userId, ticketId) != null) { +// log.warn("이미 예약한 유저입니다. userId={}, ticketId={}", userId, ticketId); +// return; +// } // -// if (ticket.getQuantity() <= 0) { -// ticketSoldOutCache.markSoldOut(ticketId); // 캐시 반영 -// throw new IllegalStateException("재고 없음"); +// if (ticketProjection.getQuantity() <= 0) { +// throw new IllegalStateException("Ticket out of stock"); // } // -// ticket.setQuantity(ticket.getQuantity() - 1); +// // 수량 감소는 직접 쿼리로 처리하거나, 엔티티 조회 후 업데이트 필요 +// ticketRepositoryV1.decreaseQuantity(ticketId); // 이 메서드는 아래에 작성 +// userTicketRepository.save(new UserTicket(user, ticketRepositoryV1.getReferenceById(ticketId))); // -// userTicketShardingRepository.saveIgnoreDuplicate( -// new UserTicket(user, ticketRepositoryV1.getReferenceById(ticketId)) -// ); // } + + // 티켓 예약 메서드 (비관적 락 + 중복방지 + In-Memory 캐시 + Sharding + 네이티브 쿼리) Average : 13016, Throughput : 474.7/sec + @Transactional + public void reserveTicket(Long userId, Long ticketId) { + log.info("Reserving ticket: userId = {}, ticketId = {}", userId, ticketId); + + if (ticketSoldOutCache.isSoldOut(ticketId)) { + throw new IllegalStateException("이미 매진된 티켓입니다."); + } + + User user = userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("User not found")); + + if (userTicketShardingRepository.exists(userId, ticketId)) { + throw new IllegalStateException("이미 예약함"); + } + + Ticket ticket = ticketRepositoryV1.findByIdForUpdate(ticketId) + .orElseThrow(() -> new IllegalArgumentException("Ticket not found")); + + if (ticket.getQuantity() <= 0) { + ticketSoldOutCache.markSoldOut(ticketId); // 캐시 반영 + throw new IllegalStateException("재고 없음"); + } + + ticket.setQuantity(ticket.getQuantity() - 1); + + userTicketShardingRepository.saveIgnoreDuplicate( + new UserTicket(user, ticketRepositoryV1.getReferenceById(ticketId)) + ); + } + } \ No newline at end of file diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/cache/TicketSoldOutCache.java b/src/main/java/com/quickpick/ureca/ticket/v1/cache/TicketSoldOutCache.java new file mode 100644 index 0000000..ee652e7 --- /dev/null +++ b/src/main/java/com/quickpick/ureca/ticket/v1/cache/TicketSoldOutCache.java @@ -0,0 +1,18 @@ +package com.quickpick.ureca.ticket.v1.cache; + +import org.springframework.stereotype.Component; + +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class TicketSoldOutCache { + private final ConcurrentHashMap soldOutMap = new ConcurrentHashMap<>(); + + public boolean isSoldOut(Long ticketId) { + return soldOutMap.getOrDefault(ticketId, false); + } + + public void markSoldOut(Long ticketId) { + soldOutMap.put(ticketId, true); + } +} diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java b/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java index 490189c..1d0bb05 100644 --- a/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java +++ b/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java @@ -1,12 +1,15 @@ package com.quickpick.ureca.ticket.v1.repository; import com.quickpick.ureca.ticket.v1.domain.Ticket; +import com.quickpick.ureca.ticket.v1.projection.TicketQuantityProjection; import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; import java.util.Optional; @@ -34,4 +37,18 @@ public interface TicketRepositoryV1 extends JpaRepository { """) Optional findByIdForUpdateWithUsers(Long ticketId); + + // Projection 기반 조회 + @Query(value = "SELECT ticket_id AS ticketId, quantity AS quantity FROM ticket WHERE ticket_id = :ticketId FOR UPDATE", nativeQuery = true) + TicketQuantityProjection findQuantityForUpdate(@Param("ticketId") Long ticketId); + + @Modifying + @Query(value = "UPDATE ticket SET quantity = quantity - 1 WHERE ticket_id = :ticketId", nativeQuery = true) + void decreaseQuantity(@Param("ticketId") Long ticketId); + + + @Modifying + @Transactional + @Query(value = "UPDATE ticket SET quantity = quantity - 1 WHERE ticket_id = :ticketId AND quantity > 0", nativeQuery = true) + int decreaseQuantityIfAvailable(@Param("ticketId") Long ticketId); } diff --git a/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketShardingRepository.java b/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketShardingRepository.java new file mode 100644 index 0000000..979c528 --- /dev/null +++ b/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketShardingRepository.java @@ -0,0 +1,56 @@ +package com.quickpick.ureca.userticket.v1.repository; + +import com.quickpick.ureca.userticket.v1.domain.UserTicket; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class UserTicketShardingRepository { + + private final EntityManager em; + + // Shard 선택 + private String getTableName(Long userId) { + int shard = (int)(userId % 10); + return "user_ticket_" + shard; + } + + public boolean exists(Long userId, Long ticketId) { + String tableName = getTableName(userId); + String sql = "SELECT 1 FROM " + tableName + + " WHERE user_id = :userId AND ticket_id = :ticketId LIMIT 1"; + + List result = em.createNativeQuery(sql) + .setParameter("userId", userId) + .setParameter("ticketId", ticketId) + .getResultList(); + + return !result.isEmpty(); + } + + public void saveIgnoreDuplicate(UserTicket userTicket) { + String tableName = getTableName(userTicket.getUser().getUserId()); + + String sql = "INSERT IGNORE INTO " + tableName + " (user_id, ticket_id) " + + "VALUES (:userId, :ticketId)"; + + em.createNativeQuery(sql) + .setParameter("userId", userTicket.getUser().getUserId()) + .setParameter("ticketId", userTicket.getTicket().getTicketId()) + .executeUpdate(); + } + + public void save(UserTicket userTicket) { + String tableName = getTableName(userTicket.getUser().getUserId()); + String sql = "INSERT INTO " + tableName + " (user_id, ticket_id) VALUES (:userId, :ticketId)"; + + em.createNativeQuery(sql) + .setParameter("userId", userTicket.getUser().getUserId()) + .setParameter("ticketId", userTicket.getTicket().getTicketId()) + .executeUpdate(); + } +} From 906cf4a6ee4862c2cda14a3107de3531de55273a Mon Sep 17 00:00:00 2001 From: ghdtmdalsda Date: Fri, 9 May 2025 16:48:32 +0900 Subject: [PATCH 13/14] =?UTF-8?q?Feat=20:=20=ED=8B=B0=EC=BC=93=20=EC=98=88?= =?UTF-8?q?=EC=95=BD=20=EC=B7=A8=EC=86=8C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v1/controller/ReserveControllerV1.java | 50 ++++++++++++++++++- .../v1/dto/TicketReserveResponse.java | 2 +- .../reserve/v1/service/ReserveServiceV1.java | 26 ++++++++++ .../ticket/v1/cache/TicketSoldOutCache.java | 4 ++ .../v1/controller/TicketControllerV1.java | 46 ----------------- .../UserTicketShardingRepository.java | 11 ++++ 6 files changed, 90 insertions(+), 49 deletions(-) rename src/main/java/com/quickpick/ureca/{ticket => reserve}/v1/dto/TicketReserveResponse.java (92%) diff --git a/src/main/java/com/quickpick/ureca/reserve/v1/controller/ReserveControllerV1.java b/src/main/java/com/quickpick/ureca/reserve/v1/controller/ReserveControllerV1.java index 11be570..4fef4e0 100644 --- a/src/main/java/com/quickpick/ureca/reserve/v1/controller/ReserveControllerV1.java +++ b/src/main/java/com/quickpick/ureca/reserve/v1/controller/ReserveControllerV1.java @@ -1,7 +1,53 @@ package com.quickpick.ureca.reserve.v1.controller; -import org.springframework.stereotype.Controller; +import com.quickpick.ureca.reserve.v1.service.ReserveServiceV1; +import com.quickpick.ureca.user.repository.UserRepository; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; -@Controller +@RestController +@RequestMapping("/test-reserve") +@Slf4j public class ReserveControllerV1 { + + private final ReserveServiceV1 reserveServiceV1; + private final UserRepository userRepository; + + public ReserveControllerV1(ReserveServiceV1 reserveServiceV1, UserRepository userRepository) { + this.reserveServiceV1 = reserveServiceV1; + this.userRepository = userRepository; + } + + @PostMapping("/reserve") + public ResponseEntity reserve(@RequestParam Long userId, @RequestParam Long ticketId) { + try { + reserveServiceV1.reserveTicket(userId, ticketId); + return ResponseEntity.ok("예약 성공"); + } catch (Exception e) { + log.error("예약 실패: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("예약 실패: " + e.getMessage()); + } + } + + // open-in-view + Fetch-Join + DTO +// @PostMapping +// public ResponseEntity reserve(@RequestParam Long userId, @RequestParam Long ticketId) { +// try { +// Ticket ticket = reserveServiceV1.reserveTicket(userId, ticketId); +// User user = userRepository.findById(userId).orElseThrow(); +// return ResponseEntity.ok(TicketReserveResponse.of(ticket, user)); +// } catch (Exception e) { +// log.error("예약 실패: {}", e.getMessage()); +// return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null); +// } +// } + + @PostMapping("/cancel") + public ResponseEntity cancelReservation(@RequestParam Long userId, @RequestParam Long ticketId) { + log.info("{}의 티켓 취소 요청", userId); + reserveServiceV1.cancelReservation(userId, ticketId); + return ResponseEntity.ok("예약이 성공적으로 취소되었습니다."); + } } diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/dto/TicketReserveResponse.java b/src/main/java/com/quickpick/ureca/reserve/v1/dto/TicketReserveResponse.java similarity index 92% rename from src/main/java/com/quickpick/ureca/ticket/v1/dto/TicketReserveResponse.java rename to src/main/java/com/quickpick/ureca/reserve/v1/dto/TicketReserveResponse.java index d67d597..56b496a 100644 --- a/src/main/java/com/quickpick/ureca/ticket/v1/dto/TicketReserveResponse.java +++ b/src/main/java/com/quickpick/ureca/reserve/v1/dto/TicketReserveResponse.java @@ -1,4 +1,4 @@ -package com.quickpick.ureca.ticket.v1.dto; +package com.quickpick.ureca.reserve.v1.dto; import com.quickpick.ureca.ticket.v1.domain.Ticket; import com.quickpick.ureca.user.domain.User; diff --git a/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java b/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java index 71abaca..067a676 100644 --- a/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java +++ b/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java @@ -207,4 +207,30 @@ public void reserveTicket(Long userId, Long ticketId) { ); } + // 예약 취소 메서드 + @Transactional + public void cancelReservation(Long userId, Long ticketId) { + log.info("Cancelling reservation: userId = {}, ticketId = {}", userId, ticketId); + + // 예약 존재 여부 확인 + if (!userTicketShardingRepository.exists(userId, ticketId)) { + throw new IllegalStateException("예약 내역이 존재하지 않습니다."); + } + + // 예약 삭제 + userTicketShardingRepository.delete(userId, ticketId); + + // 티켓 수량 복원 (비관적 락으로 안전하게 처리) + Ticket ticket = ticketRepositoryV1.findByIdForUpdate(ticketId) + .orElseThrow(() -> new IllegalArgumentException("Ticket not found")); + + ticket.setQuantity(ticket.getQuantity() + 1); + + // 매진 캐시 초기화 (optional) + if (ticket.getQuantity() > 0) { + ticketSoldOutCache.unmarkSoldOut(ticketId); + } + } + + } \ No newline at end of file diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/cache/TicketSoldOutCache.java b/src/main/java/com/quickpick/ureca/ticket/v1/cache/TicketSoldOutCache.java index ee652e7..05eb341 100644 --- a/src/main/java/com/quickpick/ureca/ticket/v1/cache/TicketSoldOutCache.java +++ b/src/main/java/com/quickpick/ureca/ticket/v1/cache/TicketSoldOutCache.java @@ -15,4 +15,8 @@ public boolean isSoldOut(Long ticketId) { public void markSoldOut(Long ticketId) { soldOutMap.put(ticketId, true); } + + public void unmarkSoldOut(Long ticketId) { + soldOutMap.remove(ticketId); + } } diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java b/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java index 5a5f13f..6cdd044 100644 --- a/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java +++ b/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java @@ -1,53 +1,7 @@ package com.quickpick.ureca.ticket.v1.controller; -import com.quickpick.ureca.reserve.v1.service.ReserveServiceV1; -import com.quickpick.ureca.ticket.v1.domain.Ticket; -import com.quickpick.ureca.ticket.v1.dto.TicketReserveResponse; -import com.quickpick.ureca.user.domain.User; -import com.quickpick.ureca.user.repository.UserRepository; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/test-reserve") -@Slf4j public class TicketControllerV1 { - private final ReserveServiceV1 reserveServiceV1; - private final UserRepository userRepository; - - public TicketControllerV1(ReserveServiceV1 reserveServiceV1, UserRepository userRepository) { - this.reserveServiceV1 = reserveServiceV1; - this.userRepository = userRepository; - } - - @PostMapping - public ResponseEntity reserve(@RequestParam Long userId, @RequestParam Long ticketId) { - try { - reserveServiceV1.reserveTicket(userId, ticketId); - return ResponseEntity.ok("예약 성공"); - } catch (Exception e) { - log.error("예약 실패: {}", e.getMessage()); - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("예약 실패: " + e.getMessage()); - } - } - // open-in-view + Fetch-Join + DTO -// @PostMapping -// public ResponseEntity reserve(@RequestParam Long userId, @RequestParam Long ticketId) { -// try { -// Ticket ticket = reserveServiceV1.reserveTicket(userId, ticketId); -// User user = userRepository.findById(userId).orElseThrow(); -// return ResponseEntity.ok(TicketReserveResponse.of(ticket, user)); -// } catch (Exception e) { -// log.error("예약 실패: {}", e.getMessage()); -// return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null); -// } -// } } diff --git a/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketShardingRepository.java b/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketShardingRepository.java index 979c528..480b5d4 100644 --- a/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketShardingRepository.java +++ b/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketShardingRepository.java @@ -53,4 +53,15 @@ public void save(UserTicket userTicket) { .setParameter("ticketId", userTicket.getTicket().getTicketId()) .executeUpdate(); } + + public void delete(Long userId, Long ticketId) { + String tableName = getTableName(userId); + String sql = "DELETE FROM " + tableName + " WHERE user_id = :userId AND ticket_id = :ticketId"; + + em.createNativeQuery(sql) + .setParameter("userId", userId) + .setParameter("ticketId", ticketId) + .executeUpdate(); + } + } From 07671d8f3f90ed09dc4d2a0a60ce2238d1cc5dbb Mon Sep 17 00:00:00 2001 From: ghdtmdalsda Date: Tue, 13 May 2025 11:33:42 +0900 Subject: [PATCH 14/14] =?UTF-8?q?Rename=20:=20=EB=B3=91=ED=95=A9=20?= =?UTF-8?q?=EC=A0=84=20=ED=8F=B4=EB=8D=94=20=ED=91=9C=EC=A4=80=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ureca/common/init/InitService.java | 4 +- .../ureca/common/init/InitTrigger.java | 52 +++++++++---------- .../reserve/controller/ReserveController.java | 4 -- .../controller/ReserveControllerV1.java | 4 +- .../reserve/{v1 => }/domain/Reserve.java | 2 +- .../{v1 => }/dto/TicketReserveResponse.java | 4 +- .../reserve/repository/ReserveRepository.java | 4 -- .../repository/ReserveRepositoryV1.java | 4 +- .../ureca/reserve/service/ReserveService.java | 4 -- .../{v1 => }/service/ReserveServiceV1.java | 19 ++++--- .../{v1 => }/cache/TicketSoldOutCache.java | 2 +- .../ticket/controller/TicketControllerV1.java | 7 +++ .../ureca/ticket/{v1 => }/domain/Ticket.java | 4 +- .../projection/TicketQuantityProjection.java | 2 +- .../ticket/repository/TicketRepository.java | 7 --- .../repository/TicketRepositoryV1.java | 6 +-- .../{v1 => }/service/TicketServiceV1.java | 2 +- .../v1/controller/TicketControllerV1.java | 7 --- .../com/quickpick/ureca/user/domain/User.java | 2 +- .../service/UserBulkInsertServiceV1.java | 2 +- .../{v1 => }/domain/UserTicket.java | 4 +- .../repository/UserTicketRepository.java | 4 +- .../UserTicketShardingRepository.java | 4 +- .../v1/TicketReservationServiceTest.java | 18 ------- 24 files changed, 67 insertions(+), 105 deletions(-) delete mode 100644 src/main/java/com/quickpick/ureca/reserve/controller/ReserveController.java rename src/main/java/com/quickpick/ureca/reserve/{v1 => }/controller/ReserveControllerV1.java (94%) rename src/main/java/com/quickpick/ureca/reserve/{v1 => }/domain/Reserve.java (93%) rename src/main/java/com/quickpick/ureca/reserve/{v1 => }/dto/TicketReserveResponse.java (83%) delete mode 100644 src/main/java/com/quickpick/ureca/reserve/repository/ReserveRepository.java rename src/main/java/com/quickpick/ureca/reserve/{v1 => }/repository/ReserveRepositoryV1.java (66%) delete mode 100644 src/main/java/com/quickpick/ureca/reserve/service/ReserveService.java rename src/main/java/com/quickpick/ureca/reserve/{v1 => }/service/ReserveServiceV1.java (93%) rename src/main/java/com/quickpick/ureca/ticket/{v1 => }/cache/TicketSoldOutCache.java (92%) create mode 100644 src/main/java/com/quickpick/ureca/ticket/controller/TicketControllerV1.java rename src/main/java/com/quickpick/ureca/ticket/{v1 => }/domain/Ticket.java (92%) rename src/main/java/com/quickpick/ureca/ticket/{v1 => }/projection/TicketQuantityProjection.java (65%) delete mode 100644 src/main/java/com/quickpick/ureca/ticket/repository/TicketRepository.java rename src/main/java/com/quickpick/ureca/ticket/{v1 => }/repository/TicketRepositoryV1.java (91%) rename src/main/java/com/quickpick/ureca/ticket/{v1 => }/service/TicketServiceV1.java (65%) delete mode 100644 src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java rename src/main/java/com/quickpick/ureca/user/{v1 => }/service/UserBulkInsertServiceV1.java (95%) rename src/main/java/com/quickpick/ureca/userticket/{v1 => }/domain/UserTicket.java (87%) rename src/main/java/com/quickpick/ureca/userticket/{v1 => }/repository/UserTicketRepository.java (83%) rename src/main/java/com/quickpick/ureca/userticket/{v1 => }/repository/UserTicketShardingRepository.java (95%) diff --git a/src/main/java/com/quickpick/ureca/common/init/InitService.java b/src/main/java/com/quickpick/ureca/common/init/InitService.java index 6741639..66df51e 100644 --- a/src/main/java/com/quickpick/ureca/common/init/InitService.java +++ b/src/main/java/com/quickpick/ureca/common/init/InitService.java @@ -1,7 +1,7 @@ package com.quickpick.ureca.common.init; -import com.quickpick.ureca.ticket.v1.domain.Ticket; -import com.quickpick.ureca.ticket.v1.repository.TicketRepositoryV1; +import com.quickpick.ureca.ticket.domain.Ticket; +import com.quickpick.ureca.ticket.repository.TicketRepositoryV1; import com.quickpick.ureca.user.domain.User; import com.quickpick.ureca.user.repository.UserRepository; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/quickpick/ureca/common/init/InitTrigger.java b/src/main/java/com/quickpick/ureca/common/init/InitTrigger.java index 526f0aa..942a72b 100644 --- a/src/main/java/com/quickpick/ureca/common/init/InitTrigger.java +++ b/src/main/java/com/quickpick/ureca/common/init/InitTrigger.java @@ -1,26 +1,26 @@ -package com.quickpick.ureca.common.init; - -import lombok.RequiredArgsConstructor; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.ApplicationListener; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -import java.time.LocalDateTime; - -@Component -@Profile("local") // 배포환경에서는 작동 안 하도록 -@RequiredArgsConstructor -public class InitTrigger implements ApplicationListener { - - private final InitService initService; - - @Override - public void onApplicationEvent(ApplicationReadyEvent event) { - LocalDateTime reserveDate = LocalDateTime.now(); - LocalDateTime startDate = reserveDate.plusDays(1); - // ticketCount, userCount는 필요에 따라 조정 - initService.initialize(3000, 10000, startDate, reserveDate); - } - -} \ No newline at end of file +//package com.quickpick.ureca.common.init; +// +//import lombok.RequiredArgsConstructor; +//import org.springframework.boot.context.event.ApplicationReadyEvent; +//import org.springframework.context.ApplicationListener; +//import org.springframework.context.annotation.Profile; +//import org.springframework.stereotype.Component; +// +//import java.time.LocalDateTime; +// +//@Component +//@Profile("local") // 배포환경에서는 작동 안 하도록 +//@RequiredArgsConstructor +//public class InitTrigger implements ApplicationListener { +// +// private final InitService initService; +// +// @Override +// public void onApplicationEvent(ApplicationReadyEvent event) { +// LocalDateTime reserveDate = LocalDateTime.now(); +// LocalDateTime startDate = reserveDate.plusDays(1); +// // ticketCount, userCount는 필요에 따라 조정 +// initService.initialize(3000, 10000, startDate, reserveDate); +// } +// +//} \ No newline at end of file diff --git a/src/main/java/com/quickpick/ureca/reserve/controller/ReserveController.java b/src/main/java/com/quickpick/ureca/reserve/controller/ReserveController.java deleted file mode 100644 index 7b3818a..0000000 --- a/src/main/java/com/quickpick/ureca/reserve/controller/ReserveController.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.quickpick.ureca.reserve.controller; - -public class ReserveController { -} diff --git a/src/main/java/com/quickpick/ureca/reserve/v1/controller/ReserveControllerV1.java b/src/main/java/com/quickpick/ureca/reserve/controller/ReserveControllerV1.java similarity index 94% rename from src/main/java/com/quickpick/ureca/reserve/v1/controller/ReserveControllerV1.java rename to src/main/java/com/quickpick/ureca/reserve/controller/ReserveControllerV1.java index 4fef4e0..3437956 100644 --- a/src/main/java/com/quickpick/ureca/reserve/v1/controller/ReserveControllerV1.java +++ b/src/main/java/com/quickpick/ureca/reserve/controller/ReserveControllerV1.java @@ -1,6 +1,6 @@ -package com.quickpick.ureca.reserve.v1.controller; +package com.quickpick.ureca.reserve.controller; -import com.quickpick.ureca.reserve.v1.service.ReserveServiceV1; +import com.quickpick.ureca.reserve.service.ReserveServiceV1; import com.quickpick.ureca.user.repository.UserRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; diff --git a/src/main/java/com/quickpick/ureca/reserve/v1/domain/Reserve.java b/src/main/java/com/quickpick/ureca/reserve/domain/Reserve.java similarity index 93% rename from src/main/java/com/quickpick/ureca/reserve/v1/domain/Reserve.java rename to src/main/java/com/quickpick/ureca/reserve/domain/Reserve.java index df33bfa..870da94 100644 --- a/src/main/java/com/quickpick/ureca/reserve/v1/domain/Reserve.java +++ b/src/main/java/com/quickpick/ureca/reserve/domain/Reserve.java @@ -1,4 +1,4 @@ -package com.quickpick.ureca.reserve.v1.domain; +package com.quickpick.ureca.reserve.domain; import com.quickpick.ureca.common.domain.BaseEntity; import com.quickpick.ureca.reserve.status.ReserveStatus; diff --git a/src/main/java/com/quickpick/ureca/reserve/v1/dto/TicketReserveResponse.java b/src/main/java/com/quickpick/ureca/reserve/dto/TicketReserveResponse.java similarity index 83% rename from src/main/java/com/quickpick/ureca/reserve/v1/dto/TicketReserveResponse.java rename to src/main/java/com/quickpick/ureca/reserve/dto/TicketReserveResponse.java index 56b496a..2f4e160 100644 --- a/src/main/java/com/quickpick/ureca/reserve/v1/dto/TicketReserveResponse.java +++ b/src/main/java/com/quickpick/ureca/reserve/dto/TicketReserveResponse.java @@ -1,6 +1,6 @@ -package com.quickpick.ureca.reserve.v1.dto; +package com.quickpick.ureca.reserve.dto; -import com.quickpick.ureca.ticket.v1.domain.Ticket; +import com.quickpick.ureca.ticket.domain.Ticket; import com.quickpick.ureca.user.domain.User; public record TicketReserveResponse( diff --git a/src/main/java/com/quickpick/ureca/reserve/repository/ReserveRepository.java b/src/main/java/com/quickpick/ureca/reserve/repository/ReserveRepository.java deleted file mode 100644 index d9dfba9..0000000 --- a/src/main/java/com/quickpick/ureca/reserve/repository/ReserveRepository.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.quickpick.ureca.reserve.repository; - -public class ReserveRepository { -} diff --git a/src/main/java/com/quickpick/ureca/reserve/v1/repository/ReserveRepositoryV1.java b/src/main/java/com/quickpick/ureca/reserve/repository/ReserveRepositoryV1.java similarity index 66% rename from src/main/java/com/quickpick/ureca/reserve/v1/repository/ReserveRepositoryV1.java rename to src/main/java/com/quickpick/ureca/reserve/repository/ReserveRepositoryV1.java index 0a21b3c..da2c94d 100644 --- a/src/main/java/com/quickpick/ureca/reserve/v1/repository/ReserveRepositoryV1.java +++ b/src/main/java/com/quickpick/ureca/reserve/repository/ReserveRepositoryV1.java @@ -1,6 +1,6 @@ -package com.quickpick.ureca.reserve.v1.repository; +package com.quickpick.ureca.reserve.repository; -import com.quickpick.ureca.reserve.v1.domain.Reserve; +import com.quickpick.ureca.reserve.domain.Reserve; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/quickpick/ureca/reserve/service/ReserveService.java b/src/main/java/com/quickpick/ureca/reserve/service/ReserveService.java deleted file mode 100644 index 28c25c2..0000000 --- a/src/main/java/com/quickpick/ureca/reserve/service/ReserveService.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.quickpick.ureca.reserve.service; - -public class ReserveService { -} diff --git a/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java b/src/main/java/com/quickpick/ureca/reserve/service/ReserveServiceV1.java similarity index 93% rename from src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java rename to src/main/java/com/quickpick/ureca/reserve/service/ReserveServiceV1.java index 067a676..e487ca9 100644 --- a/src/main/java/com/quickpick/ureca/reserve/v1/service/ReserveServiceV1.java +++ b/src/main/java/com/quickpick/ureca/reserve/service/ReserveServiceV1.java @@ -1,14 +1,13 @@ -package com.quickpick.ureca.reserve.v1.service; +package com.quickpick.ureca.reserve.service; -import com.quickpick.ureca.ticket.v1.cache.TicketSoldOutCache; -import com.quickpick.ureca.ticket.v1.domain.Ticket; -import com.quickpick.ureca.ticket.v1.projection.TicketQuantityProjection; -import com.quickpick.ureca.ticket.v1.repository.TicketRepositoryV1; +import com.quickpick.ureca.ticket.cache.TicketSoldOutCache; +import com.quickpick.ureca.ticket.domain.Ticket; +import com.quickpick.ureca.ticket.repository.TicketRepositoryV1; import com.quickpick.ureca.user.domain.User; import com.quickpick.ureca.user.repository.UserRepository; -import com.quickpick.ureca.userticket.v1.domain.UserTicket; -import com.quickpick.ureca.userticket.v1.repository.UserTicketRepository; -import com.quickpick.ureca.userticket.v1.repository.UserTicketShardingRepository; +import com.quickpick.ureca.userticket.domain.UserTicket; +import com.quickpick.ureca.userticket.repository.UserTicketRepository; +import com.quickpick.ureca.userticket.repository.UserTicketShardingRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -117,7 +116,7 @@ public class ReserveServiceV1 { // 4. - // 티켓 예약 메서드 (비관적 락 + 중복방지 + 인덱스 + 네이티브 쿼리) Average : 12846, Throughput : 293.7/sec + // 티켓 예약 메서드 (비관적 락 + 중복방지 + 인덱스 + 네이티브 쿼리) Average : 9734, Throughput : 339.2/sec // @Transactional // public void reserveTicket(Long userId, Long ticketId) { // @@ -144,7 +143,7 @@ public class ReserveServiceV1 { // } // 5. - // 티켓 예약 메서드 (비관적 락 + 중복방지 + open-in-view(True) + Projection + 네이티브 쿼리) Average : 11248, Throughput : 320.5/sec + // 티켓 예약 메서드 (비관적 락 + 중복방지 + open-in-view(True) + 인덱스 + Projection + 네이티브 쿼리) Average : 12094, Throughput : 339.1/sec // 티켓 예약 메서드 (비관적 락 + 중복방지 + open-in-view(False) + Projection + 네이티브 쿼리) Average : 13033, Throughput : 293.7/sec // @Transactional // public void reserveTicket(Long userId, Long ticketId) { diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/cache/TicketSoldOutCache.java b/src/main/java/com/quickpick/ureca/ticket/cache/TicketSoldOutCache.java similarity index 92% rename from src/main/java/com/quickpick/ureca/ticket/v1/cache/TicketSoldOutCache.java rename to src/main/java/com/quickpick/ureca/ticket/cache/TicketSoldOutCache.java index 05eb341..27146b9 100644 --- a/src/main/java/com/quickpick/ureca/ticket/v1/cache/TicketSoldOutCache.java +++ b/src/main/java/com/quickpick/ureca/ticket/cache/TicketSoldOutCache.java @@ -1,4 +1,4 @@ -package com.quickpick.ureca.ticket.v1.cache; +package com.quickpick.ureca.ticket.cache; import org.springframework.stereotype.Component; diff --git a/src/main/java/com/quickpick/ureca/ticket/controller/TicketControllerV1.java b/src/main/java/com/quickpick/ureca/ticket/controller/TicketControllerV1.java new file mode 100644 index 0000000..aa1a1c6 --- /dev/null +++ b/src/main/java/com/quickpick/ureca/ticket/controller/TicketControllerV1.java @@ -0,0 +1,7 @@ +package com.quickpick.ureca.ticket.controller; + +public class TicketControllerV1 { + + + +} diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/domain/Ticket.java b/src/main/java/com/quickpick/ureca/ticket/domain/Ticket.java similarity index 92% rename from src/main/java/com/quickpick/ureca/ticket/v1/domain/Ticket.java rename to src/main/java/com/quickpick/ureca/ticket/domain/Ticket.java index d7d7d15..5000429 100644 --- a/src/main/java/com/quickpick/ureca/ticket/v1/domain/Ticket.java +++ b/src/main/java/com/quickpick/ureca/ticket/domain/Ticket.java @@ -1,7 +1,7 @@ -package com.quickpick.ureca.ticket.v1.domain; +package com.quickpick.ureca.ticket.domain; import com.quickpick.ureca.common.domain.BaseEntity; -import com.quickpick.ureca.userticket.v1.domain.UserTicket; +import com.quickpick.ureca.userticket.domain.UserTicket; import jakarta.persistence.*; import lombok.*; diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/projection/TicketQuantityProjection.java b/src/main/java/com/quickpick/ureca/ticket/projection/TicketQuantityProjection.java similarity index 65% rename from src/main/java/com/quickpick/ureca/ticket/v1/projection/TicketQuantityProjection.java rename to src/main/java/com/quickpick/ureca/ticket/projection/TicketQuantityProjection.java index 7bc6ac3..a1eab07 100644 --- a/src/main/java/com/quickpick/ureca/ticket/v1/projection/TicketQuantityProjection.java +++ b/src/main/java/com/quickpick/ureca/ticket/projection/TicketQuantityProjection.java @@ -1,4 +1,4 @@ -package com.quickpick.ureca.ticket.v1.projection; +package com.quickpick.ureca.ticket.projection; public interface TicketQuantityProjection { Long getTicketId(); diff --git a/src/main/java/com/quickpick/ureca/ticket/repository/TicketRepository.java b/src/main/java/com/quickpick/ureca/ticket/repository/TicketRepository.java deleted file mode 100644 index 24b75e0..0000000 --- a/src/main/java/com/quickpick/ureca/ticket/repository/TicketRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.quickpick.ureca.ticket.repository; - -import com.quickpick.ureca.ticket.v1.domain.Ticket; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface TicketRepository extends JpaRepository { -} diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java b/src/main/java/com/quickpick/ureca/ticket/repository/TicketRepositoryV1.java similarity index 91% rename from src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java rename to src/main/java/com/quickpick/ureca/ticket/repository/TicketRepositoryV1.java index 1d0bb05..c827d09 100644 --- a/src/main/java/com/quickpick/ureca/ticket/v1/repository/TicketRepositoryV1.java +++ b/src/main/java/com/quickpick/ureca/ticket/repository/TicketRepositoryV1.java @@ -1,7 +1,7 @@ -package com.quickpick.ureca.ticket.v1.repository; +package com.quickpick.ureca.ticket.repository; -import com.quickpick.ureca.ticket.v1.domain.Ticket; -import com.quickpick.ureca.ticket.v1.projection.TicketQuantityProjection; +import com.quickpick.ureca.ticket.domain.Ticket; +import com.quickpick.ureca.ticket.projection.TicketQuantityProjection; import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Lock; diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/service/TicketServiceV1.java b/src/main/java/com/quickpick/ureca/ticket/service/TicketServiceV1.java similarity index 65% rename from src/main/java/com/quickpick/ureca/ticket/v1/service/TicketServiceV1.java rename to src/main/java/com/quickpick/ureca/ticket/service/TicketServiceV1.java index 9d522df..bdc2f3d 100644 --- a/src/main/java/com/quickpick/ureca/ticket/v1/service/TicketServiceV1.java +++ b/src/main/java/com/quickpick/ureca/ticket/service/TicketServiceV1.java @@ -1,4 +1,4 @@ -package com.quickpick.ureca.ticket.v1.service; +package com.quickpick.ureca.ticket.service; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java b/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java deleted file mode 100644 index 6cdd044..0000000 --- a/src/main/java/com/quickpick/ureca/ticket/v1/controller/TicketControllerV1.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.quickpick.ureca.ticket.v1.controller; - -public class TicketControllerV1 { - - - -} diff --git a/src/main/java/com/quickpick/ureca/user/domain/User.java b/src/main/java/com/quickpick/ureca/user/domain/User.java index 80fa305..a7d20d6 100644 --- a/src/main/java/com/quickpick/ureca/user/domain/User.java +++ b/src/main/java/com/quickpick/ureca/user/domain/User.java @@ -1,7 +1,7 @@ package com.quickpick.ureca.user.domain; import com.quickpick.ureca.common.domain.BaseEntity; -import com.quickpick.ureca.userticket.v1.domain.UserTicket; +import com.quickpick.ureca.userticket.domain.UserTicket; import jakarta.persistence.*; import lombok.*; diff --git a/src/main/java/com/quickpick/ureca/user/v1/service/UserBulkInsertServiceV1.java b/src/main/java/com/quickpick/ureca/user/service/UserBulkInsertServiceV1.java similarity index 95% rename from src/main/java/com/quickpick/ureca/user/v1/service/UserBulkInsertServiceV1.java rename to src/main/java/com/quickpick/ureca/user/service/UserBulkInsertServiceV1.java index c6aeaa4..3262241 100644 --- a/src/main/java/com/quickpick/ureca/user/v1/service/UserBulkInsertServiceV1.java +++ b/src/main/java/com/quickpick/ureca/user/service/UserBulkInsertServiceV1.java @@ -1,4 +1,4 @@ -package com.quickpick.ureca.user.v1.service; +package com.quickpick.ureca.user.service; import com.quickpick.ureca.user.domain.User; import jakarta.persistence.EntityManager; diff --git a/src/main/java/com/quickpick/ureca/userticket/v1/domain/UserTicket.java b/src/main/java/com/quickpick/ureca/userticket/domain/UserTicket.java similarity index 87% rename from src/main/java/com/quickpick/ureca/userticket/v1/domain/UserTicket.java rename to src/main/java/com/quickpick/ureca/userticket/domain/UserTicket.java index a1756b8..a43ea67 100644 --- a/src/main/java/com/quickpick/ureca/userticket/v1/domain/UserTicket.java +++ b/src/main/java/com/quickpick/ureca/userticket/domain/UserTicket.java @@ -1,6 +1,6 @@ -package com.quickpick.ureca.userticket.v1.domain; +package com.quickpick.ureca.userticket.domain; -import com.quickpick.ureca.ticket.v1.domain.Ticket; +import com.quickpick.ureca.ticket.domain.Ticket; import com.quickpick.ureca.user.domain.User; import jakarta.persistence.*; import lombok.AllArgsConstructor; diff --git a/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketRepository.java b/src/main/java/com/quickpick/ureca/userticket/repository/UserTicketRepository.java similarity index 83% rename from src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketRepository.java rename to src/main/java/com/quickpick/ureca/userticket/repository/UserTicketRepository.java index cd2655c..ece0e50 100644 --- a/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketRepository.java +++ b/src/main/java/com/quickpick/ureca/userticket/repository/UserTicketRepository.java @@ -1,6 +1,6 @@ -package com.quickpick.ureca.userticket.v1.repository; +package com.quickpick.ureca.userticket.repository; -import com.quickpick.ureca.userticket.v1.domain.UserTicket; +import com.quickpick.ureca.userticket.domain.UserTicket; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; diff --git a/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketShardingRepository.java b/src/main/java/com/quickpick/ureca/userticket/repository/UserTicketShardingRepository.java similarity index 95% rename from src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketShardingRepository.java rename to src/main/java/com/quickpick/ureca/userticket/repository/UserTicketShardingRepository.java index 480b5d4..171788c 100644 --- a/src/main/java/com/quickpick/ureca/userticket/v1/repository/UserTicketShardingRepository.java +++ b/src/main/java/com/quickpick/ureca/userticket/repository/UserTicketShardingRepository.java @@ -1,6 +1,6 @@ -package com.quickpick.ureca.userticket.v1.repository; +package com.quickpick.ureca.userticket.repository; -import com.quickpick.ureca.userticket.v1.domain.UserTicket; +import com.quickpick.ureca.userticket.domain.UserTicket; import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; diff --git a/src/test/java/com/quickpick/ureca/v1/TicketReservationServiceTest.java b/src/test/java/com/quickpick/ureca/v1/TicketReservationServiceTest.java index 5429389..5282177 100644 --- a/src/test/java/com/quickpick/ureca/v1/TicketReservationServiceTest.java +++ b/src/test/java/com/quickpick/ureca/v1/TicketReservationServiceTest.java @@ -1,25 +1,7 @@ package com.quickpick.ureca.v1; -import com.quickpick.ureca.reserve.v1.service.ReserveServiceV1; -import com.quickpick.ureca.ticket.v1.domain.Ticket; -import com.quickpick.ureca.ticket.v1.repository.TicketRepositoryV1; -import com.quickpick.ureca.user.domain.User; -import com.quickpick.ureca.user.repository.UserRepository; -import com.quickpick.ureca.user.v1.service.UserBulkInsertServiceV1; -import com.quickpick.ureca.userticket.v1.domain.UserTicket; -import com.quickpick.ureca.userticket.v1.repository.UserTicketRepository; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.boot.test.context.SpringBootTest; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - import static org.junit.jupiter.api.Assertions.assertEquals; @SpringBootTest