Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature : 웨이팅 생성 시 동시성 구현하기 #109

Merged
merged 13 commits into from
Jan 17, 2024
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
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