Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<DemoDayUser, Long> {

Optional<DemoDayUser> findByDemoDayIdAndUserId(final Long demoDayId, final Long userId);

List<DemoDayUser> findByDemoDayId(final Long demoDayId);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT D FROM DemoDayUser D WHERE D.demoDayId = :demoDayId")
List<DemoDayUser> findByDemoDayIdForUpdate(final @Param("demoDayId") Long demoDayId);
}
Original file line number Diff line number Diff line change
@@ -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<Ticket, Long> {

Optional<Ticket> findByDemoDayId(final Long demoDayId);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT T FROM Ticket T WHERE T.demoDay.id = :demoDayId")
Optional<Ticket> findByDemoDayIdForUpdate(final @Param("demoDayId") Long demoDayId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<DemoDayUser> demoDayUsers = demoDayUserRepository.findByDemoDayId(demoDayId);
final List<DemoDayUser> demoDayUsers = demoDayUserRepository.findByDemoDayIdForUpdate(demoDayId);

final DemoDayUser apply = demoDay.apply(user, ticket, new DemoDayUsers(demoDayUsers));
demoDayUserRepository.save(apply);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<User> 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<DemoDayUser> 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<DemoDayUser> 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);
});
}
}
}