Skip to content

Commit

Permalink
feat: 좋아요 수 집계에 대한 동시성 문제 해결 (#567)
Browse files Browse the repository at this point in the history
* feat: 장소 통계 도메인에 대해 낙관적 락 적용

* feat: 퍼사드 계층에서 낙관적 락 재시도 로직 구현

* feat: 낙관적 락을 사용하는 Repository 메서드 분리

* fix: `@Param` 어노테이션 추가

* fix: catch 하는 예외를 OptimisticLockingFailureException 로 구체화

* refactor: 재시도 로직을 사용할 대상 클래스를 PlaceStatistics 에서 PlaceLike 로 변경

* refactor: 낙관적 락에서 비관적 락으로 변경

* test: 동시성 테스트 수정
  • Loading branch information
kokodak authored Nov 20, 2023
1 parent 3d48146 commit fd793a1
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ public MyPlaceLikeType checkMyLike(final CheckMyPlaceLikeCommand checkMyPlaceLik
final Long placeId = checkMyPlaceLikeCommand.placeId();

return placeLikeRepository.findByPlaceIdAndPlayerId(placeId, playerId)
.map(PlaceLike::getType)
.map(MyPlaceLikeType::from)
.orElse(MyPlaceLikeType.NONE);
.map(PlaceLike::getType)
.map(MyPlaceLikeType::from)
.orElse(MyPlaceLikeType.NONE);
}

@Transactional(readOnly = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,16 @@ public PlaceStatistics createPlaceStatistics(final CreatePlaceStatisticsCommand

public void plusLike(final PlusLikeCommand plusLikeCommand) {
final Long placeId = plusLikeCommand.placeId();
final PlaceStatistics placeStatistics = placeStatisticsRepository.findByPlaceId(placeId)
final PlaceStatistics placeStatistics = placeStatisticsRepository.findByPlaceIdForUpdate(placeId)
.orElseThrow(() -> new PlaceException(PlaceExceptionType.NO_EXIST));
placeStatistics.plusLike();
}

public void subtractLike(final SubtractLikeCommand subtractLikeCommand) {
final Long placeId = subtractLikeCommand.placeId();

final PlaceStatistics placeStatistics = placeStatisticsRepository.findByPlaceId(placeId)
final PlaceStatistics placeStatistics = placeStatisticsRepository.findByPlaceIdForUpdate(placeId)
.orElseThrow(() -> new PlaceStatisticsException(PlaceStatisticsExceptionType.NOT_FOUND));

placeStatistics.subtractLike();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ public class PlaceStatistics extends BaseEntity {
private Long likeCount;

protected PlaceStatistics() {

}

public PlaceStatistics(final Place place,
Expand All @@ -48,7 +47,7 @@ public void plusLike() {
}

public void subtractLike() {
if(isDefaultValue()) {
if (isDefaultValue()) {
return;
}
likeCount--;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
package com.now.naaga.place.repository;

import com.now.naaga.place.domain.PlaceStatistics;
import jakarta.persistence.LockModeType;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface PlaceStatisticsRepository extends JpaRepository<PlaceStatistics, Long> {

Optional<PlaceStatistics> findByPlaceId(Long placeId);
Optional<PlaceStatistics> findByPlaceId(final Long placeId);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT ps FROM PlaceStatistics ps WHERE ps.place.id = :placeId")
Optional<PlaceStatistics> findByPlaceIdForUpdate(@Param("placeId") final Long placeId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.SoftAssertions.assertSoftly;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;

Expand All @@ -19,7 +20,11 @@
import com.now.naaga.place.domain.Place;
import com.now.naaga.place.domain.PlaceStatistics;
import com.now.naaga.player.domain.Player;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down Expand Up @@ -380,4 +385,95 @@ class PlaceLikeServiceTest extends ServiceTest {
// then
assertThat(actual).isEqualTo(MyPlaceLikeType.NONE);
}

@Test
void 동시다발적인_좋아요_등록_요청으로부터_정확한_좋아요_수_집계를_처리한다() throws InterruptedException {
// given
final Place place = placeBuilder.init()
.build();

final Player registeredPlayer = place.getRegisteredPlayer();

final PlaceStatistics placeStatistics = placeStatisticsBuilder.init()
.place(place)
.build();

final int threadCnt = 20;
final ExecutorService executorService = Executors.newFixedThreadPool(threadCnt);
final CountDownLatch countDownLatch = new CountDownLatch(threadCnt);

for (int i = 0; i < threadCnt; i++) {
playerBuilder.init()
.build();
}

// when
final List<Player> players = playerRepository.findAll();
for (final Player player : players) {
if (player.equals(registeredPlayer)) {
continue;
}
executorService.submit(() -> {
final ApplyLikeCommand command = new ApplyLikeCommand(player.getId(), place.getId(), PlaceLikeType.LIKE);
placeLikeService.applyLike(command);
countDownLatch.countDown();
});
}
countDownLatch.await();

// then
final List<PlaceLike> actualPlaceLikes = placeLikeRepository.findAll();
final PlaceStatistics actualPlaceStatistics = placeStatisticsRepository.findById(placeStatistics.getId()).get();
assertAll(() -> assertThat(actualPlaceLikes).hasSize(threadCnt),
() -> assertThat(actualPlaceStatistics.getLikeCount()).isEqualTo(threadCnt));
}

@Test
void 동시다발적인_좋아요_삭제_요청으로부터_정확한_좋아요_수_집계를_처리한다() throws InterruptedException {
// given
final int threadCnt = 20;

final Place place = placeBuilder.init()
.build();

final Player registeredPlayer = place.getRegisteredPlayer();

final PlaceStatistics placeStatistics = placeStatisticsBuilder.init()
.place(place)
.likeCount((long) threadCnt)
.build();

final ExecutorService executorService = Executors.newFixedThreadPool(threadCnt);
final CountDownLatch countDownLatch = new CountDownLatch(threadCnt);

for (int i = 0; i < threadCnt; i++) {
final Player player = playerBuilder.init()
.build();
placeLikeBuilder.init()
.placeLikeType(PlaceLikeType.LIKE)
.place(place)
.player(player)
.build();
}

// when
final List<Player> players = playerRepository.findAll();
for (final Player player : players) {
if (player.equals(registeredPlayer)) {
continue;
}
executorService.submit(() -> {
final CancelLikeCommand command = new CancelLikeCommand(player.getId(), place.getId());
placeLikeService.cancelLike(command);
countDownLatch.countDown();
});
}
countDownLatch.await();

// then
final List<PlaceLike> actualPlaceLikes = placeLikeRepository.findAll();
final PlaceStatistics actualPlaceStatistics = placeStatisticsRepository.findById(placeStatistics.getId()).get();
assertAll(() -> assertThat(actualPlaceLikes).hasSize(0),
() -> assertThat(actualPlaceStatistics.getLikeCount()).isEqualTo(0));
}
}

0 comments on commit fd793a1

Please sign in to comment.