Skip to content

Commit

Permalink
Merge pull request #109 from dev-hooon/feat/#103/create-waiting-concu…
Browse files Browse the repository at this point in the history
…rrency

feature : 웨이팅 생성 시 동시성 구현하기
  • Loading branch information
hyun2371 authored Jan 17, 2024
2 parents 46bdd9e + e8a25f9 commit de44359
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 50 deletions.
7 changes: 7 additions & 0 deletions src/main/java/com/prgrms/catchtable/shop/domain/Shop.java
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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<Menu> menuList) {
this.menuList.addAll(menuList);
this.menuList.forEach(menu -> menu.insertShop(this));
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Shop, Long>, ShopRepositoryCustom {

@EntityGraph(attributePaths = {"menuList"})
Optional<Shop> findShopById(Long id);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select s from Shop s where s.id = :id")
Optional<Shop> findByIdWithPessimisticLock(@Param("id") Long id);

@Transactional
@Modifying(clearAutomatically = true)
@Query("update Shop s set s.waitingCount = 0")
void initWaitingCount();
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -17,8 +15,6 @@ public interface WaitingRepository extends JpaRepository<Waiting, Long> {

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<Waiting> findByMemberAndStatusWithShop(@Param("member") Member member,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<String> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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);

Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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("[email protected]", "owner");
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,18 @@
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;
import org.junit.jupiter.api.Test;
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
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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%[email protected]",
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit de44359

Please sign in to comment.