Skip to content
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 @@ -17,8 +17,6 @@
import swyp.team5.greening.post.domain.repository.PostRepository;
import swyp.team5.greening.post.exception.PostExceptionMessage;

//todo: 댓글 추가나 삭제를 통해 발생하는 댓글 수 증감 -> 이벤트 처리
//todo: 동시성 문제 처리
@Service
@RequiredArgsConstructor
public class CommentCommandService {
Expand All @@ -34,7 +32,7 @@ public SaveCommentResponseDto saveComment(
) {
//게시물 존재 조회
Post post = postRepository
.findByIdAndState(requestDto.postId(), PostState.IN_PROGRESS)
.findByIdAndStateWithLock(requestDto.postId(), PostState.IN_PROGRESS)
.orElseThrow(() ->
new GreeningGlobalException(PostExceptionMessage.NOT_FOUND_POST));

Expand Down Expand Up @@ -82,7 +80,7 @@ public void deleteComment(
CommentExceptionMessage.NOT_FOUND_COMMENT));

//게시물 존재 조회
Post post = postRepository.findByIdAndState(comment.getPostId(), PostState.IN_PROGRESS)
Post post = postRepository.findByIdAndStateWithLock(comment.getPostId(), PostState.IN_PROGRESS)
.orElseThrow(() ->
new GreeningGlobalException(PostExceptionMessage.NOT_FOUND_POST));

Expand All @@ -91,11 +89,11 @@ public void deleteComment(
throw new GreeningGlobalException(CommentExceptionMessage.BAD_REQUEST_COMMENT_WRITER);
}

//게시물 댓글 수 감소
post.decreaseCommentCount();

//댓글 삭제
commentRepository.deleteById(requestDto.commentId());

//게시물 댓글 수 감소
post.decreaseCommentCount();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ public interface PostRepository {

Optional<Post> findByIdAndState(Long postId, PostState state);

Optional<Post> findByIdAndStateWithLock(Long postId, PostState state);

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
package swyp.team5.greening.post.infrastructure;

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;
import swyp.team5.greening.post.domain.entity.Post;
import swyp.team5.greening.post.domain.entity.PostState;
import swyp.team5.greening.post.domain.repository.PostRepository;

public interface PostJpaRepository extends JpaRepository<Post, Long>, PostRepository {

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("""
SELECT post
FROM Post post
WHERE post.id = :postId
AND post.state = :state
""")
Optional<Post> findByIdAndStateWithLock(
@Param("postId") Long postId,
@Param("state") PostState state
);

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
import swyp.team5.greening.postLike.domain.repository.LikeRepository;
import swyp.team5.greening.postLike.dto.PostLikeResponseDto;

//todo: 게시글 좋아요 수 상태 변화 -> 이벤트 처리
//todo: 동시성 문제 처리
@Service
@RequiredArgsConstructor
public class PostLikeCommandService {
Expand All @@ -26,7 +24,7 @@ public class PostLikeCommandService {
@Transactional
public PostLikeResponseDto likeOrCancel(Long userId, Long postId) {
//게시글 조회
Post post = postRepository.findByIdAndState(postId, PostState.IN_PROGRESS)
Post post = postRepository.findByIdAndStateWithLock(postId, PostState.IN_PROGRESS)
.orElseThrow(
() -> new GreeningGlobalException(PostExceptionMessage.NOT_FOUND_POST));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class saveCommentTest {
@DisplayName("사용자는 게시물에 댓글을 작성할 수 있다. 이 때 게시물의 댓글 수가 증가한다.")
void testCase1() {
//given
given(postRepository.findByIdAndState(postId, PostState.IN_PROGRESS)).willReturn(
given(postRepository.findByIdAndStateWithLock(postId, PostState.IN_PROGRESS)).willReturn(
Optional.of(postEntity));
given(commentRepository.save(any(Comment.class))).willReturn(commentEntity);
ReflectionTestUtils.setField(postEntity, "id", postId);
Expand All @@ -82,7 +82,7 @@ void testCase1() {
userId, new SaveCommentRequestDto(postId, comment));

//then
verify(postRepository).findByIdAndState(postId, PostState.IN_PROGRESS);
verify(postRepository).findByIdAndStateWithLock(postId, PostState.IN_PROGRESS);
verify(commentRepository).save(any(Comment.class));
assertThat(responseDto.id()).isEqualTo(commentEntity.getId());
assertThat(postEntity.getCommentCount()).isEqualTo(1L);
Expand All @@ -92,7 +92,7 @@ void testCase1() {
@DisplayName("게시물이 존재하지 않을 경우, 예외가 발생한다.")
void testCase2() {
//given
given(postRepository.findByIdAndState(postId, PostState.IN_PROGRESS)).willReturn(
given(postRepository.findByIdAndStateWithLock(postId, PostState.IN_PROGRESS)).willReturn(
Optional.empty());

//when
Expand Down Expand Up @@ -170,7 +170,7 @@ void testCase2() {
@DisplayName("댓글 삭제할 수 있다. 이 때 게시글의 댓글 수는 감소한다.")
void testCase3() {
//given
given(postRepository.findByIdAndState(postId, PostState.IN_PROGRESS)).willReturn(
given(postRepository.findByIdAndStateWithLock(postId, PostState.IN_PROGRESS)).willReturn(
Optional.of(postEntity));
given(commentRepository.findById(commentId)).willReturn(Optional.of(testComment));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package swyp.team5.greening.comment.service;

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

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.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import swyp.team5.greening.comment.domain.repository.CommentRepository;
import swyp.team5.greening.comment.dto.request.SaveCommentRequestDto;
import swyp.team5.greening.post.domain.entity.Post;
import swyp.team5.greening.post.domain.entity.PostState;
import swyp.team5.greening.post.domain.repository.PostRepository;
import swyp.team5.greening.support.TestContainerSupport;

@SpringBootTest
public class PostCommentCountConcurrencyTest extends TestContainerSupport {

@Autowired
private CommentCommandService commentCommandService;

@Autowired
private CommentRepository commentRepository;

@Autowired
private PostRepository postRepository;

@BeforeEach
void init() {
commentRepository.deleteAll();
postRepository.deleteAll();
}

@Nested
@DisplayName("게시글 1개가 존재한다.")
class TestCase1 {

Post post;

@BeforeEach
void init() {
post = Post.builder()
.title("제목")
.commentCount(0L)
.likeCount(0L)
.state(PostState.IN_PROGRESS)
.categoryId(1L)
.userId(1L)
.build();
postRepository.save(post);
}

@Test
@DisplayName("동시에 20명의 유저가 게시글의 댓글을 작성한다면, 댓글 수는 20이다.")
void saveCommentConcurrencyTest() throws InterruptedException {
//given
ExecutorService executorService = Executors.newCachedThreadPool();
CountDownLatch countDownLatch = new CountDownLatch(20);

//when
for (long i = 0; i < 20; i++) {
final long userId = i;
executorService.submit(() -> {
commentCommandService.saveComment(userId,
new SaveCommentRequestDto(
post.getId(), "댓글" + userId
));

countDownLatch.countDown();
});
}

countDownLatch.await();

Post result = postRepository.findByIdAndState(post.getId(), PostState.IN_PROGRESS)
.orElseThrow();

assertThat(result.getCommentCount()).isEqualTo(20L);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class TestCase1 {
@DisplayName("게시글을 좋아요 한 상태라면, 좋아요가 취소되며 좋아요 수가 감소한다.")
void likeCancelTest() {
//given
given(postRepository.findByIdAndState(postId, PostState.IN_PROGRESS)).willReturn(
given(postRepository.findByIdAndStateWithLock(postId, PostState.IN_PROGRESS)).willReturn(
Optional.of(post));
ReflectionTestUtils.setField(post, "id", postId);
given(likeRepository.findByUserIdAndPostId(userId, postId)).willReturn(
Expand All @@ -71,7 +71,7 @@ void likeCancelTest() {
@DisplayName("게시글을 좋아요 하지 않은 상태라면, 좋아요 처리되며 좋아요 수가 증가한ㄷ.")
void likeTest() {
//given
given(postRepository.findByIdAndState(postId, PostState.IN_PROGRESS)).willReturn(
given(postRepository.findByIdAndStateWithLock(postId, PostState.IN_PROGRESS)).willReturn(
Optional.of(post));
ReflectionTestUtils.setField(post, "id", postId);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package swyp.team5.greening.postLike.service;

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

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.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import swyp.team5.greening.post.domain.entity.Post;
import swyp.team5.greening.post.domain.entity.PostState;
import swyp.team5.greening.post.domain.repository.PostRepository;
import swyp.team5.greening.postLike.domain.repository.LikeRepository;
import swyp.team5.greening.support.TestContainerSupport;

@SpringBootTest
@DisplayName("게시글 좋아요 동시성 테스트")
class PostLikeCountConcurrencyTest extends TestContainerSupport {

@Autowired
private PostLikeCommandService postLikeCommandService;

@Autowired
private PostRepository postRepository;

@Autowired
private LikeRepository likeRepository;

@BeforeEach
void init() {
postRepository.deleteAll();
likeRepository.deleteAll();
}

@Nested
@DisplayName("게시글 1개가 존재한다.")
class TestCase1 {

Post post;

@BeforeEach
void init() {
post = Post.builder()
.title("제목")
.commentCount(0L)
.likeCount(0L)
.state(PostState.IN_PROGRESS)
.categoryId(1L)
.userId(1L)
.build();
postRepository.save(post);
}

@Test
@DisplayName("동시에 20명의 유저가 게시글을 좋아요 할 경우, 좋아요 수가 알맞게 증가한다.")
void likeConcurrencyTest() throws InterruptedException {
//given
ExecutorService executorService = Executors.newCachedThreadPool();
CountDownLatch countDownLatch = new CountDownLatch(20);

//when
for (long i = 0; i < 20; i++) {
final long userId = i;
executorService.submit(() -> {
postLikeCommandService.likeOrCancel(userId, post.getId());
countDownLatch.countDown();
});
}

countDownLatch.await();

Post result = postRepository.findByIdAndState(post.getId(), PostState.IN_PROGRESS)
.orElseThrow();

//then
assertThat(result.getLikeCount()).isEqualTo(20L);

executorService.shutdown();
}

@Test
@DisplayName("한 명의 유저가 해당 기능을 30번 호출할 경우, 좋아요 수는 0이다.")
void likeOrCancelConcurrencyTest() throws InterruptedException {
//given
ExecutorService executorService = Executors.newCachedThreadPool();
CountDownLatch countDownLatch = new CountDownLatch(30);

//when
for (int i = 0; i < 30; i++) {
executorService.submit(() -> {
postLikeCommandService.likeOrCancel(1L, post.getId());
countDownLatch.countDown();
});
}

countDownLatch.await();

Post result = postRepository.findByIdAndState(post.getId(), PostState.IN_PROGRESS)
.orElseThrow();

//then
assertThat(result.getLikeCount()).isEqualTo(0L);

}
}
}
Loading