From 1dd4523d135bee2b15f2bb5c2d3cb4d2c42a5812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=EC=98=81=ED=83=9C?= <56019823+dudxo@users.noreply.github.com> Date: Sat, 23 Nov 2024 20:29:14 +0900 Subject: [PATCH] =?UTF-8?q?[re=08factor#148]=20=EB=8B=B5=EB=B3=80=20?= =?UTF-8?q?=EC=B1=84=ED=83=9D=20API=20=EC=84=B1=EB=8A=A5=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94=20(#155)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [test] : 채택 시 크레딧 정합성 테스트 * [feat] : QuestionPostSimpleQueryRepo 생성 - fetch join을 활용한 한방 쿼리 추가 * [feat] : AnswerSimpleQueryRepo 생성 - fetch join을 활용한 한방 쿼리 추가 * [feat] : 답변채택 쿼리 개수 개선 v2 추가 * [feat] : 답변채택 쿼리 개수 개선 v2 API 추가 * [test] : 답변채택 쿼리 개수 개선 v2 단위테스트 추가 * [test] : 답변채택 쿼리 개수 개선 v2 통합테스트 추가 * [Refactor #151] JWT Subject 개인정보 제거 (#154) * [refactor] JWT 생성 로직 변경 - JWT 생성시 subject에 개인정보(이메일)이 아닌 PK값이 들어가도록 변경 - 검증 토큰을 이용한 인증 객체 생성 시 subject 이메일 -> PK 변경에 따라 PK로 회원 찾도록 변경 * [refactor] : 토큰 generate 메서드 파라미터 변경으로 인한 리팩토링 * [refactor] : 토큰 generate 메서드 파라미터 변경으로 인한 리팩토링 * [refactor] : 토큰 generate 메서드 파라미터 변경으로 인한 리팩토링 * [refactor #156] 채팅 요청 목록 API에서 채팅 파트너 조회 로직 리팩토링 (#157) * [feat] : 채팅 요청에 createdAt 필드 추가 * [feat] : 채팅 요청 테스트 픽스쳐에 createdAt 추가 * [feat] : 채팅 파트너 구하는 로직 DTO에 추가 * [refactor] : 채팅 파트너 구하는 로직 DTO로 이동 * [style] : 코드 리포멧팅 * [test] : createdAt 포함된 testFixture 사용 * [refactor] : V1, V2 통합 * [test] : V1, V2 통합에 따른 테스트 코드 삭제 * [feat #158] 채팅 요청 상세 조회 API (#159) * [feat] : 채팅 요청 상세 조회 응답 추가 * [feat] : 채팅 요청 상세 조회 비즈니스 로직 추가 * [feat] : 채팅 요청 상세 조회 비즈니스 로직 테스트 * [feat] : 채팅 요청 상세 조회 API 메서드 추가 * [test] : 채팅 요청 상세 조회 API 메서드 테스트 * [feat] : 채팅 요청 API pk 필드명 수정 * [rename] : memberInfo DTO 위치 이동 * [remove] : 불필요한 예외 로직 처리 삭제 * [refactor] : 채팅 파트너 구하는 로직 도메인으로 이동 * [refactor] : 채팅 파트너 구하는 로직 mapper가 아닌 서비스 내에서 호출 * [style] : 코드 리포멧팅 * [style] : 줄바꿈 취소 * [test] : V1, V2 통합에 따른 테스트 코드 수정 * [feat] : QuestionPost 단건조회 메서드 추가 * [test]: 정합성 테스트 disabled 처리 * [refactor]: fetch join 함수 repository로 이동 --------- Co-authored-by: Son Gahyun <77109954+hyun2371@users.noreply.github.com> Co-authored-by: hs12 --- .../answer/controller/AnswerController.java | 2 +- .../answer/repository/AnswerRepository.java | 5 ++ .../AnswerSimpleQueryRepository.java | 28 ++++++++ .../answer/service/AnswerService.java | 17 +++-- .../chat_inquiry/dto/ChatInquiryResponse.java | 4 +- .../repository/QuestionPostRepository.java | 7 +- .../QuestionPostSimpleQueryRepository.java | 28 ++++++++ .../answer/service/AnswerServiceTest.java | 69 +++++++++++++++++-- 8 files changed, 143 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/dnd/gongmuin/answer/repository/AnswerSimpleQueryRepository.java create mode 100644 src/main/java/com/dnd/gongmuin/question_post/repository/QuestionPostSimpleQueryRepository.java 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