diff --git a/lime-api/src/main/java/com/programmers/lime/domains/vote/application/VoteLockManager.java b/lime-api/src/main/java/com/programmers/lime/domains/vote/application/VoteLockManager.java new file mode 100644 index 000000000..d7f62b3ee --- /dev/null +++ b/lime-api/src/main/java/com/programmers/lime/domains/vote/application/VoteLockManager.java @@ -0,0 +1,34 @@ +package com.programmers.lime.domains.vote.application; + +import org.springframework.stereotype.Component; + +import com.programmers.lime.domains.vote.domain.Vote; +import com.programmers.lime.domains.vote.implementation.VoteManager; +import com.programmers.lime.redis.vote.VoteRedisManager; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class VoteLockManager { + + private final VoteManager voteManager; + private final VoteRedisManager voteRedisManager; + + public void participate( + final Vote vote, + final Long memberId, + final Long itemId + ) throws InterruptedException + { + while (Boolean.FALSE.equals(voteRedisManager.lock(String.valueOf(vote.getId())))) { + Thread.sleep(10); + } + + try { + voteManager.participate(vote, memberId, itemId); + } finally { + voteRedisManager.unlock(String.valueOf(vote.getId())); + } + } +} diff --git a/lime-api/src/main/java/com/programmers/lime/domains/vote/application/VoteService.java b/lime-api/src/main/java/com/programmers/lime/domains/vote/application/VoteService.java index e0f51983d..1a2b60360 100644 --- a/lime-api/src/main/java/com/programmers/lime/domains/vote/application/VoteService.java +++ b/lime-api/src/main/java/com/programmers/lime/domains/vote/application/VoteService.java @@ -36,7 +36,9 @@ import com.programmers.lime.redis.vote.VoteRedisManager; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +@Slf4j @Service @RequiredArgsConstructor public class VoteService { @@ -46,6 +48,7 @@ public class VoteService { private final VoteManager voteManager; private final VoteRemover voteRemover; private final VoterReader voterReader; + private final VoteLockManager voteLockManager; private final MemberUtils memberUtils; private final ItemReader itemReader; private final VoteRedisManager voteRedisManager; @@ -85,11 +88,15 @@ private void participate( final Long memberId, final Long itemId ) { - voterReader.find(vote, memberId) + voterReader.find(vote.getId(), memberId) .ifPresentOrElse( voter -> voteManager.reParticipate(itemId, voter), () -> { - voteManager.participate(vote, memberId, itemId); + try { + voteLockManager.participate(vote, memberId, itemId); + } catch (InterruptedException e) { + log.info("투표 참여 중 락 획득 실패, voteId={}, memberId={}", vote.getId(), memberId); + } eventPublisher.publishEvent(new RankingUpdateEvent(String.valueOf(vote.getHobby()), vote.isVoting(), getVoteRedis(vote))); } ); @@ -99,7 +106,7 @@ public void cancelVote(final Long voteId) { final Long memberId = memberUtils.getCurrentMemberId(); final Vote vote = voteReader.read(voteId); - voteManager.cancel(vote, memberId); + voteManager.cancel(voteId, memberId); eventPublisher.publishEvent(new RankingDecreasePopularityEvent(String.valueOf(vote.getHobby()), getVoteRedis(vote))); } @@ -162,7 +169,7 @@ public VoteGetByKeywordServiceResponse getVotesByKeyword( parameters, null ); - final long totalVoteCount = voteReader.countByKeyword(keyword); + final long totalVoteCount = voteReader.countByKeyword(keyword); // 키워드를 가진 아이템 쿼리 두 번 나감 -> 리팩토링 return new VoteGetByKeywordServiceResponse(cursorSummary, totalVoteCount); } diff --git a/lime-api/src/test/java/com/programmers/lime/domains/vote/application/VoteServiceTest.java b/lime-api/src/test/java/com/programmers/lime/domains/vote/application/VoteServiceTest.java index 170a79d34..b6847847d 100644 --- a/lime-api/src/test/java/com/programmers/lime/domains/vote/application/VoteServiceTest.java +++ b/lime-api/src/test/java/com/programmers/lime/domains/vote/application/VoteServiceTest.java @@ -4,6 +4,7 @@ import static org.mockito.BDDMockito.*; import java.time.LocalDateTime; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -28,6 +29,7 @@ import com.programmers.lime.domains.vote.domain.setup.VoteSetUp; import com.programmers.lime.domains.vote.domain.setup.VoterSetUp; import com.programmers.lime.domains.vote.implementation.VoteReader; +import com.programmers.lime.domains.vote.implementation.VoterReader; import com.programmers.lime.domains.vote.model.VoteDetailInfo; import com.programmers.lime.domains.vote.model.VoteSortCondition; import com.programmers.lime.domains.vote.model.VoteStatusCondition; @@ -51,6 +53,9 @@ class VoteServiceTest extends IntegrationTest { @Autowired private VoteReader voteReader; + @Autowired + private VoterReader voterReader; + @Autowired private ItemSetup itemSetup; @@ -150,15 +155,17 @@ class ParticipateVote { void participateVoteTest() { // given final Long itemId = vote.getItem1Id(); + final Long memberId = 1L; given(memberUtils.getCurrentMemberId()) - .willReturn(1L); + .willReturn(memberId); // when voteService.participateVote(voteId, itemId); // then - assertThat(vote.getVoters()).hasSize(1); + final Voter voter = voterReader.read(voteId, memberId); + assertThat(voter.getItemId()).isEqualTo(itemId); } @Test @@ -167,7 +174,7 @@ void reParticipateVoteTest() { // given final Long memberId = 1L; final Long selectedItemId = vote.getItem1Id(); - final Voter voter = voterSetup.saveOne(vote, memberId, selectedItemId); + voterSetup.saveOne(voteId, memberId, selectedItemId); final Long reSelectedItemId = vote.getItem2Id(); given(memberUtils.getCurrentMemberId()) @@ -177,7 +184,7 @@ void reParticipateVoteTest() { voteService.participateVote(voteId, reSelectedItemId); // then - assertThat(vote.getVoters()).hasSize(1); + final Voter voter = voterReader.read(voteId, memberId); assertThat(voter.getItemId()).isEqualTo(reSelectedItemId); } @@ -217,7 +224,7 @@ void participateVoteWithNotExistItemTest() { void cancelVoteTest() { // given final Long memberId = 1L; - voterSetup.saveOne(vote, memberId, vote.getItem1Id()); + voterSetup.saveOne(voteId, memberId, vote.getItem1Id()); given(memberUtils.getCurrentMemberId()) .willReturn(memberId); @@ -226,7 +233,8 @@ void cancelVoteTest() { voteService.cancelVote(voteId); // then - assertThat(vote.getVoters()).isEmpty(); + final Optional voter = voterReader.find(voteId, memberId); + assertThat(voter.isEmpty()).isTrue(); } @Nested @@ -303,7 +311,7 @@ void readVoteWithParticipatedTest() { // given final Long memberId = 1L; final Long selectedItemId = vote.getItem1Id(); - voterSetup.saveOne(vote, memberId, selectedItemId); + voterSetup.saveOne(voteId, memberId, selectedItemId); given(memberUtils.getCurrentMemberId()) .willReturn(memberId); @@ -319,13 +327,13 @@ void readVoteWithParticipatedTest() { @Nested class GetVotesByCursor { - Vote vote2; - Vote vote3; + Long vote2Id = 2L; + Long vote3Id = 3L; @BeforeEach void setUp() { - vote2 = voteSetup.saveOne(2L, item1.getId(), item2.getId()); - vote3 = voteSetup.saveOne(3L, item1.getId(), item2.getId()); + voteSetup.saveOne(vote2Id, item1.getId(), item2.getId()); + voteSetup.saveOne(vote3Id, item1.getId(), item2.getId()); } @Test @@ -345,9 +353,9 @@ void getVotesByCursorWithRecentTest() { // then assertThat(result.summaries()).hasSize(3); - assertThat(result.summaries().get(0).voteInfo().id()).isEqualTo(vote3.getId()); - assertThat(result.summaries().get(1).voteInfo().id()).isEqualTo(vote2.getId()); - assertThat(result.summaries().get(2).voteInfo().id()).isEqualTo(vote.getId()); + assertThat(result.summaries().get(0).voteInfo().id()).isEqualTo(vote3Id); + assertThat(result.summaries().get(1).voteInfo().id()).isEqualTo(vote2Id); + assertThat(result.summaries().get(2).voteInfo().id()).isEqualTo(voteId); } @Test @@ -357,9 +365,9 @@ void getVotesByCursorWithPopularTest() { final Hobby hobby = Hobby.BASKETBALL; final VoteSortCondition sortCondition = VoteSortCondition.POPULARITY; - voterSetup.saveOne(vote2, 1L, vote.getItem1Id()); - voterSetup.saveOne(vote2, 2L, vote.getItem1Id()); - voterSetup.saveOne(vote3, 1L, vote.getItem1Id()); + voterSetup.saveOne(vote2Id, 1L, vote.getItem1Id()); + voterSetup.saveOne(vote2Id, 2L, vote.getItem1Id()); + voterSetup.saveOne(vote3Id, 1L, vote.getItem1Id()); // when final CursorSummary result = voteService.getVotesByCursor( @@ -371,9 +379,12 @@ void getVotesByCursorWithPopularTest() { // then assertThat(result.summaries()).hasSize(3); - assertThat(result.summaries().get(0).voteInfo().id()).isEqualTo(vote2.getId()); - assertThat(result.summaries().get(1).voteInfo().id()).isEqualTo(vote3.getId()); - assertThat(result.summaries().get(2).voteInfo().id()).isEqualTo(vote.getId()); + assertThat(result.summaries().get(0).voteInfo().id()).isEqualTo(vote2Id); + assertThat(result.summaries().get(0).voteInfo().participants()).isEqualTo(2); + assertThat(result.summaries().get(1).voteInfo().id()).isEqualTo(vote3Id); + assertThat(result.summaries().get(1).voteInfo().participants()).isEqualTo(1); + assertThat(result.summaries().get(2).voteInfo().id()).isEqualTo(voteId); + assertThat(result.summaries().get(2).voteInfo().participants()).isEqualTo(0); } @Test @@ -393,9 +404,9 @@ void getVotesByCursorWithClosedTest() { // then assertThat(result.summaries()).hasSize(3); - assertThat(result.summaries().get(0).voteInfo().id()).isEqualTo(vote.getId()); - assertThat(result.summaries().get(1).voteInfo().id()).isEqualTo(vote2.getId()); - assertThat(result.summaries().get(2).voteInfo().id()).isEqualTo(vote3.getId()); + assertThat(result.summaries().get(0).voteInfo().id()).isEqualTo(voteId); + assertThat(result.summaries().get(1).voteInfo().id()).isEqualTo(vote2Id); + assertThat(result.summaries().get(2).voteInfo().id()).isEqualTo(vote3Id); } @Test @@ -419,6 +430,9 @@ void getVotesByCursorWithPostedTest() { // then assertThat(result.summaries()).hasSize(3); + assertThat(result.summaries().get(0).voteInfo().id()).isEqualTo(vote3Id); + assertThat(result.summaries().get(1).voteInfo().id()).isEqualTo(vote2Id); + assertThat(result.summaries().get(2).voteInfo().id()).isEqualTo(voteId); } @Test @@ -429,8 +443,8 @@ void getVotesByCursorWithParticipatedTest() { final VoteStatusCondition statusCondition = VoteStatusCondition.PARTICIPATED; final Long memberId = 1L; - voterSetup.saveOne(vote2, memberId, vote.getItem1Id()); - voterSetup.saveOne(vote3, memberId, vote.getItem1Id()); + voterSetup.saveOne(vote2Id, memberId, vote.getItem1Id()); + voterSetup.saveOne(vote3Id, memberId, vote.getItem1Id()); given(memberUtils.getCurrentMemberId()) .willReturn(memberId); @@ -445,8 +459,8 @@ void getVotesByCursorWithParticipatedTest() { // then assertThat(result.summaries()).hasSize(2); - assertThat(result.summaries().get(0).voteInfo().id()).isEqualTo(vote3.getId()); - assertThat(result.summaries().get(1).voteInfo().id()).isEqualTo(vote2.getId()); + assertThat(result.summaries().get(0).voteInfo().id()).isEqualTo(vote3Id); + assertThat(result.summaries().get(1).voteInfo().id()).isEqualTo(vote2Id); } @Test diff --git a/lime-domain/src/main/java/com/programmers/lime/domains/vote/domain/Vote.java b/lime-domain/src/main/java/com/programmers/lime/domains/vote/domain/Vote.java index 8ad0a20c4..7edddcede 100644 --- a/lime-domain/src/main/java/com/programmers/lime/domains/vote/domain/Vote.java +++ b/lime-domain/src/main/java/com/programmers/lime/domains/vote/domain/Vote.java @@ -1,8 +1,6 @@ package com.programmers.lime.domains.vote.domain; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; import java.util.Objects; import com.programmers.lime.common.model.Hobby; @@ -11,7 +9,6 @@ import com.programmers.lime.error.BusinessException; import com.programmers.lime.error.ErrorCode; -import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; @@ -20,7 +17,6 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import lombok.AccessLevel; import lombok.Builder; @@ -65,9 +61,6 @@ public class Vote extends BaseEntity { @Column(name = "maximum_participants", nullable = false) private int maximumParticipants; - @OneToMany(mappedBy = "vote", cascade = CascadeType.ALL) - private final List voters = new ArrayList<>(); - @Builder private Vote( final Long memberId, @@ -102,12 +95,6 @@ public boolean containsItem(final Long itemId) { return item1Id.equals(itemId) || item2Id.equals(itemId); } - public void addVoter(final Voter voter) { - if (!this.voters.contains(voter)) { - this.voters.add(voter); - } - } - public boolean isOwner(final Long memberId) { return this.memberId.equals(memberId); } @@ -121,7 +108,7 @@ public void close(final LocalDateTime now) { this.endTime = now; } - public boolean reachMaximumParticipants() { - return this.maximumParticipants == this.voters.size(); + public boolean reachMaximumParticipants(final int participants) { + return this.maximumParticipants <= participants; } } diff --git a/lime-domain/src/main/java/com/programmers/lime/domains/vote/domain/Voter.java b/lime-domain/src/main/java/com/programmers/lime/domains/vote/domain/Voter.java index 3865d2348..fca84a239 100644 --- a/lime-domain/src/main/java/com/programmers/lime/domains/vote/domain/Voter.java +++ b/lime-domain/src/main/java/com/programmers/lime/domains/vote/domain/Voter.java @@ -6,12 +6,9 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import lombok.AccessLevel; import lombok.Getter; @@ -28,9 +25,8 @@ public class Voter extends BaseEntity { @Column(name = "id") private Long id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "vote_id", nullable = false) - private Vote vote; + @Column(name = "vote_id", nullable = false) + private Long voteId; @Column(name = "member_id", nullable = false) private Long memberId; @@ -39,17 +35,16 @@ public class Voter extends BaseEntity { private Long itemId; public Voter( - final Vote vote, + final Long voteId, final Long memberId, final Long itemId ) { - this.vote = Objects.requireNonNull(vote); + this.voteId = Objects.requireNonNull(voteId); this.memberId = Objects.requireNonNull(memberId); this.itemId = Objects.requireNonNull(itemId); } public void participate(final Long itemId) { this.itemId = itemId; - this.vote.addVoter(this); } } diff --git a/lime-domain/src/main/java/com/programmers/lime/domains/vote/implementation/VoteCounter.java b/lime-domain/src/main/java/com/programmers/lime/domains/vote/implementation/VoteCounter.java deleted file mode 100644 index 0bef37e35..000000000 --- a/lime-domain/src/main/java/com/programmers/lime/domains/vote/implementation/VoteCounter.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.programmers.lime.domains.vote.implementation; - -import org.springframework.stereotype.Component; - -import com.programmers.lime.domains.vote.domain.Vote; -import com.programmers.lime.domains.vote.repository.VoterRepository; - -import lombok.RequiredArgsConstructor; - -@Component -@RequiredArgsConstructor -public class VoteCounter { - - private final VoterRepository voterRepository; - - public int count( - final Vote vote, - final Long itemId - ) { - return voterRepository.countByVoteAndItemId(vote, itemId); - } -} diff --git a/lime-domain/src/main/java/com/programmers/lime/domains/vote/implementation/VoteManager.java b/lime-domain/src/main/java/com/programmers/lime/domains/vote/implementation/VoteManager.java index 9022db9a6..7a5af7533 100644 --- a/lime-domain/src/main/java/com/programmers/lime/domains/vote/implementation/VoteManager.java +++ b/lime-domain/src/main/java/com/programmers/lime/domains/vote/implementation/VoteManager.java @@ -8,6 +8,8 @@ import com.programmers.lime.domains.vote.domain.Vote; import com.programmers.lime.domains.vote.domain.Voter; import com.programmers.lime.domains.vote.repository.VoterRepository; +import com.programmers.lime.error.BusinessException; +import com.programmers.lime.error.ErrorCode; import lombok.RequiredArgsConstructor; @@ -24,11 +26,15 @@ public void participate( final Long memberId, final Long itemId ) { - final Voter voter = new Voter(vote, memberId, itemId); + final int participants = voterReader.count(vote.getId()); + if (vote.reachMaximumParticipants(participants)) { + throw new BusinessException(ErrorCode.VOTE_CANNOT_PARTICIPATE); + } - voter.participate(itemId); + final Voter voter = new Voter(vote.getId(), memberId, itemId); + voterRepository.save(voter); - if (vote.reachMaximumParticipants()) { + if (vote.reachMaximumParticipants(participants + 1)) { vote.close(LocalDateTime.now()); } } @@ -43,10 +49,9 @@ public void reParticipate( @Transactional public void cancel( - final Vote vote, + final Long voteId, final Long memberId ) { - final Voter voter = voterReader.read(vote, memberId); - voterRepository.delete(voter); + voterRepository.deleteByVoteIdAndMemberId(voteId, memberId); } } diff --git a/lime-domain/src/main/java/com/programmers/lime/domains/vote/implementation/VoteReader.java b/lime-domain/src/main/java/com/programmers/lime/domains/vote/implementation/VoteReader.java index 0011a889b..9fccf39e3 100644 --- a/lime-domain/src/main/java/com/programmers/lime/domains/vote/implementation/VoteReader.java +++ b/lime-domain/src/main/java/com/programmers/lime/domains/vote/implementation/VoteReader.java @@ -31,7 +31,6 @@ public class VoteReader { public static final int DEFAULT_PAGING_SIZE = 20; - private final VoteCounter voteCounter; private final VoteRepository voteRepository; private final VoterReader voterReader; private final ItemReader itemReader; @@ -53,12 +52,12 @@ public VoteDetail readDetail( final ItemInfo item1Info = getItemInfo(item1Id); final ItemInfo item2Info = getItemInfo(item2Id); - final int item1Votes = voteCounter.count(vote, item1Id); - final int item2Votes = voteCounter.count(vote, item2Id); + final int item1Votes = voterReader.count(voteId, item1Id); + final int item2Votes = voterReader.count(voteId, item2Id); final VoteDetailInfo voteInfo = VoteDetailInfo.of(vote, item1Votes, item2Votes); final boolean isOwner = vote.isOwner(memberId); - final Long selectedItemId = voterReader.readItemId(vote, memberId); + final Long selectedItemId = voterReader.readItemId(voteId, memberId); return VoteDetail.builder() .item1Info(item1Info) diff --git a/lime-domain/src/main/java/com/programmers/lime/domains/vote/implementation/VoteRemover.java b/lime-domain/src/main/java/com/programmers/lime/domains/vote/implementation/VoteRemover.java index b2d556fed..6407bfbbe 100644 --- a/lime-domain/src/main/java/com/programmers/lime/domains/vote/implementation/VoteRemover.java +++ b/lime-domain/src/main/java/com/programmers/lime/domains/vote/implementation/VoteRemover.java @@ -5,6 +5,7 @@ import com.programmers.lime.domains.vote.domain.Vote; import com.programmers.lime.domains.vote.repository.VoteRepository; +import com.programmers.lime.domains.vote.repository.VoterRepository; import lombok.RequiredArgsConstructor; @@ -13,9 +14,11 @@ public class VoteRemover { private final VoteRepository voteRepository; + private final VoterRepository voterRepository; @Transactional public void remove(final Vote vote) { voteRepository.delete(vote); + voterRepository.deleteByVoteId(vote.getId()); } } diff --git a/lime-domain/src/main/java/com/programmers/lime/domains/vote/implementation/VoterReader.java b/lime-domain/src/main/java/com/programmers/lime/domains/vote/implementation/VoterReader.java index c363a0a52..112b57420 100644 --- a/lime-domain/src/main/java/com/programmers/lime/domains/vote/implementation/VoterReader.java +++ b/lime-domain/src/main/java/com/programmers/lime/domains/vote/implementation/VoterReader.java @@ -5,7 +5,6 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import com.programmers.lime.domains.vote.domain.Vote; import com.programmers.lime.domains.vote.domain.Voter; import com.programmers.lime.domains.vote.repository.VoterRepository; import com.programmers.lime.error.EntityNotFoundException; @@ -19,29 +18,42 @@ public class VoterReader { private final VoterRepository voterRepository; - @Transactional(readOnly = true) + @Transactional(readOnly = true) // 삭제하기 public Long readItemId( - final Vote vote, + final Long voteId, final Long memberId ) { - return voterRepository.findByVoteAndMemberId(vote, memberId) + return voterRepository.findByVoteIdAndMemberId(voteId, memberId) .map(Voter::getItemId).orElse(null); } @Transactional(readOnly = true) public Optional find( - final Vote vote, + final Long voteId, final Long memberId ) { - return voterRepository.findByVoteAndMemberId(vote, memberId); + return voterRepository.findByVoteIdAndMemberId(voteId, memberId); } @Transactional(readOnly = true) public Voter read( - final Vote vote, + final Long voteId, final Long memberId ) { - return voterRepository.findByVoteAndMemberId(vote, memberId) + return voterRepository.findByVoteIdAndMemberId(voteId, memberId) .orElseThrow(() -> new EntityNotFoundException(ErrorCode.VOTER_NOT_FOUND)); } + + @Transactional(readOnly = true) + public int count(final Long voteId) { + return voterRepository.countByVoteId(voteId); + } + + @Transactional(readOnly = true) + public int count( + final Long voteId, + final Long itemId + ) { + return voterRepository.countByVoteIdAndItemId(voteId, itemId); + } } diff --git a/lime-domain/src/main/java/com/programmers/lime/domains/vote/model/VoteInfo.java b/lime-domain/src/main/java/com/programmers/lime/domains/vote/model/VoteInfo.java index 7dbd6b4e6..97e7f64b0 100644 --- a/lime-domain/src/main/java/com/programmers/lime/domains/vote/model/VoteInfo.java +++ b/lime-domain/src/main/java/com/programmers/lime/domains/vote/model/VoteInfo.java @@ -10,14 +10,14 @@ public record VoteInfo( String content, LocalDateTime startTime, boolean isVoting, - int participants + long participants ) { public VoteInfo( final Long id, final String content, final LocalDateTime startTime, final LocalDateTime endTime, - final int participants + final long participants ) { this(id, content, startTime, isVoting(endTime), participants); } diff --git a/lime-domain/src/main/java/com/programmers/lime/domains/vote/repository/VoteRepositoryForCursorImpl.java b/lime-domain/src/main/java/com/programmers/lime/domains/vote/repository/VoteRepositoryForCursorImpl.java index b84cd2b22..ffd935c82 100644 --- a/lime-domain/src/main/java/com/programmers/lime/domains/vote/repository/VoteRepositoryForCursorImpl.java +++ b/lime-domain/src/main/java/com/programmers/lime/domains/vote/repository/VoteRepositoryForCursorImpl.java @@ -2,6 +2,7 @@ import static com.programmers.lime.domains.item.domain.QItem.*; import static com.programmers.lime.domains.vote.domain.QVote.*; +import static com.programmers.lime.domains.vote.domain.QVoter.*; import java.util.List; @@ -45,21 +46,22 @@ public List findAllByCursor( vote.content.content, vote.startTime, vote.endTime, - vote.voters.size() + voter.count().as("participants") ), vote.item1Id, vote.item2Id, generateCursorId(sortCondition) )) .from(vote) + .leftJoin(voter).on(voter.voteId.eq(vote.id)) .where( eqHobby(hobby), getExpressionBy(statusCondition, memberId), containsKeyword(keyword), lessThanNextCursorId(sortCondition, nextCursorId) ) - .orderBy(getOrderSpecifierBy(sortCondition), - vote.id.desc()) + .groupBy(vote.id) + .orderBy(getOrderSpecifierBy(sortCondition), vote.id.desc()) .limit(pageSize) .fetch(); } @@ -106,14 +108,13 @@ private BooleanExpression isPosted(final Long memberId) { } private BooleanExpression isParticipatedIn(final Long memberId) { - return vote.voters.any() - .memberId.eq(memberId); + return voter.voteId.eq(vote.id).and(voter.memberId.eq(memberId)); } private OrderSpecifier getOrderSpecifierBy(final VoteSortCondition sortCondition) { switch (sortCondition) { case POPULARITY -> { - return new OrderSpecifier<>(Order.DESC, vote.voters.size()); + return new OrderSpecifier<>(Order.DESC, getVoterCount()); } case RECENT -> { return new OrderSpecifier<>(Order.DESC, vote.createdAt); @@ -127,6 +128,10 @@ private OrderSpecifier getOrderSpecifierBy(final VoteSortCondition sortCondit } } + private NumberExpression getVoterCount() { + return voter.voteId.eq(vote.id).count(); + } + private BooleanExpression containsKeyword(final String keyword) { if (keyword == null) { return null; @@ -158,7 +163,7 @@ private BooleanExpression lessThanNextCursorId( private StringExpression generateCursorId(final VoteSortCondition sortCondition) { if (sortCondition == VoteSortCondition.POPULARITY) { - final NumberExpression popularity = vote.voters.size(); + final NumberExpression popularity = getVoterCount(); return StringExpressions.lpad( popularity.stringValue(), 8, '0' diff --git a/lime-domain/src/main/java/com/programmers/lime/domains/vote/repository/VoterRepository.java b/lime-domain/src/main/java/com/programmers/lime/domains/vote/repository/VoterRepository.java index 1552ac13b..bfd0c7c1c 100644 --- a/lime-domain/src/main/java/com/programmers/lime/domains/vote/repository/VoterRepository.java +++ b/lime-domain/src/main/java/com/programmers/lime/domains/vote/repository/VoterRepository.java @@ -4,17 +4,25 @@ import org.springframework.data.jpa.repository.JpaRepository; -import com.programmers.lime.domains.vote.domain.Vote; import com.programmers.lime.domains.vote.domain.Voter; public interface VoterRepository extends JpaRepository { - int countByVoteAndItemId( - final Vote vote, + int countByVoteId(final Long voteId); + + int countByVoteIdAndItemId( + final Long voteId, final Long itemId ); - Optional findByVoteAndMemberId( - final Vote vote, + Optional findByVoteIdAndMemberId( + final Long voteId, + final Long memberId + ); + + void deleteByVoteId(final Long voteId); + + void deleteByVoteIdAndMemberId( + final Long voteId, final Long memberId ); } diff --git a/lime-domain/src/test/java/com/programmers/lime/domains/vote/implementation/VoteCounterTest.java b/lime-domain/src/test/java/com/programmers/lime/domains/vote/implementation/VoteCounterTest.java deleted file mode 100644 index 52b729db8..000000000 --- a/lime-domain/src/test/java/com/programmers/lime/domains/vote/implementation/VoteCounterTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.programmers.lime.domains.vote.implementation; - -import static org.assertj.core.api.Assertions.*; -import static org.mockito.BDDMockito.*; - -import java.util.List; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import com.programmers.lime.domains.vote.domain.Vote; -import com.programmers.lime.domains.vote.domain.VoteBuilder; -import com.programmers.lime.domains.vote.domain.Voter; -import com.programmers.lime.domains.vote.domain.VoterBuilder; -import com.programmers.lime.domains.vote.repository.VoterRepository; - -@ExtendWith(MockitoExtension.class) -class VoteCounterTest { - - @InjectMocks - private VoteCounter voteCounter; - - @Mock - private VoterRepository voterRepository; - - @Test - @DisplayName("투표에서 아이템에 투표한 투표자의 수를 센다.") - void countTest() { - // given - final int voterSize = 3; - final Vote vote = VoteBuilder.build(); - final Long itemId = 1L; - final List voters = VoterBuilder.buildMany(voterSize, vote, itemId); - - given(voterRepository.countByVoteAndItemId(any(Vote.class), anyLong())) - .willReturn(voterSize); - - // when - final int count = voteCounter.count(vote, itemId); - - // then - assertThat(count).isEqualTo(voters.size()); - } -} diff --git a/lime-domain/src/test/java/com/programmers/lime/domains/vote/implementation/VoteManagerTest.java b/lime-domain/src/test/java/com/programmers/lime/domains/vote/implementation/VoteManagerTest.java index fca4a3d6c..6f09b9e54 100644 --- a/lime-domain/src/test/java/com/programmers/lime/domains/vote/implementation/VoteManagerTest.java +++ b/lime-domain/src/test/java/com/programmers/lime/domains/vote/implementation/VoteManagerTest.java @@ -38,14 +38,22 @@ void participateTest() { // given final Vote vote = VoteBuilder.build(); final Long memberId = 1L; - final Long itemId = 1L; + final Long itemId = vote.getItem1Id(); + + given(voterRepository.save(any(Voter.class))) + .willReturn(VoterBuilder.build(vote.getId(), memberId, itemId)); + + given(voterReader.count(vote.getId())) + .willReturn(vote.getMaximumParticipants() - 1); // when voteManager.participate(vote, memberId, itemId); // then - assertThat(vote.getVoters()).hasSize(1); assertThat(vote.isVoting()).isTrue(); + + // verify + then(voterRepository).should().save(any(Voter.class)); } @Test @@ -54,17 +62,22 @@ void participateAndCloseTest() { // given final Vote vote = VoteBuilder.build(); final Long memberId = 1L; - final Long itemId = 1L; + final Long itemId = vote.getItem1Id(); - vote.addVoter(VoterBuilder.build(vote, 2L, 1L)); - vote.addVoter(VoterBuilder.build(vote, 3L, 2L)); + given(voterRepository.save(any(Voter.class))) + .willReturn(VoterBuilder.build(vote.getId(), memberId, itemId)); + + given(voterReader.count(vote.getId())) + .willReturn(vote.getMaximumParticipants()); // when voteManager.participate(vote, memberId, itemId); // then - assertThat(vote.getVoters()).hasSize(3); assertThat(vote.isVoting()).isFalse(); + + // verify + then(voterRepository).should().save(any(Voter.class)); } } @@ -72,10 +85,9 @@ void participateAndCloseTest() { @DisplayName("재참여한다.") void reParticipateTest() { // given - final Vote vote = VoteBuilder.build(); final Long originSelectedItemId = 1L; final Long newSelectedItemId = 2L; - final Voter voter = VoterBuilder.build(vote, 1L, originSelectedItemId); + final Voter voter = VoterBuilder.build(1L, 1L, originSelectedItemId); // when voteManager.reParticipate(newSelectedItemId, voter); @@ -88,20 +100,16 @@ void reParticipateTest() { @DisplayName("투표를 취소한다.") void cancelTest() { // given - final Vote vote = VoteBuilder.build(); + final Long voteId = 1L; final Long memberId = 1L; - final Voter voter = VoterBuilder.build(vote, memberId, 1L); - - vote.addVoter(voter); - given(voterReader.read(any(Vote.class), anyLong())) - .willReturn(voter); - doNothing().when(voterRepository).delete(voter); + doNothing() + .when(voterRepository).deleteByVoteIdAndMemberId(voteId, memberId); // when - voteManager.cancel(vote, memberId); + voteManager.cancel(voteId, memberId); // then - then(voterRepository).should().delete(voter); + then(voterRepository).should().deleteByVoteIdAndMemberId(voteId, memberId); } } diff --git a/lime-domain/src/test/java/com/programmers/lime/domains/vote/implementation/VoteReaderTest.java b/lime-domain/src/test/java/com/programmers/lime/domains/vote/implementation/VoteReaderTest.java index 16ff29c5c..986d40a80 100644 --- a/lime-domain/src/test/java/com/programmers/lime/domains/vote/implementation/VoteReaderTest.java +++ b/lime-domain/src/test/java/com/programmers/lime/domains/vote/implementation/VoteReaderTest.java @@ -42,9 +42,6 @@ class VoteReaderTest { @InjectMocks private VoteReader voteReader; - @Mock - private VoteCounter voteCounter; - @Mock private VoteRepository voteRepository; @@ -114,11 +111,11 @@ void readDetailTest() { .willReturn(item1); given(itemReader.read(item2Id)) .willReturn(item2); - given(voteCounter.count(any(Vote.class), eq(item1Id))) + given(voterReader.count(anyLong(), eq(item1Id))) .willReturn(item1Votes); - given(voteCounter.count(any(Vote.class), eq(item2Id))) + given(voterReader.count(anyLong(), eq(item2Id))) .willReturn(item2Votes); - given(voterReader.readItemId(any(Vote.class), anyLong())) + given(voterReader.readItemId(anyLong(), anyLong())) .willReturn(item1Id); // when diff --git a/lime-domain/src/test/java/com/programmers/lime/domains/vote/implementation/VoteRemoverTest.java b/lime-domain/src/test/java/com/programmers/lime/domains/vote/implementation/VoteRemoverTest.java index a72b3369a..3b45711be 100644 --- a/lime-domain/src/test/java/com/programmers/lime/domains/vote/implementation/VoteRemoverTest.java +++ b/lime-domain/src/test/java/com/programmers/lime/domains/vote/implementation/VoteRemoverTest.java @@ -12,6 +12,7 @@ import com.programmers.lime.domains.vote.domain.Vote; import com.programmers.lime.domains.vote.domain.VoteBuilder; import com.programmers.lime.domains.vote.repository.VoteRepository; +import com.programmers.lime.domains.vote.repository.VoterRepository; @ExtendWith(MockitoExtension.class) class VoteRemoverTest { @@ -22,6 +23,9 @@ class VoteRemoverTest { @Mock private VoteRepository voteRepository; + @Mock + private VoterRepository voterRepository; + @Test @DisplayName("투표를 삭제한다.") void removeTest() { @@ -31,6 +35,9 @@ void removeTest() { willDoNothing() .given(voteRepository).delete(any(Vote.class)); + willDoNothing() + .given(voterRepository).deleteByVoteId(anyLong()); + // when voteRemover.remove(vote); diff --git a/lime-domain/src/test/java/com/programmers/lime/domains/vote/implementation/VoterReaderTest.java b/lime-domain/src/test/java/com/programmers/lime/domains/vote/implementation/VoterReaderTest.java index f9d7f0477..4ee07b019 100644 --- a/lime-domain/src/test/java/com/programmers/lime/domains/vote/implementation/VoterReaderTest.java +++ b/lime-domain/src/test/java/com/programmers/lime/domains/vote/implementation/VoterReaderTest.java @@ -1,6 +1,7 @@ package com.programmers.lime.domains.vote.implementation; import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.BDDMockito.*; import java.util.Optional; @@ -13,8 +14,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import com.programmers.lime.domains.vote.domain.Vote; -import com.programmers.lime.domains.vote.domain.VoteBuilder; import com.programmers.lime.domains.vote.domain.Voter; import com.programmers.lime.domains.vote.domain.VoterBuilder; import com.programmers.lime.domains.vote.repository.VoterRepository; @@ -36,16 +35,16 @@ class ReadItemIdTest { @DisplayName("투표와 회원 id가 일치하는 투표자가 있다면 투표자가 선택한 아이템 id를 반환한다.") void readVoterSelectedItemIdTest() { // given - final Vote vote = VoteBuilder.build(); + final Long voteId = 1L; final Long memberId = 1L; final Long itemId = 1L; - final Voter voter = VoterBuilder.build(vote, memberId, itemId); + final Voter voter = VoterBuilder.build(voteId, memberId, itemId); - given(voterRepository.findByVoteAndMemberId(any(Vote.class), anyLong())) + given(voterRepository.findByVoteIdAndMemberId(anyLong(), anyLong())) .willReturn(Optional.of(voter)); // when - final Long result = voterReader.readItemId(vote, memberId); + final Long result = voterReader.readItemId(voteId, memberId); // then assertThat(result).isEqualTo(voter.getItemId()); @@ -55,14 +54,14 @@ void readVoterSelectedItemIdTest() { @DisplayName("투표와 회원 id가 일치하는 투표자가 없다면 null을 반환한다.") void readNullItemIdTest() { // given - final Vote vote = VoteBuilder.build(); + final Long voteId = 1L; final Long memberId = 1L; - given(voterRepository.findByVoteAndMemberId(any(Vote.class), anyLong())) + given(voterRepository.findByVoteIdAndMemberId(anyLong(), anyLong())) .willReturn(Optional.empty()); // when - final Long result = voterReader.readItemId(vote, memberId); + final Long result = voterReader.readItemId(voteId, memberId); // then assertThat(result).isNull(); @@ -73,15 +72,15 @@ void readNullItemIdTest() { @DisplayName("투표와 회원 id가 일치하는 투표자가 있다면 Optional로 투표자를 반환한다.") void findTest() { // given - final Vote vote = VoteBuilder.build(); + final Long voteId = 1L; final Long memberId = 1L; - final Voter voter = VoterBuilder.build(vote, memberId, 1L); + final Voter voter = VoterBuilder.build(voteId, memberId, 1L); - given(voterRepository.findByVoteAndMemberId(any(Vote.class), anyLong())) + given(voterRepository.findByVoteIdAndMemberId(anyLong(), anyLong())) .willReturn(Optional.of(voter)); // when - final Optional result = voterReader.find(vote, memberId); + final Optional result = voterReader.find(voteId, memberId); // then assertThat(result).isEqualTo(Optional.of(voter)); @@ -94,15 +93,15 @@ class ReadTest { @DisplayName("투표와 회원 id가 일치하는 투표자가 있다면 기존의 투표자를 반환한다.") void readExistingVoterTest() { // given - final Vote vote = VoteBuilder.build(); + final Long voteId = 1L; final Long memberId = 1L; - final Voter voter = VoterBuilder.build(vote, memberId, 1L); + final Voter voter = VoterBuilder.build(voteId, memberId, 1L); - given(voterRepository.findByVoteAndMemberId(any(Vote.class), anyLong())) + given(voterRepository.findByVoteIdAndMemberId(anyLong(), anyLong())) .willReturn(Optional.of(voter)); // when - final Voter result = voterReader.read(vote, memberId); + final Voter result = voterReader.read(voteId, memberId); // then assertThat(result).usingRecursiveComparison() @@ -113,16 +112,48 @@ void readExistingVoterTest() { @DisplayName("투표와 회원 id가 일치하는 투표자가 없다면 예외가 발생한다.") void occurExceptionIfNoVoterTest() { // given - final Vote vote = VoteBuilder.build(); - final Long memberId = 1L; - - given(voterRepository.findByVoteAndMemberId(any(Vote.class), anyLong())) + given(voterRepository.findByVoteIdAndMemberId(anyLong(), anyLong())) .willReturn(Optional.empty()); // when & then assertThatThrownBy( - () -> voterReader.read(vote, memberId) + () -> voterReader.read(1L, 1L) ).isInstanceOf(EntityNotFoundException.class); } } + + @Test + @DisplayName("투표에 참여한 투표자 수를 반환한다.") + void countTest() { + // given + final Long voteId = 1L; + final int count = 100; + + given(voterRepository.countByVoteId(anyLong())) + .willReturn(count); + + // when + final int result = voterReader.count(voteId); + + // then + assertThat(result).isEqualTo(count); + } + + @Test + @DisplayName("투표와 아이템 id가 일치하는 투표자 수를 반환한다.") + void countByVoteIdAndItemIdTest() { + // given + final Long voteId = 1L; + final Long itemId = 1L; + final int count = 100; + + given(voterRepository.countByVoteIdAndItemId(anyLong(), anyLong())) + .willReturn(count); + + // when + final int result = voterReader.count(voteId, itemId); + + // then + assertThat(result).isEqualTo(count); + } } diff --git a/lime-domain/src/testFixtures/java/com/programmers/lime/domains/vote/domain/VoterBuilder.java b/lime-domain/src/testFixtures/java/com/programmers/lime/domains/vote/domain/VoterBuilder.java index 9ba9b4071..9c95f02f6 100644 --- a/lime-domain/src/testFixtures/java/com/programmers/lime/domains/vote/domain/VoterBuilder.java +++ b/lime-domain/src/testFixtures/java/com/programmers/lime/domains/vote/domain/VoterBuilder.java @@ -12,12 +12,12 @@ public class VoterBuilder { public static List buildMany( final long size, - final Vote vote, + final Long voteId, final Long itemId ) { return LongStream.range(1, size + 1) .mapToObj(i -> { - Voter voter = VoterBuilder.build(vote, i, itemId); + Voter voter = VoterBuilder.build(voteId, i, itemId); setVoterId(voter, i); return voter; @@ -26,11 +26,11 @@ public static List buildMany( } public static Voter build( - final Vote vote, + final Long voteId, final Long memberId, final Long itemId ) { - final Voter voter = new Voter(vote, memberId, itemId); + final Voter voter = new Voter(voteId, memberId, itemId); setVoterId(voter, 1L); diff --git a/lime-domain/src/testFixtures/java/com/programmers/lime/domains/vote/domain/setup/VoterSetUp.java b/lime-domain/src/testFixtures/java/com/programmers/lime/domains/vote/domain/setup/VoterSetUp.java index 726612264..0fe1cef26 100644 --- a/lime-domain/src/testFixtures/java/com/programmers/lime/domains/vote/domain/setup/VoterSetUp.java +++ b/lime-domain/src/testFixtures/java/com/programmers/lime/domains/vote/domain/setup/VoterSetUp.java @@ -2,7 +2,6 @@ import org.springframework.stereotype.Component; -import com.programmers.lime.domains.vote.domain.Vote; import com.programmers.lime.domains.vote.domain.Voter; import com.programmers.lime.domains.vote.repository.VoterRepository; @@ -15,11 +14,11 @@ public class VoterSetUp { private final VoterRepository voterRepository; public Voter saveOne( - final Vote vote, + final Long voteId, final Long memberId, final Long itemId ) { - final Voter voter = new Voter(vote, memberId, itemId); + final Voter voter = new Voter(voteId, memberId, itemId); return voterRepository.save(voter); diff --git a/lime-domain/src/testFixtures/java/com/programmers/lime/domains/vote/model/VoteCursorSummaryBuilder.java b/lime-domain/src/testFixtures/java/com/programmers/lime/domains/vote/model/VoteCursorSummaryBuilder.java index b9c32e4ac..ec2623c1d 100644 --- a/lime-domain/src/testFixtures/java/com/programmers/lime/domains/vote/model/VoteCursorSummaryBuilder.java +++ b/lime-domain/src/testFixtures/java/com/programmers/lime/domains/vote/model/VoteCursorSummaryBuilder.java @@ -21,7 +21,7 @@ public static List buildMany(final int size) { .content(vote.getContent()) .startTime(vote.getStartTime()) .isVoting(false) - .participants(vote.getVoters().size()) + .participants(100) .build(); final String cursorId = generateCursorId(vote); final VoteCursorSummary voteCursorSummary = new VoteCursorSummary(voteInfo, vote.getItem1Id(), vote.getItem2Id(), cursorId); diff --git a/lime-infrastructure/src/main/java/com/programmers/lime/redis/vote/VoteRedisManager.java b/lime-infrastructure/src/main/java/com/programmers/lime/redis/vote/VoteRedisManager.java index 07339b2c2..c87aae80a 100644 --- a/lime-infrastructure/src/main/java/com/programmers/lime/redis/vote/VoteRedisManager.java +++ b/lime-infrastructure/src/main/java/com/programmers/lime/redis/vote/VoteRedisManager.java @@ -1,5 +1,6 @@ package com.programmers.lime.redis.vote; +import java.time.Duration; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -68,4 +69,12 @@ public void updateRanking( deleteRanking(hobby, rankingInfo); } } + + public Boolean lock(final String key) { + return redisTemplate.opsForValue().setIfAbsent(key, "LOCK", Duration.ofSeconds(3)); + } + + public void unlock(final String key) { + redisTemplate.delete(key); + } }