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

Fix/#547 킬링파트 여러 사용자 동시 좋아요 누를 때 동시성 이슈 해결 #548

Merged
merged 4 commits into from
Oct 29, 2023
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
Expand Up @@ -30,11 +30,9 @@ public void updateLikeStatus(
final KillingPartLikeRequest request
) {
final Member member = memberRepository.findById(memberId)
.orElseThrow(
() -> new MemberException.MemberNotExistException(
Map.of("MemberId", String.valueOf(memberId))
)
);
.orElseThrow(() -> new MemberException.MemberNotExistException(
Map.of("MemberId", String.valueOf(memberId))
));

final KillingPart killingPart = killingPartRepository.findById(killingPartId)
.orElseThrow(() -> new KillingPartException.PartNotExistException(
Expand All @@ -53,20 +51,25 @@ private void create(final KillingPart killingPart, final Member member) {
return;
}

final KillingPartLike likeOnKillingPart =
likeRepository.findByKillingPartAndMember(killingPart, member)
.orElseGet(() -> createNewLike(killingPart, member));

killingPart.like(likeOnKillingPart);
final KillingPartLike likeOnKillingPart = likeRepository.findByKillingPartAndMember(killingPart, member)
.orElseGet(() -> createNewLike(killingPart, member));
if (likeOnKillingPart.isDeleted()) {
likeRepository.pressLike(likeOnKillingPart.getId());
killingPartRepository.increaseLikeCount(killingPart.getId());
}
}

private KillingPartLike createNewLike(final KillingPart killingPart, final Member member) {
final KillingPartLike like = new KillingPartLike(killingPart, member);

return likeRepository.save(like);
}

private void delete(final KillingPart killingPart, final Member member) {
killingPart.findLikeByMember(member)
.ifPresent(killingPart::unlike);
.ifPresent(likeOnKillingPart -> {
likeRepository.cancelLike(likeOnKillingPart.getId());
killingPartRepository.decreaseLikeCount(killingPart.getId());
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
Expand Down Expand Up @@ -32,4 +33,12 @@ List<SongKillingPartKillingPartLikeCreatedAtDto> findLikedKillingPartAndSongByMe
+ "FROM KillingPartLike kp_like "
+ "WHERE kp_like.member=:member and kp_like.isDeleted=false")
List<Long> findLikedKillingPartIdsByMember(@Param("member") final Member member);

@Query("update KillingPartLike kp_like set kp_like.isDeleted = false where kp_like.id = :id")
@Modifying(clearAutomatically = true, flushAutomatically = true)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

굿입니다 👍🏻

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

부끄럽네요 (^///^) --<-<-<@

void pressLike(@Param("id") final Long killingPartLikeId);

@Query("update KillingPartLike kp_like set kp_like.isDeleted = true where kp_like.id = :id")
@Modifying(clearAutomatically = true, flushAutomatically = true)
void cancelLike(@Param("id") final Long killingPartLikeId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import shook.shook.song.domain.Song;
import shook.shook.song.domain.killingpart.KillingPart;
Expand All @@ -10,4 +13,12 @@
public interface KillingPartRepository extends JpaRepository<KillingPart, Long> {

List<KillingPart> findAllBySong(final Song song);

@Query("update KillingPart kp set kp.likeCount = kp.likeCount + 1 where kp.id = :id")
@Modifying(clearAutomatically = true, flushAutomatically = true)
void increaseLikeCount(@Param("id") final Long killingPartLikeId);

@Query("update KillingPart kp set kp.likeCount = kp.likeCount - 1 where kp.id = :id")
@Modifying(clearAutomatically = true, flushAutomatically = true)
void decreaseLikeCount(@Param("id") final Long killingPartLikeId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package shook.shook.song.application.killingpart;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
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.test.context.jdbc.Sql;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
import shook.shook.member.domain.Member;
import shook.shook.member.domain.repository.MemberRepository;
import shook.shook.song.application.killingpart.dto.KillingPartLikeRequest;
import shook.shook.song.domain.killingpart.KillingPart;
import shook.shook.song.domain.killingpart.repository.KillingPartLikeRepository;
import shook.shook.song.domain.killingpart.repository.KillingPartRepository;

@Sql("classpath:/killingpart/initialize_killing_part_song.sql")
@SpringBootTest
class KillingPartLikeConcurrencyTest {

private static KillingPart SAVED_KILLING_PART;
private static Member SAVED_MEMBER;

@Autowired
private KillingPartRepository killingPartRepository;

@Autowired
private KillingPartLikeRepository killingPartLikeRepository;

@Autowired
private MemberRepository memberRepository;

@Autowired
private PlatformTransactionManager transactionManager;

private KillingPartLikeService likeService;
private TransactionTemplate transactionTemplate;

@BeforeEach
void setUp() {
SAVED_KILLING_PART = killingPartRepository.findById(1L).get();
SAVED_MEMBER = memberRepository.findById(1L).get();
likeService = new KillingPartLikeService(killingPartRepository, memberRepository, killingPartLikeRepository);
transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
}

@DisplayName("두 사용자가 동시에 좋아요를 누르면 좋아요 개수가 2 증가한다.")
@Test
void likeByMultiplePeople() throws InterruptedException {
// given
final Member first = SAVED_MEMBER;
final Member second = memberRepository.save(new Member("[email protected]", "second"));

// when
ExecutorService executorService = Executors.newFixedThreadPool(2);

CountDownLatch latch = new CountDownLatch(2);
final KillingPartLikeRequest request = new KillingPartLikeRequest(true);

executorService.execute(() ->
transactionTemplate.execute((status -> {
likeService.updateLikeStatus(SAVED_KILLING_PART.getId(), first.getId(), request);
latch.countDown();
return null;
}))
);
executorService.execute(() ->
transactionTemplate.execute((status -> {
likeService.updateLikeStatus(SAVED_KILLING_PART.getId(), second.getId(), request);
latch.countDown();
return null;
}))
);
latch.await();
Thread.sleep(1000);

// then
final KillingPart killingPart = killingPartRepository.findById(SAVED_KILLING_PART.getId()).get();
assertThat(killingPart.getLikeCount()).isEqualTo(2);
}

@Disabled("UPDATE + 1 사용 시 한 사용자의 동시에 도착하는 좋아요 요청 동시성 문제 발생")
@DisplayName("한 사용자가 좋아요, 취소, 좋아요를 누르면 좋아요 개수가 1 증가한다.")
@Test
void likeByOnePersonMultipleTimes() throws InterruptedException {
// given
final Member first = SAVED_MEMBER;

// when
ExecutorService executorService = Executors.newFixedThreadPool(3);

CountDownLatch latch = new CountDownLatch(3);
final KillingPartLikeRequest likeRequest = new KillingPartLikeRequest(true);
final KillingPartLikeRequest unlikeRequest = new KillingPartLikeRequest(false);

executorService.execute(() ->
transactionTemplate.execute((status -> {
likeService.updateLikeStatus(SAVED_KILLING_PART.getId(), first.getId(), likeRequest);
latch.countDown();
return null;
}))
);
executorService.execute(() ->
transactionTemplate.execute((status -> {
likeService.updateLikeStatus(SAVED_KILLING_PART.getId(), first.getId(), unlikeRequest);
latch.countDown();
return null;
}))
);
executorService.execute(() ->
transactionTemplate.execute((status -> {
likeService.updateLikeStatus(SAVED_KILLING_PART.getId(), first.getId(), likeRequest);
latch.countDown();
return null;
}))
);

latch.await();
Thread.sleep(1000);

// then
final KillingPart killingPart = killingPartRepository.findById(SAVED_KILLING_PART.getId()).get();
assertThat(killingPart.getLikeCount()).isEqualTo(1); // 예상: 2, 결과: 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -192,4 +192,41 @@ void findLikedKillingPartIdsByMember() {
SECOND_SAVED_KILLING_PART.getId()
);
}

@DisplayName("좋아요 데이터를 삭제되지 않은 상태로 변경한다. (좋아요를 누른다.)")
@Test
void pressLike() {
// given
final KillingPartLike killingPartLike = new KillingPartLike(FIRST_SAVED_KILLING_PART, SAVED_MEMBER);
killingPartLikeRepository.save(killingPartLike);

// when
killingPartLikeRepository.pressLike(killingPartLike.getId());

// then
final Optional<KillingPartLike> foundLike = killingPartLikeRepository.findById(killingPartLike.getId());

assertThat(foundLike).isPresent()
.get()
.hasFieldOrPropertyWithValue("isDeleted", false);
}

@DisplayName("좋아요 데이터를 삭제된 상태로 변경한다. (좋아요를 취소한다.)")
@Test
void cancelLike() {
// given
final KillingPartLike killingPartLike = new KillingPartLike(FIRST_SAVED_KILLING_PART, SAVED_MEMBER);
killingPartLikeRepository.save(killingPartLike);
killingPartLikeRepository.pressLike(killingPartLike.getId());

// when
killingPartLikeRepository.cancelLike(killingPartLike.getId());

// then
final Optional<KillingPartLike> foundLike = killingPartLikeRepository.findById(killingPartLike.getId());

assertThat(foundLike).isPresent()
.get()
.hasFieldOrPropertyWithValue("isDeleted", true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,41 @@ void findAllBySong() {
List.of(FIRST_KILLING_PART, SECOND_KILLING_PART, THIRD_KILLING_PART)
);
}

@DisplayName("한 킬링파트에 UPDATE + 1로 좋아요 수를 증가시킨다.")
@Test
void increaseLikeCount() {
// given
killingPartRepository.saveAll(KILLING_PARTS.getKillingParts());
final KillingPart killingPart = killingPartRepository.findById(FIRST_KILLING_PART.getId()).get();
final int initialLikeCount = killingPart.getLikeCount();

// when
saveAndClearEntityManager();
killingPartRepository.increaseLikeCount(killingPart.getId());

// then
final KillingPart foundKillingPart = killingPartRepository.findById(killingPart.getId()).get();

assertThat(foundKillingPart.getLikeCount()).isEqualTo(initialLikeCount + 1);
}

@DisplayName("한 킬링파트에 UPDATE - 1로 좋아요 수를 감소시킨다.")
@Test
void decreaseLikeCount() {
// given
killingPartRepository.saveAll(KILLING_PARTS.getKillingParts());
killingPartRepository.increaseLikeCount(FIRST_KILLING_PART.getId());
final KillingPart killingPart = killingPartRepository.findById(FIRST_KILLING_PART.getId()).get();
final int initialLikeCount = killingPart.getLikeCount();

// when
saveAndClearEntityManager();
killingPartRepository.decreaseLikeCount(killingPart.getId());

// then
final KillingPart foundKillingPart = killingPartRepository.findById(killingPart.getId()).get();

assertThat(foundKillingPart.getLikeCount()).isEqualTo(initialLikeCount - 1);
}
}
Loading