Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[refactor#148] 답변 채택 API 성능 최적화 #155

Merged
merged 17 commits into from
Nov 23, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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 @@ -52,10 +52,21 @@ public ResponseEntity<PageResponse<AnswerDetailResponse>> getAnswersByQuestionPo
@ApiResponse(useReturnTypeSchema = true)
@PostMapping("/api/question-posts/answers/{answerId}")
public ResponseEntity<AnswerDetailResponse> getAnswersByQuestionPostId(
@PathVariable Long answerId,
@PathVariable("answerId") Long answerId,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정 감사합니다~

@AuthenticationPrincipal Member member
) {
AnswerDetailResponse response = answerService.chooseAnswer(answerId, member);
return ResponseEntity.ok(response);
}

@Operation(summary = "답변 채택 API", description = "질문자가 답변을 채택한다.")
@ApiResponse(useReturnTypeSchema = true)
@PostMapping("/api/v2/question-posts/answers/{answerId}")
public ResponseEntity<AnswerDetailResponse> getAnswersByQuestionPostIdV2(
@PathVariable("answerId") Long answerId,
@AuthenticationPrincipal Member member
) {
AnswerDetailResponse response = answerService.chooseAnswerV2(answerId, member);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -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<Answer> 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);
}
}
31 changes: 31 additions & 0 deletions src/main/java/com/dnd/gongmuin/answer/service/AnswerService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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())) {
Expand Down Expand Up @@ -85,6 +89,23 @@ public AnswerDetailResponse chooseAnswer(
return AnswerMapper.toAnswerDetailResponse(answer);
}

@Transactional
public AnswerDetailResponse chooseAnswerV2(
Long answerId,
Member member
) {
Answer answer = getAnswerByIdV2(answerId);
QuestionPost questionPost = findQuestionPostByIdV2(answer.getQuestionPostId());
validateIfQuestioner(member, questionPost);
chooseAnswer(questionPost, answer);

eventPublisher.publishEvent(new NotificationEvent(
CHOSEN, questionPost.getId(), member.getId(), answer.getMember()
));

return AnswerMapper.toAnswerDetailResponse(answer);
}

private void chooseAnswer(QuestionPost questionPost, Answer answer) {
questionPost.updateIsChosen(answer);
answer.getMember().increaseCredit(questionPost.getReward());
Expand All @@ -108,4 +129,14 @@ private QuestionPost findQuestionPostById(Long questionPostId) {
return questionPostRepository.findById(questionPostId)
.orElseThrow(() -> new NotFoundException(QuestionPostErrorCode.NOT_FOUND_QUESTION_POST));
}

private Answer getAnswerByIdV2(Long answerId) {
return answerSimpleQueryRepository.findAnswerById(answerId)
.orElseThrow(() -> new NotFoundException(AnswerErrorCode.NOT_FOUND_ANSWER));
}

private QuestionPost findQuestionPostByIdV2(Long questionPostId) {
return questionPostSimpleQueryRepository.findQuestionPostById(questionPostId)
.orElseThrow(() -> new NotFoundException(QuestionPostErrorCode.NOT_FOUND_QUESTION_POST));
}
}
Original file line number Diff line number Diff line change
@@ -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<QuestionPost> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -137,5 +138,31 @@ 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");
}

@DisplayName("[질문자는 답변을 채택할 수 있다.V2]")
@Test
void chooseAnswerV2() throws Exception {
QuestionPost questionPost
= 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/v2/question-posts/answers/{answerId}", answer.getId())
.cookie(accessToken)
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.content").value(answer.getContent()))
.andExpect(jsonPath("$.isChosen").value(true))
.andExpect(jsonPath("$.isQuestioner").value(false))
.andExpect(jsonPath("$.memberInfo.memberId").value(answerer.getId()))
.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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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)
Expand All @@ -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;

Expand Down Expand Up @@ -130,6 +142,27 @@ void chooseAnswer() {
Assertions.assertThat(response.isChosen()).isTrue();
}

@DisplayName("[답변을 채택할 수 있다.]")
@Test
void chooseAnswerV2() {
//given
Long questionPostId = 1L;
Member member = MemberFixture.member(1L);
QuestionPost questionPost = QuestionPostFixture.questionPost(questionPostId, member);
Answer answer = AnswerFixture.answer(1L, questionPostId);

given(answerSimpleQueryRepository.findAnswerById(answer.getId()))
.willReturn(Optional.of(answer));
given(questionPostSimpleQueryRepository.findQuestionPostById(questionPost.getId()))
.willReturn(Optional.of(questionPost));

//when
AnswerDetailResponse response = answerService.chooseAnswerV2(answer.getId(), member);

//then
Assertions.assertThat(response.isChosen()).isTrue();
}

@DisplayName("[크레딧이 부족하면 답변을 채택할 수 없다.]")
@Test
void chooseAnswer_fail() {
Expand Down Expand Up @@ -172,4 +205,54 @@ 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<QuestionPost> questionPosts = new ArrayList<>();
List<Answer> 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.findById(i)).willReturn(Optional.of(answer1));
given(questionPostRepository.findById(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);
}
}
Loading