diff --git a/src/main/java/com/dnd/gongmuin/answer/controller/AnswerController.java b/src/main/java/com/dnd/gongmuin/answer/controller/AnswerController.java index ddd2913f..799b5986 100644 --- a/src/main/java/com/dnd/gongmuin/answer/controller/AnswerController.java +++ b/src/main/java/com/dnd/gongmuin/answer/controller/AnswerController.java @@ -52,7 +52,7 @@ public ResponseEntity> getAnswersByQuestionPo @ApiResponse(useReturnTypeSchema = true) @PostMapping("/api/question-posts/answers/{answerId}") public ResponseEntity getAnswersByQuestionPostId( - @PathVariable Long answerId, + @PathVariable("answerId") Long answerId, @AuthenticationPrincipal Member member ) { AnswerDetailResponse response = answerService.chooseAnswer(answerId, member); diff --git a/src/main/java/com/dnd/gongmuin/answer/repository/AnswerSimpleQueryRepository.java b/src/main/java/com/dnd/gongmuin/answer/repository/AnswerSimpleQueryRepository.java new file mode 100644 index 00000000..53bc7419 --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/answer/repository/AnswerSimpleQueryRepository.java @@ -0,0 +1,28 @@ +package com.dnd.gongmuin.answer.repository; + +import java.util.Optional; + +import org.springframework.stereotype.Repository; + +import com.dnd.gongmuin.answer.domain.Answer; + +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class AnswerSimpleQueryRepository { + + private final EntityManager em; + + public Optional findAnswerById(Long answerId) { + Answer answer = em.createQuery( + "select a from Answer a" + + " join fetch a.member m" + + " where a.id = :answerId", Answer.class + ) + .setParameter("answerId", answerId) + .getSingleResult(); + return Optional.of(answer); + } +} diff --git a/src/main/java/com/dnd/gongmuin/answer/service/AnswerService.java b/src/main/java/com/dnd/gongmuin/answer/service/AnswerService.java index 81b7ef7e..4774da6d 100644 --- a/src/main/java/com/dnd/gongmuin/answer/service/AnswerService.java +++ b/src/main/java/com/dnd/gongmuin/answer/service/AnswerService.java @@ -13,6 +13,7 @@ import com.dnd.gongmuin.answer.dto.RegisterAnswerRequest; import com.dnd.gongmuin.answer.exception.AnswerErrorCode; import com.dnd.gongmuin.answer.repository.AnswerRepository; +import com.dnd.gongmuin.answer.repository.AnswerSimpleQueryRepository; import com.dnd.gongmuin.common.dto.PageMapper; import com.dnd.gongmuin.common.dto.PageResponse; import com.dnd.gongmuin.common.exception.runtime.NotFoundException; @@ -23,6 +24,7 @@ import com.dnd.gongmuin.question_post.domain.QuestionPost; import com.dnd.gongmuin.question_post.exception.QuestionPostErrorCode; import com.dnd.gongmuin.question_post.repository.QuestionPostRepository; +import com.dnd.gongmuin.question_post.repository.QuestionPostSimpleQueryRepository; import lombok.RequiredArgsConstructor; @@ -34,6 +36,8 @@ public class AnswerService { private final AnswerRepository answerRepository; private final CreditHistoryService creditHistoryService; private final ApplicationEventPublisher eventPublisher; + private final QuestionPostSimpleQueryRepository questionPostSimpleQueryRepository; + private final AnswerSimpleQueryRepository answerSimpleQueryRepository; private static void validateIfQuestioner(Member member, QuestionPost questionPost) { if (!questionPost.isQuestioner(member.getId())) { @@ -47,7 +51,7 @@ public AnswerDetailResponse registerAnswer( RegisterAnswerRequest request, Member member ) { - QuestionPost questionPost = findQuestionPostById(questionPostId); + QuestionPost questionPost = getQuestionPostById(questionPostId); Answer answer = AnswerMapper.toAnswer(questionPostId, questionPost.isQuestioner(member.getId()), request, member); Answer savedAnswer = answerRepository.save(answer); @@ -100,12 +104,17 @@ private void validateIfQuestionPostExists(Long questionPostId) { } private Answer getAnswerById(Long answerId) { - return answerRepository.findById(answerId) + return answerSimpleQueryRepository.findAnswerById(answerId) .orElseThrow(() -> new NotFoundException(AnswerErrorCode.NOT_FOUND_ANSWER)); } - private QuestionPost findQuestionPostById(Long questionPostId) { + private QuestionPost getQuestionPostById(Long questionPostId) { return questionPostRepository.findById(questionPostId) .orElseThrow(() -> new NotFoundException(QuestionPostErrorCode.NOT_FOUND_QUESTION_POST)); } + + private QuestionPost findQuestionPostById(Long questionPostId) { + return questionPostSimpleQueryRepository.findQuestionPostById(questionPostId) + .orElseThrow(() -> new NotFoundException(QuestionPostErrorCode.NOT_FOUND_QUESTION_POST)); + } } \ No newline at end of file diff --git a/src/main/java/com/dnd/gongmuin/question_post/repository/QuestionPostSimpleQueryRepository.java b/src/main/java/com/dnd/gongmuin/question_post/repository/QuestionPostSimpleQueryRepository.java new file mode 100644 index 00000000..01120751 --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/question_post/repository/QuestionPostSimpleQueryRepository.java @@ -0,0 +1,28 @@ +package com.dnd.gongmuin.question_post.repository; + +import java.util.Optional; + +import org.springframework.stereotype.Repository; + +import com.dnd.gongmuin.question_post.domain.QuestionPost; + +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class QuestionPostSimpleQueryRepository { + + private final EntityManager em; + + public Optional findQuestionPostById(Long questionPostId) { + QuestionPost questionPost = em.createQuery( + "select q from QuestionPost q" + + " join fetch q.member m" + + " where q.id = :questionPostId", QuestionPost.class + ) + .setParameter("questionPostId", questionPostId) + .getSingleResult(); + return Optional.of(questionPost); + } +} diff --git a/src/test/java/com/dnd/gongmuin/answer/controller/AnswerControllerTest.java b/src/test/java/com/dnd/gongmuin/answer/controller/AnswerControllerTest.java index d8888bb9..9cccbc84 100644 --- a/src/test/java/com/dnd/gongmuin/answer/controller/AnswerControllerTest.java +++ b/src/test/java/com/dnd/gongmuin/answer/controller/AnswerControllerTest.java @@ -125,6 +125,7 @@ void chooseAnswer() throws Exception { = questionPostRepository.save(QuestionPostFixture.questionPost(loginMember)); Member answerer = memberRepository.save(MemberFixture.member2()); Answer answer = answerRepository.save(AnswerFixture.answer(questionPost.getId(), answerer)); + long startTime = System.currentTimeMillis(); mockMvc.perform(post("/api/question-posts/answers/{answerId}", answer.getId()) .cookie(accessToken) @@ -137,5 +138,7 @@ void chooseAnswer() throws Exception { .andExpect(jsonPath("$.memberInfo.nickname").value(answerer.getNickname())) .andExpect(jsonPath("$.memberInfo.memberJobGroup").value(answerer.getJobGroup().getLabel()) ); + long endTime = System.currentTimeMillis(); + System.out.println("Execution time: " + (endTime - startTime) + " ms"); } } \ No newline at end of file diff --git a/src/test/java/com/dnd/gongmuin/answer/service/AnswerServiceTest.java b/src/test/java/com/dnd/gongmuin/answer/service/AnswerServiceTest.java index a1e18c38..bdc90b21 100644 --- a/src/test/java/com/dnd/gongmuin/answer/service/AnswerServiceTest.java +++ b/src/test/java/com/dnd/gongmuin/answer/service/AnswerServiceTest.java @@ -4,8 +4,12 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.BDDMockito.*; +import java.util.ArrayList; 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.Assertions; import org.junit.jupiter.api.DisplayName; @@ -24,6 +28,7 @@ import com.dnd.gongmuin.answer.dto.AnswerDetailResponse; import com.dnd.gongmuin.answer.dto.RegisterAnswerRequest; import com.dnd.gongmuin.answer.repository.AnswerRepository; +import com.dnd.gongmuin.answer.repository.AnswerSimpleQueryRepository; import com.dnd.gongmuin.common.dto.PageResponse; import com.dnd.gongmuin.common.exception.runtime.ValidationException; import com.dnd.gongmuin.common.fixture.AnswerFixture; @@ -36,6 +41,7 @@ import com.dnd.gongmuin.question_post.domain.QuestionPost; import com.dnd.gongmuin.question_post.exception.QuestionPostErrorCode; import com.dnd.gongmuin.question_post.repository.QuestionPostRepository; +import com.dnd.gongmuin.question_post.repository.QuestionPostSimpleQueryRepository; @DisplayName("[AnswerService 테스트]") @ExtendWith(MockitoExtension.class) @@ -46,9 +52,15 @@ class AnswerServiceTest { @Mock private QuestionPostRepository questionPostRepository; + @Mock + private QuestionPostSimpleQueryRepository questionPostSimpleQueryRepository; + @Mock private AnswerRepository answerRepository; + @Mock + private AnswerSimpleQueryRepository answerSimpleQueryRepository; + @Mock private CreditHistoryService creditHistoryService; @@ -118,9 +130,9 @@ void chooseAnswer() { QuestionPost questionPost = QuestionPostFixture.questionPost(questionPostId, member); Answer answer = AnswerFixture.answer(1L, questionPostId); - given(answerRepository.findById(answer.getId())) + given(answerSimpleQueryRepository.findAnswerById(answer.getId())) .willReturn(Optional.of(answer)); - given(questionPostRepository.findById(questionPost.getId())) + given(questionPostSimpleQueryRepository.findQuestionPostById(questionPost.getId())) .willReturn(Optional.of(questionPost)); //when @@ -140,9 +152,9 @@ void chooseAnswer_fail() { ReflectionTestUtils.setField(questionPost, "reward", member.getCredit() + 1); Answer answer = AnswerFixture.answer(1L, questionPostId); - given(answerRepository.findById(answer.getId())) + given(answerSimpleQueryRepository.findAnswerById(answer.getId())) .willReturn(Optional.of(answer)); - given(questionPostRepository.findById(questionPost.getId())) + given(questionPostSimpleQueryRepository.findQuestionPostById(questionPost.getId())) .willReturn(Optional.of(questionPost)); //when & then @@ -162,9 +174,9 @@ void chooseAnswer_fail2() { QuestionPost questionPost = QuestionPostFixture.questionPost(questionPostId, questioner); Answer answer = AnswerFixture.answer(1L, questionPostId); - given(answerRepository.findById(answer.getId())) + given(answerSimpleQueryRepository.findAnswerById(answer.getId())) .willReturn(Optional.of(answer)); - given(questionPostRepository.findById(questionPost.getId())) + given(questionPostSimpleQueryRepository.findQuestionPostById(questionPost.getId())) .willReturn(Optional.of(questionPost)); //when & then @@ -172,4 +184,55 @@ void chooseAnswer_fail2() { .isInstanceOf(ValidationException.class) .hasMessageContaining(QuestionPostErrorCode.NOT_AUTHORIZED.getMessage()); } + + @DisplayName("[동시에 10_000개의 채택이 일어나 크레딧을 입금 받는다.]") + @Test + void creditHistoryWithOneHundred() throws Exception { + // given + final long threadCount = 10_000L; + final int writerCredit = 10_000_000; + ExecutorService executorService = Executors.newFixedThreadPool(32); + CountDownLatch latch = new CountDownLatch((int)threadCount); + + Member writer = MemberFixture.member(1L); + Member answer = MemberFixture.member(2L); + ReflectionTestUtils.setField(writer, "credit", writerCredit); + ReflectionTestUtils.setField(answer, "credit", 0); + + List questionPosts = new ArrayList<>(); + List answers = new ArrayList<>(); + + for (long i = 1L; i <= threadCount; i++) { + QuestionPost questionPost = QuestionPostFixture.questionPost(i, writer); + + Answer answer1 = AnswerFixture.answer(questionPost.getId(), answer); + ReflectionTestUtils.setField(answer1, "id", i); + questionPosts.add(questionPost); + answers.add(answer1); + + given(answerSimpleQueryRepository.findAnswerById(i)).willReturn(Optional.of(answer1)); + given(questionPostSimpleQueryRepository.findQuestionPostById(questionPost.getId())) + .willReturn(Optional.of(questionPost)); + } + + // when + long startTime = System.currentTimeMillis(); + for (long i = 0L; i < threadCount; i++) { + final int index = (int)i; + executorService.submit(() -> { + try { + answerService.chooseAnswer(answers.get(index).getId(), writer); + } finally { + latch.countDown(); + } + }); + } + latch.await(); + + long endTime = System.currentTimeMillis(); + System.out.println("Execution time: " + (endTime - startTime) + " ms"); + + // then + assertEquals(answer.getCredit(), writerCredit); + } } \ No newline at end of file