diff --git a/src/main/java/greeny/backend/domain/community/entity/PostLike.java b/src/main/java/greeny/backend/domain/community/entity/PostLike.java index 918eac0..0db5a79 100644 --- a/src/main/java/greeny/backend/domain/community/entity/PostLike.java +++ b/src/main/java/greeny/backend/domain/community/entity/PostLike.java @@ -22,6 +22,4 @@ public class PostLike extends AuditEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "liker_id", nullable = false) private Member liker; - @Version - private int version; } diff --git a/src/main/java/greeny/backend/domain/community/repository/PostLikeRepository.java b/src/main/java/greeny/backend/domain/community/repository/PostLikeRepository.java index b876e85..063ec8d 100644 --- a/src/main/java/greeny/backend/domain/community/repository/PostLikeRepository.java +++ b/src/main/java/greeny/backend/domain/community/repository/PostLikeRepository.java @@ -1,6 +1,7 @@ package greeny.backend.domain.community.repository; import greeny.backend.domain.community.entity.PostLike; +import greeny.backend.domain.member.entity.Member; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Lock; @@ -8,5 +9,7 @@ import java.util.Optional; public interface PostLikeRepository extends JpaRepository { - Optional findByPostIdAndLikerId(Long postId, Long likerId); + @Lock(LockModeType.PESSIMISTIC_WRITE) + Optional findByPostIdAndLiker(Long postId, Member liker); + boolean existsByPostIdAndLikerId(Long postId, Long likerId); } diff --git a/src/main/java/greeny/backend/domain/community/service/PostLikeService.java b/src/main/java/greeny/backend/domain/community/service/PostLikeService.java index d49eef0..36e10e8 100644 --- a/src/main/java/greeny/backend/domain/community/service/PostLikeService.java +++ b/src/main/java/greeny/backend/domain/community/service/PostLikeService.java @@ -8,59 +8,58 @@ import greeny.backend.exception.situation.PostNotFoundException; import greeny.backend.exception.situation.SelfLikeNotAllowedException; import lombok.RequiredArgsConstructor; -import org.springframework.dao.OptimisticLockingFailureException; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; + import java.util.Optional; @Service @RequiredArgsConstructor +@Slf4j public class PostLikeService { private final PostLikeRepository postLikeRepository; private final PostRepository postRepository; @Transactional public void like(Long postId, Member liker) { - boolean retry; - do { - try { - retry = false; - Optional foundPostLike = findPostLike(postId, liker.getId()); - toggle(postId, liker, foundPostLike); - } - catch (OptimisticLockingFailureException e) { - retry = true; - } - } while (retry); + log.info("like"); + toggle(postId, liker, findPostLike(postId, liker)); + } + + public Optional findPostLike(Long postId, Member liker) { + log.info("find post like"); + return postLikeRepository.findByPostIdAndLiker(postId, liker); + } + + public void toggle(Long postId, Member liker, Optional postLike) { + log.info("toggle"); + if (postLike.isEmpty()) { + create(postId, liker); + return; + } + log.info("delete"); + delete(postLike.get()); } public void create(Long postId, Member liker) { + log.info("create"); Post post = getPost(postId); if (post.getWriter().getId().equals(liker.getId())) throw new SelfLikeNotAllowedException(); + log.info("save post like"); postLikeRepository.save(PostLike.builder() .post(post) .liker(liker) .build()); } - public Post getPost(Long postId) { - return postRepository.findByIdWithWriter(postId).orElseThrow(PostNotFoundException::new); - } - - public Optional findPostLike(Long postId, Long likerId) { - return postLikeRepository.findByPostIdAndLikerId(postId, likerId); - } - private void delete(PostLike postLike) { postLikeRepository.delete(postLike); } - private void toggle(Long postId, Member liker, Optional postLike) { - if (postLike.isEmpty()) { - create(postId, liker); - return; - } - delete(postLike.get()); + public Post getPost(Long postId) { + log.info("get post"); + return postRepository.findByIdWithWriter(postId).orElseThrow(PostNotFoundException::new); } } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 87630eb..6ee8b84 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -14,10 +14,10 @@ spring: # check-location: false # baseline-on-migration: true - sql: - init: - mode: always - data-locations: classpath:sql/data.sql +# sql: +# init: +# mode: always +# data-locations: classpath:sql/data.sql jpa: open-in-view: false @@ -32,7 +32,7 @@ spring: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl database: mysql database-platform: org.hibernate.dialect.MySQL8Dialect - defer-datasource-initialization: true +# defer-datasource-initialization: true logging: level: diff --git a/src/test/java/greeny/backend/service/PostLikeServiceTest.java b/src/test/java/greeny/backend/service/PostLikeServiceTest.java new file mode 100644 index 0000000..e697c04 --- /dev/null +++ b/src/test/java/greeny/backend/service/PostLikeServiceTest.java @@ -0,0 +1,77 @@ +package greeny.backend.service; + +import greeny.backend.domain.community.entity.Post; +import greeny.backend.domain.community.repository.PostLikeRepository; +import greeny.backend.domain.community.repository.PostRepository; +import greeny.backend.domain.community.service.PostLikeService; +import greeny.backend.domain.member.entity.Member; +import greeny.backend.domain.member.entity.Role; +import greeny.backend.domain.member.repository.MemberRepository; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Slf4j +class PostLikeServiceTest { + @Autowired + PostLikeService postLikeService; + @Autowired + MemberRepository memberRepository; + @Autowired + PostRepository postRepository; + @Autowired + PostLikeRepository postLikeRepository; + + @Test + void likeConcurrency() throws InterruptedException { + // Given + log.info("test start"); + Member savedWriter = memberRepository.save(createMember("asd123@naver.com")); + Member savedLiker = memberRepository.save(createMember("fgh123@naver.com")); + Post savedPost = postRepository.save(createPost(savedWriter)); + + // When + int numberOfThread = 3; + ExecutorService executorService = Executors.newFixedThreadPool(numberOfThread); + CountDownLatch countDownLatch = new CountDownLatch(numberOfThread); + for (int i = 0; i < numberOfThread; i++) { + executorService.submit(() -> { + log.info("Current thread"); + postLikeService.like(savedPost.getId(), savedLiker); + countDownLatch.countDown(); + }); + } + countDownLatch.await(); + + // Then + assertThat(postLikeRepository.existsByPostIdAndLikerId(savedPost.getId(), savedLiker.getId())).isTrue(); + memberRepository.deleteAll(); + postRepository.deleteAll(); + postLikeRepository.deleteAll(); + log.info("test end"); + } + + Member createMember(String email) { + return Member.builder() + .email(email) + .role(Role.ROLE_USER) + .build(); + } + + Post createPost(Member writer) { + return Post.builder() + .writer(writer) + .title("안녕") + .content("반가워!") + .hits(0) + .build(); + } +} \ No newline at end of file