diff --git a/src/main/java/com/prgrms/catchtable/shop/domain/Shop.java b/src/main/java/com/prgrms/catchtable/shop/domain/Shop.java index 0b0114b3..19686be4 100644 --- a/src/main/java/com/prgrms/catchtable/shop/domain/Shop.java +++ b/src/main/java/com/prgrms/catchtable/shop/domain/Shop.java @@ -47,6 +47,8 @@ public class Shop extends BaseEntity { private Address address; @Column(name = "capacity") private int capacity; + @Column(name = "waiting_count") + private int waitingCount; @Column(name = "opening_time") private LocalTime openingTime; @Column(name = "closing_time") @@ -60,10 +62,15 @@ public Shop(String name, BigDecimal rating, Category category, Address address, this.category = category; this.address = address; this.capacity = capacity; + this.waitingCount = 0; this.openingTime = openingTime; this.closingTime = closingTime; } + public int findWaitingNumber() { + return ++waitingCount; + } + public void updateMenuList(List menuList) { this.menuList.addAll(menuList); this.menuList.forEach(menu -> menu.insertShop(this)); diff --git a/src/main/java/com/prgrms/catchtable/shop/repository/ShopRepository.java b/src/main/java/com/prgrms/catchtable/shop/repository/ShopRepository.java index 860f893f..fc22af52 100644 --- a/src/main/java/com/prgrms/catchtable/shop/repository/ShopRepository.java +++ b/src/main/java/com/prgrms/catchtable/shop/repository/ShopRepository.java @@ -1,12 +1,27 @@ package com.prgrms.catchtable.shop.repository; import com.prgrms.catchtable.shop.domain.Shop; +import jakarta.persistence.LockModeType; import java.util.Optional; import org.springframework.data.jpa.repository.EntityGraph; 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.transaction.annotation.Transactional; public interface ShopRepository extends JpaRepository, ShopRepositoryCustom { @EntityGraph(attributePaths = {"menuList"}) Optional findShopById(Long id); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("select s from Shop s where s.id = :id") + Optional findByIdWithPessimisticLock(@Param("id") Long id); + + @Transactional + @Modifying(clearAutomatically = true) + @Query("update Shop s set s.waitingCount = 0") + void initWaitingCount(); } diff --git a/src/main/java/com/prgrms/catchtable/waiting/repository/WaitingRepository.java b/src/main/java/com/prgrms/catchtable/waiting/repository/WaitingRepository.java index e7c3a6d6..5644b922 100644 --- a/src/main/java/com/prgrms/catchtable/waiting/repository/WaitingRepository.java +++ b/src/main/java/com/prgrms/catchtable/waiting/repository/WaitingRepository.java @@ -1,10 +1,8 @@ package com.prgrms.catchtable.waiting.repository; import com.prgrms.catchtable.member.domain.Member; -import com.prgrms.catchtable.shop.domain.Shop; import com.prgrms.catchtable.waiting.domain.Waiting; import com.prgrms.catchtable.waiting.domain.WaitingStatus; -import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -17,8 +15,6 @@ public interface WaitingRepository extends JpaRepository { boolean existsByMemberAndStatus(Member member, WaitingStatus status); - Long countByShopAndCreatedAtBetween(Shop shop, LocalDateTime start, LocalDateTime end); - @Query("select w from Waiting w join fetch w.shop " + "where w.member = :member and w.status = :status") Optional findByMemberAndStatusWithShop(@Param("member") Member member, diff --git a/src/main/java/com/prgrms/catchtable/waiting/schedular/WaitingScheduler.java b/src/main/java/com/prgrms/catchtable/waiting/schedular/WaitingScheduler.java index ba4ccc51..2988a0df 100644 --- a/src/main/java/com/prgrms/catchtable/waiting/schedular/WaitingScheduler.java +++ b/src/main/java/com/prgrms/catchtable/waiting/schedular/WaitingScheduler.java @@ -3,6 +3,7 @@ import static com.prgrms.catchtable.waiting.domain.WaitingStatus.CANCELED; import static com.prgrms.catchtable.waiting.domain.WaitingStatus.PROGRESS; +import com.prgrms.catchtable.shop.repository.ShopRepository; import com.prgrms.catchtable.waiting.repository.WaitingRepository; import java.util.Set; import lombok.RequiredArgsConstructor; @@ -21,17 +22,24 @@ public class WaitingScheduler { private final StringRedisTemplate redisTemplate; private final WaitingRepository waitingRepository; + private final ShopRepository shopRepository; - //매일 자정 레디스 데이터 비우기 + // 매일 자정 레디스 데이터 비우기 @Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul") public void clearRedis() { Set keys = redisTemplate.keys("s*"); redisTemplate.delete(keys); } - //매일 자정 대기 상태 바꾸기 + // 매일 자정 대기 상태 바꾸기 @Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul") - public void changeProgressStatus() { + public void updateProgressStatus() { waitingRepository.updateWaitingStatus(CANCELED, PROGRESS); } + + // 매일 자정 대기 수 초기화 + @Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul") + public void initWaitingCount() { + shopRepository.initWaitingCount(); + } } diff --git a/src/main/java/com/prgrms/catchtable/waiting/service/MemberWaitingService.java b/src/main/java/com/prgrms/catchtable/waiting/service/MemberWaitingService.java index 2f902aec..c256fc6e 100644 --- a/src/main/java/com/prgrms/catchtable/waiting/service/MemberWaitingService.java +++ b/src/main/java/com/prgrms/catchtable/waiting/service/MemberWaitingService.java @@ -22,8 +22,6 @@ import com.prgrms.catchtable.waiting.dto.response.MemberWaitingResponse; import com.prgrms.catchtable.waiting.repository.WaitingRepository; import com.prgrms.catchtable.waiting.repository.waitingline.WaitingLineRepository; -import java.time.LocalDate; -import java.time.LocalDateTime; import java.time.LocalTime; import java.util.List; import lombok.RequiredArgsConstructor; @@ -34,10 +32,6 @@ @Service public class MemberWaitingService { - private final LocalDateTime START_DATE_TIME = LocalDateTime.of(LocalDate.now(), - LocalTime.of(0, 0, 0)); - private final LocalDateTime END_DATE_TIME = LocalDateTime.of(LocalDate.now(), - LocalTime.of(23, 59, 59)); private final WaitingRepository waitingRepository; private final ShopRepository shopRepository; private final WaitingLineRepository waitingLineRepository; @@ -47,14 +41,12 @@ public class MemberWaitingService { @Transactional public MemberWaitingResponse createWaiting(Long shopId, Member member, CreateWaitingRequest request) { - Shop shop = getShopEntity(shopId); // 연관 엔티티 조회 - Owner owner = getOwnerEntity(shop); - validateIfMemberWaitingExists(member); // 기존 진행 중인 waiting이 있는지 검증 - int waitingNumber = (waitingRepository.countByShopAndCreatedAtBetween(shop, - START_DATE_TIME, END_DATE_TIME)).intValue() + 1; // 대기 번호 생성 + Shop shop = getShopEntityWithPessimisticLock(shopId); // 연관 엔티티 조회 + Owner owner = getOwnerEntity(shop); + int waitingNumber = shop.findWaitingNumber();// 대기 번호 생성 Waiting waiting = toWaiting(request, waitingNumber, member, shop); //waiting 생성 후 저장 Waiting savedWaiting = waitingRepository.save(waiting); @@ -120,8 +112,8 @@ private void validateIfMemberWaitingExists(Member member) { } } - private Shop getShopEntity(Long shopId) { - Shop shop = shopRepository.findById(shopId).orElseThrow( + private Shop getShopEntityWithPessimisticLock(Long shopId) { + Shop shop = shopRepository.findByIdWithPessimisticLock(shopId).orElseThrow( () -> new NotFoundCustomException(NOT_EXIST_SHOP) ); shop.validateIfShopOpened(LocalTime.now()); diff --git a/src/test/java/com/prgrms/catchtable/shop/repository/ShopRepositoryTest.java b/src/test/java/com/prgrms/catchtable/shop/repository/ShopRepositoryTest.java index 179f0d2e..30ebfcb3 100644 --- a/src/test/java/com/prgrms/catchtable/shop/repository/ShopRepositoryTest.java +++ b/src/test/java/com/prgrms/catchtable/shop/repository/ShopRepositoryTest.java @@ -1,6 +1,7 @@ package com.prgrms.catchtable.shop.repository; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace.NONE; import com.prgrms.catchtable.shop.domain.Category; @@ -73,4 +74,26 @@ void findSearchTest() { //then assertThat(searchList.size()).isZero(); } + + @Test + @DisplayName("벌크 연산으로 가게 웨이팅 수를 0으로 만들 수 있다.") + void updateWaitingStatus() { + //given + Shop shop1 = ShopFixture.shop(); + shop1.findWaitingNumber(); // waitingCount 증가 + Shop shop2 = ShopFixture.shop(); + shop2.findWaitingNumber(); + shopRepository.saveAll(List.of(shop1, shop2)); + + //when + shopRepository.initWaitingCount(); + Shop savedShop1 = shopRepository.findById(shop1.getId()).orElseThrow(); + Shop savedShop2 = shopRepository.findById(shop2.getId()).orElseThrow(); + + //then + assertAll( + () -> assertThat(savedShop1.getWaitingCount()).isZero(), + () -> assertThat(savedShop2.getWaitingCount()).isZero() + ); + } } \ No newline at end of file diff --git a/src/test/java/com/prgrms/catchtable/waiting/controller/MemberWaitingControllerTest.java b/src/test/java/com/prgrms/catchtable/waiting/controller/MemberWaitingControllerTest.java index f26cc92f..3ad68969 100644 --- a/src/test/java/com/prgrms/catchtable/waiting/controller/MemberWaitingControllerTest.java +++ b/src/test/java/com/prgrms/catchtable/waiting/controller/MemberWaitingControllerTest.java @@ -73,6 +73,7 @@ void setUp() { memberRepository.saveAll(List.of(member1, member2, member3)); shop = ShopFixture.shopWith24(); + ReflectionTestUtils.setField(shop, "waitingCount", 3); shopRepository.save(shop); Owner owner = OwnerFixture.getOwner("owner@naver.com", "owner"); @@ -145,7 +146,6 @@ void createWaitingSuccess() throws Exception { waiting1.changeStatusCanceled(); waitingRepository.save(waiting1); - // when, then mockMvc.perform(post("/waitings/{shopId}", shop.getId()) .contentType(APPLICATION_JSON) diff --git a/src/test/java/com/prgrms/catchtable/waiting/repository/WaitingRepositoryTest.java b/src/test/java/com/prgrms/catchtable/waiting/repository/WaitingRepositoryTest.java index fa32f3c1..4c34a9be 100644 --- a/src/test/java/com/prgrms/catchtable/waiting/repository/WaitingRepositoryTest.java +++ b/src/test/java/com/prgrms/catchtable/waiting/repository/WaitingRepositoryTest.java @@ -15,9 +15,6 @@ import com.prgrms.catchtable.shop.repository.ShopRepository; import com.prgrms.catchtable.waiting.domain.Waiting; import com.prgrms.catchtable.waiting.fixture.WaitingFixture; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -25,16 +22,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.test.util.ReflectionTestUtils; @DataJpaTest @AutoConfigureTestDatabase(replace = NONE) class WaitingRepositoryTest { - private final LocalDateTime START_DATE_TIME = LocalDateTime.of(LocalDate.now(), - LocalTime.of(0, 0, 0)); - private final LocalDateTime END_DATE_TIME = LocalDateTime.of(LocalDate.now(), - LocalTime.of(23, 59, 59)); @Autowired private WaitingRepository waitingRepository; @Autowired @@ -55,26 +47,6 @@ void setUp() { shopRepository.save(shop); } - @DisplayName("특정 가게의 당일 대기 번호를 조회할 수 있다.") - @Test - void countByShopAndCreatedAtBetween() { - //given - Waiting yesterdayWaiting = WaitingFixture.progressWaiting(member1, shop, 1); - Waiting completedWaiting = WaitingFixture.completedWaiting(member2, shop, 2); - Waiting normalWaiting = WaitingFixture.progressWaiting(member3, shop, 3); - waitingRepository.saveAll(List.of(yesterdayWaiting, completedWaiting, normalWaiting)); - - ReflectionTestUtils.setField(yesterdayWaiting, "createdAt", - LocalDateTime.now().minusDays(1)); - waitingRepository.save(yesterdayWaiting); - - //when - Long count = waitingRepository.countByShopAndCreatedAtBetween(shop, START_DATE_TIME, - END_DATE_TIME); - //then - assertThat(count).isEqualTo(2L); //waiting2, waiting3 - } - @DisplayName("멤버의 아이디 리스트로 waiting 목록을 조회 가능하다.") @Test void findByIdsWithMember() { diff --git a/src/test/java/com/prgrms/catchtable/waiting/service/MemberWaitingServiceIntegrationTest.java b/src/test/java/com/prgrms/catchtable/waiting/service/MemberWaitingServiceIntegrationTest.java new file mode 100644 index 00000000..728553d4 --- /dev/null +++ b/src/test/java/com/prgrms/catchtable/waiting/service/MemberWaitingServiceIntegrationTest.java @@ -0,0 +1,89 @@ +package com.prgrms.catchtable.waiting.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.prgrms.catchtable.member.MemberFixture; +import com.prgrms.catchtable.member.domain.Member; +import com.prgrms.catchtable.member.repository.MemberRepository; +import com.prgrms.catchtable.owner.domain.Owner; +import com.prgrms.catchtable.owner.fixture.OwnerFixture; +import com.prgrms.catchtable.owner.repository.OwnerRepository; +import com.prgrms.catchtable.shop.domain.Shop; +import com.prgrms.catchtable.shop.fixture.ShopFixture; +import com.prgrms.catchtable.shop.repository.ShopRepository; +import com.prgrms.catchtable.waiting.dto.request.CreateWaitingRequest; +import com.prgrms.catchtable.waiting.fixture.WaitingFixture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +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 org.springframework.data.redis.core.StringRedisTemplate; + +@SpringBootTest +@Disabled +class MemberWaitingServiceIntegrationTest { + + @Autowired + private ShopRepository shopRepository; + @Autowired + private OwnerRepository ownerRepository; + @Autowired + private MemberRepository memberRepository; + @Autowired + private MemberWaitingService memberWaitingService; + + @Autowired + private StringRedisTemplate redisTemplate; + + private Shop shop; + private CreateWaitingRequest request; + + @BeforeEach + void setUp() { + request = WaitingFixture.createWaitingRequest(); + + shop = ShopFixture.shopWith24(); + shopRepository.save(shop); + + Owner owner = OwnerFixture.getOwner(shop); + ownerRepository.save(owner); + } + + @AfterEach + void clear() { + redisTemplate.delete("s" + shop.getId()); + } + + @DisplayName("동시에 30개 요청이 들어와도 각각 다른 대기번호를 부여한다.") + @Test + void createWaitingNumberConcurrency() throws InterruptedException { + int threadCount = 30; + ExecutorService executorService = Executors.newFixedThreadPool(30); + CountDownLatch latch = new CountDownLatch( + threadCount); // 다른 thread에서 수행 중인 작업이 완료될 때까지 대기할 수 있도록 돕는 클래스 + for (int i = 0; i < threadCount; i++) { + Member member = MemberFixture.member(String.format("hyun%d@gmail.com", + i)); // validateMemberWaitingExists 오류 안 나도록 (한 기기 당 한 회원 웨이팅 생성) + memberRepository.save(member); + + executorService.submit(() -> { + try { + memberWaitingService.createWaiting(shop.getId(), member, request); + } finally { + latch.countDown(); + } + }); + } + + latch.await(); //다른 스레드에서 수행중인 작업이 완료될 때까지 대기 + int waitingCount = shopRepository.findAll().get(0) + .getWaitingCount(); + assertEquals(threadCount, waitingCount); + } +} \ No newline at end of file diff --git a/src/test/java/com/prgrms/catchtable/waiting/service/MemberWaitingServiceTest.java b/src/test/java/com/prgrms/catchtable/waiting/service/MemberWaitingServiceTest.java index 35523c60..d143bfb3 100644 --- a/src/test/java/com/prgrms/catchtable/waiting/service/MemberWaitingServiceTest.java +++ b/src/test/java/com/prgrms/catchtable/waiting/service/MemberWaitingServiceTest.java @@ -66,7 +66,7 @@ void createWaiting() { .build(); given(ownerRepository.findOwnerByShop(shop)).willReturn(Optional.of(owner)); doNothing().when(shop).validateIfShopOpened(any(LocalTime.class)); - given(shopRepository.findById(1L)).willReturn(Optional.of(shop)); + given(shopRepository.findByIdWithPessimisticLock(1L)).willReturn(Optional.of(shop)); given(shop.getId()).willReturn(1L); given(waitingRepository.existsByMemberAndStatus(member, PROGRESS)).willReturn(false); given(waitingRepository.save(any(Waiting.class))).willReturn(waiting);