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/AnswerRepository.java b/src/main/java/com/dnd/gongmuin/answer/repository/AnswerRepository.java index e170f249..7577930c 100644 --- a/src/main/java/com/dnd/gongmuin/answer/repository/AnswerRepository.java +++ b/src/main/java/com/dnd/gongmuin/answer/repository/AnswerRepository.java @@ -1,6 +1,7 @@ package com.dnd.gongmuin.answer.repository; import java.util.List; +import java.util.Optional; import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; @@ -21,4 +22,8 @@ public interface AnswerRepository extends JpaRepository { @Modifying(flushAutomatically = true, clearAutomatically = true) @Query("UPDATE Answer a SET a.member = :member WHERE a.member.id = :memberId") void updateAnswersMember(Long memberId, Member member); + + @Query("select a from Answer a " + + "join fetch a.member where a.id = :answerId") + Optional findByIdWithMember(Long answerId); } \ No newline at end of file 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..027c93c9 100644 --- a/src/main/java/com/dnd/gongmuin/answer/service/AnswerService.java +++ b/src/main/java/com/dnd/gongmuin/answer/service/AnswerService.java @@ -47,7 +47,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); @@ -73,8 +73,8 @@ public AnswerDetailResponse chooseAnswer( Long answerId, Member member ) { - Answer answer = getAnswerById(answerId); - QuestionPost questionPost = findQuestionPostById(answer.getQuestionPostId()); + Answer answer = getAnswerWithMember(answerId); + QuestionPost questionPost = getQuestionPostWithMember(answer.getQuestionPostId()); validateIfQuestioner(member, questionPost); chooseAnswer(questionPost, answer); @@ -99,13 +99,18 @@ private void validateIfQuestionPostExists(Long questionPostId) { } } - private Answer getAnswerById(Long answerId) { - return answerRepository.findById(answerId) + private Answer getAnswerWithMember(Long answerId) { + return answerRepository.findByIdWithMember(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 getQuestionPostWithMember(Long questionPostId) { + return questionPostRepository.findByIdWithMember(questionPostId) + .orElseThrow(() -> new NotFoundException(QuestionPostErrorCode.NOT_FOUND_QUESTION_POST)); + } } \ No newline at end of file diff --git a/src/main/java/com/dnd/gongmuin/chat_inquiry/dto/ChatInquiryResponse.java b/src/main/java/com/dnd/gongmuin/chat_inquiry/dto/ChatInquiryResponse.java index 384bc5f8..468315c3 100644 --- a/src/main/java/com/dnd/gongmuin/chat_inquiry/dto/ChatInquiryResponse.java +++ b/src/main/java/com/dnd/gongmuin/chat_inquiry/dto/ChatInquiryResponse.java @@ -1,10 +1,8 @@ package com.dnd.gongmuin.chat_inquiry.dto; -import com.dnd.gongmuin.chat_inquiry.domain.InquiryStatus; -import com.dnd.gongmuin.member.domain.JobGroup; -import com.dnd.gongmuin.member.dto.response.MemberInfo; import com.dnd.gongmuin.chat_inquiry.domain.ChatInquiry; import com.dnd.gongmuin.member.domain.Member; +import com.dnd.gongmuin.member.dto.response.MemberInfo; import com.querydsl.core.annotations.QueryProjection; public record ChatInquiryResponse( diff --git a/src/main/java/com/dnd/gongmuin/question_post/repository/QuestionPostRepository.java b/src/main/java/com/dnd/gongmuin/question_post/repository/QuestionPostRepository.java index 39d750fb..5fc33635 100644 --- a/src/main/java/com/dnd/gongmuin/question_post/repository/QuestionPostRepository.java +++ b/src/main/java/com/dnd/gongmuin/question_post/repository/QuestionPostRepository.java @@ -1,6 +1,7 @@ package com.dnd.gongmuin.question_post.repository; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; @@ -16,7 +17,11 @@ public interface QuestionPostRepository extends JpaRepository findAllByMember(Member member); + @Query("select q from QuestionPost q " + + "join fetch q.member where q.id = :questionPostId") + Optional findByIdWithMember(Long questionPostId); + @Modifying(flushAutomatically = true, clearAutomatically = true) @Query("UPDATE QuestionPost q SET q.member = :member WHERE q.member.id = :memberId") - public void updateQuestionPostsMember(Long memberId, Member member); + void updateQuestionPostsMember(Long memberId, Member member); } \ 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/service/AnswerServiceTest.java b/src/test/java/com/dnd/gongmuin/answer/service/AnswerServiceTest.java index a1e18c38..d8395ce8 100644 --- a/src/test/java/com/dnd/gongmuin/answer/service/AnswerServiceTest.java +++ b/src/test/java/com/dnd/gongmuin/answer/service/AnswerServiceTest.java @@ -4,10 +4,15 @@ 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.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -118,9 +123,9 @@ void chooseAnswer() { QuestionPost questionPost = QuestionPostFixture.questionPost(questionPostId, member); Answer answer = AnswerFixture.answer(1L, questionPostId); - given(answerRepository.findById(answer.getId())) + given(answerRepository.findByIdWithMember(answer.getId())) .willReturn(Optional.of(answer)); - given(questionPostRepository.findById(questionPost.getId())) + given(questionPostRepository.findByIdWithMember(questionPost.getId())) .willReturn(Optional.of(questionPost)); //when @@ -140,9 +145,9 @@ void chooseAnswer_fail() { ReflectionTestUtils.setField(questionPost, "reward", member.getCredit() + 1); Answer answer = AnswerFixture.answer(1L, questionPostId); - given(answerRepository.findById(answer.getId())) + given(answerRepository.findByIdWithMember(answer.getId())) .willReturn(Optional.of(answer)); - given(questionPostRepository.findById(questionPost.getId())) + given(questionPostRepository.findByIdWithMember(questionPost.getId())) .willReturn(Optional.of(questionPost)); //when & then @@ -162,9 +167,9 @@ void chooseAnswer_fail2() { QuestionPost questionPost = QuestionPostFixture.questionPost(questionPostId, questioner); Answer answer = AnswerFixture.answer(1L, questionPostId); - given(answerRepository.findById(answer.getId())) + given(answerRepository.findByIdWithMember(answer.getId())) .willReturn(Optional.of(answer)); - given(questionPostRepository.findById(questionPost.getId())) + given(questionPostRepository.findByIdWithMember(questionPost.getId())) .willReturn(Optional.of(questionPost)); //when & then @@ -172,4 +177,56 @@ void chooseAnswer_fail2() { .isInstanceOf(ValidationException.class) .hasMessageContaining(QuestionPostErrorCode.NOT_AUTHORIZED.getMessage()); } + + @Disabled + @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(answerRepository.findByIdWithMember(i)).willReturn(Optional.of(answer1)); + given(questionPostRepository.findByIdWithMember(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