Skip to content

Commit

Permalink
Fix/#547 킬링파트 여러 사용자 동시 좋아요 누를 때 동시성 이슈 해결 (#548)
Browse files Browse the repository at this point in the history
* fix: 킬링파트 여러 사용자 동시 좋아요 누를 때 동시성 이슈 해결

* refactor: 불필요한 Optional 변수 추출 제거
  • Loading branch information
somsom13 authored Oct 29, 2023
1 parent 5760dae commit ba423bc
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 11 deletions.
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)
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);
}
}

0 comments on commit ba423bc

Please sign in to comment.