diff --git a/src/main/java/dayone/dayone/demoday/entity/respository/DemoDayUserRepository.java b/src/main/java/dayone/dayone/demoday/entity/respository/DemoDayUserRepository.java index 02a8da9..36e776f 100644 --- a/src/main/java/dayone/dayone/demoday/entity/respository/DemoDayUserRepository.java +++ b/src/main/java/dayone/dayone/demoday/entity/respository/DemoDayUserRepository.java @@ -1,13 +1,22 @@ package dayone.dayone.demoday.entity.respository; import dayone.dayone.demoday.entity.DemoDayUser; +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 java.util.List; import java.util.Optional; public interface DemoDayUserRepository extends JpaRepository { + Optional findByDemoDayIdAndUserId(final Long demoDayId, final Long userId); List findByDemoDayId(final Long demoDayId); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT D FROM DemoDayUser D WHERE D.demoDayId = :demoDayId") + List findByDemoDayIdForUpdate(final @Param("demoDayId") Long demoDayId); } diff --git a/src/main/java/dayone/dayone/demoday/entity/respository/TicketRepository.java b/src/main/java/dayone/dayone/demoday/entity/respository/TicketRepository.java index bb2a141..14b6ec3 100644 --- a/src/main/java/dayone/dayone/demoday/entity/respository/TicketRepository.java +++ b/src/main/java/dayone/dayone/demoday/entity/respository/TicketRepository.java @@ -1,11 +1,19 @@ package dayone.dayone.demoday.entity.respository; import dayone.dayone.demoday.entity.Ticket; +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 java.util.Optional; public interface TicketRepository extends JpaRepository { Optional findByDemoDayId(final Long demoDayId); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT T FROM Ticket T WHERE T.demoDay.id = :demoDayId") + Optional findByDemoDayIdForUpdate(final @Param("demoDayId") Long demoDayId); } diff --git a/src/main/java/dayone/dayone/demoday/service/DemoDayService.java b/src/main/java/dayone/dayone/demoday/service/DemoDayService.java index e268eb1..6b3b939 100644 --- a/src/main/java/dayone/dayone/demoday/service/DemoDayService.java +++ b/src/main/java/dayone/dayone/demoday/service/DemoDayService.java @@ -61,9 +61,9 @@ public void applyDemoDay(final Long userId, final Long demoDayId) { .orElseThrow(() -> new UserException(UserErrorCode.NOT_EXIST_USER)); final DemoDay demoDay = demoDayRepository.findById(demoDayId) .orElseThrow(() -> new UserException(DemoDayErrorCode.NOT_EXIST_DEMO_DAY)); - final Ticket ticket = ticketRepository.findByDemoDayId(demoDayId) + final Ticket ticket = ticketRepository.findByDemoDayIdForUpdate(demoDayId) .orElseThrow(() -> new UserException(DemoDayErrorCode.NOT_EXIST_DEMO_DAY)); - final List demoDayUsers = demoDayUserRepository.findByDemoDayId(demoDayId); + final List demoDayUsers = demoDayUserRepository.findByDemoDayIdForUpdate(demoDayId); final DemoDayUser apply = demoDay.apply(user, ticket, new DemoDayUsers(demoDayUsers)); demoDayUserRepository.save(apply); diff --git a/src/test/java/dayone/dayone/demoday/service/DemoDayServiceTest.java b/src/test/java/dayone/dayone/demoday/service/DemoDayServiceTest.java index d7a7632..0ee2084 100644 --- a/src/test/java/dayone/dayone/demoday/service/DemoDayServiceTest.java +++ b/src/test/java/dayone/dayone/demoday/service/DemoDayServiceTest.java @@ -27,6 +27,10 @@ import java.time.LocalTime; import java.util.List; import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -150,5 +154,83 @@ void applyDemoDay() { softly.assertThat(byDemoDayIdAndUserId.isPresent()).isTrue(); }); } + + @DisplayName("티켓의 개수가 1개인 데모데이에 10명의 유저가 동시에 참여 요청을 하더라도 1명한 참여 가능하다.") + @Test + void applyDemoDayWithConcurrent() throws InterruptedException { + // given + final User DemoDayOwner = testUserFactory.createUser("test@test.com", "test", "test", 1); + final List users = testUserFactory.createNUser(10, "test2@test.com", "test2", "test2", 1); + + + final DemoDay demoDayOpen = testDemoDayFactory.createDemoDayOpen("title", "description", DemoDayOwner.getId()); + final Ticket tickets = testTicketFactory.createNTicket(demoDayOpen, 1); + + final ExecutorService executorService = Executors.newFixedThreadPool(users.size()); + final CountDownLatch countDownLatch = new CountDownLatch(users.size()); + + // when + final AtomicInteger errorCount = new AtomicInteger(0); + for (final User user : users) { + executorService.submit(() -> { + try { + demoDayService.applyDemoDay(user.getId(), demoDayOpen.getId()); + } catch (Exception ignored) { + errorCount.incrementAndGet(); + } finally { + countDownLatch.countDown(); + } + }); + } + + countDownLatch.await(); + + final List demoDayUsers = demoDayUserRepository.findByDemoDayId(demoDayOpen.getId()); + final Ticket ticket = ticketRepository.findByDemoDayId(demoDayOpen.getId()).get(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(demoDayUsers).hasSize(tickets.getCapacity()); + softly.assertThat(ticket.getCapacity()).isEqualTo(0); + softly.assertThat(errorCount.get()).isEqualTo(users.size() - 1); + }); + } + + @DisplayName("같은 유저가 빠르게 2번 데모데이에 신청하더라도 1번만 신청 처리된다.") + @Test + void applyDemoDayWithConcurrentSameUser() throws InterruptedException { + // given + final User DemoDayOwner = testUserFactory.createUser("test@test.com", "test", "test", 1); + final DemoDay demoDayOpen = testDemoDayFactory.createDemoDayOpen("title", "description", DemoDayOwner.getId()); + testTicketFactory.createNTicket(demoDayOpen, 2); + + final User demodayUser = testUserFactory.createUser("test2@test.com", "test2", "test2", 1); + + final int threadCount = 2; + final ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + final CountDownLatch countDownLatch = new CountDownLatch(threadCount); + + // when + final AtomicInteger errorCount = new AtomicInteger(0); + for (int i = 0; i < threadCount; i++) { + executorService.submit(() -> { + try { + demoDayService.applyDemoDay(demodayUser.getId(), demoDayOpen.getId()); + } catch (Exception ignored) { + errorCount.incrementAndGet(); + } finally { + countDownLatch.countDown(); + } + }); + } + countDownLatch.await(); + + // then + final List demoDayUsers = demoDayUserRepository.findByDemoDayId(demoDayOpen.getId()); + final Ticket ticket = ticketRepository.findByDemoDayId(demoDayOpen.getId()).get(); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(demoDayUsers).hasSize(1); + softly.assertThat(ticket.getCapacity()).isEqualTo(1); + softly.assertThat(errorCount.get()).isEqualTo(threadCount - 1); + }); + } } }