Skip to content

Commit 34e18f5

Browse files
authored
Merge pull request #69 from WiSoft-PrePair/refactor/n-plus-one-query
refactor: AllAnalysisCompletedHandler 메서드 책임 분리 및 N+1 쿼리 개선
2 parents 47c194f + 54e4d9c commit 34e18f5

3 files changed

Lines changed: 138 additions & 66 deletions

File tree

Lines changed: 123 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
11
package io.wisoft.prepair.prepair_api.interview.answer.event;
22

33
import io.wisoft.prepair.prepair_api.interview.answer.dto.CombinedFeedbackResult;
4-
import java.io.IOException;
5-
import java.nio.file.Files;
6-
import java.nio.file.Path;
74
import io.wisoft.prepair.prepair_api.interview.answer.dto.FinalFeedbackResult;
85
import io.wisoft.prepair.prepair_api.interview.answer.dto.FinalFeedbackResponse;
96
import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewAnswer;
107
import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewFeedback;
11-
import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion;
12-
import io.wisoft.prepair.prepair_api.interview.session.entity.InterviewSession;
138
import io.wisoft.prepair.prepair_api.interview.answer.entity.FeedbackType;
149
import io.wisoft.prepair.prepair_api.interview.answer.repository.AnswerRepository;
1510
import io.wisoft.prepair.prepair_api.interview.answer.repository.FeedbackRepository;
16-
import io.wisoft.prepair.prepair_api.interview.question.repository.QuestionRepository;
17-
import io.wisoft.prepair.prepair_api.interview.session.repository.SessionRepository;
1811
import io.wisoft.prepair.prepair_api.interview.answer.service.AnswerPersistenceService;
1912
import io.wisoft.prepair.prepair_api.interview.answer.service.FeedbackGenerator;
13+
import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion;
14+
import io.wisoft.prepair.prepair_api.interview.question.repository.QuestionRepository;
15+
import io.wisoft.prepair.prepair_api.interview.session.entity.InterviewSession;
16+
import io.wisoft.prepair.prepair_api.interview.session.repository.SessionRepository;
2017
import io.wisoft.prepair.prepair_api.common.support.SseEmitterManager;
2118
import lombok.RequiredArgsConstructor;
2219
import lombok.extern.slf4j.Slf4j;
@@ -25,10 +22,15 @@
2522
import org.springframework.stereotype.Component;
2623
import org.springframework.transaction.annotation.Transactional;
2724

25+
import java.io.IOException;
26+
import java.nio.file.Files;
27+
import java.nio.file.Path;
2828
import java.util.ArrayList;
2929
import java.util.List;
3030
import java.util.Map;
31+
import java.util.Optional;
3132
import java.util.UUID;
33+
import java.util.stream.Collectors;
3234

3335
@Slf4j
3436
@Component
@@ -48,92 +50,133 @@ public class AllAnalysisCompletedHandler {
4850
@Transactional
4951
public void handle(AllAnalysisCompletedEvent event) {
5052
UUID answerId = event.answerId();
51-
5253
deleteTempFile(event.videoPath());
5354

5455
if (event.hasFailed()) {
5556
log.error("[종합평가] 분석 실패로 종합평가 생략 - answerId: {}", answerId);
56-
sendFailureToSession(answerId, "분석 중 오류가 발생했습니다.");
57+
failSession(answerId, "분석 중 오류가 발생했습니다.");
5758
return;
5859
}
5960

6061
try {
61-
List<InterviewFeedback> feedbacks = feedbackRepository.findByInterviewAnswerId(answerId);
62-
63-
InterviewFeedback sttFeedback = feedbacks.stream()
64-
.filter(f -> f.getFeedbackType() == FeedbackType.STT)
65-
.findFirst().orElse(null);
66-
67-
InterviewFeedback videoFeedback = feedbacks.stream()
68-
.filter(f -> f.getFeedbackType() == FeedbackType.VIDEO)
69-
.findFirst().orElse(null);
70-
71-
if (sttFeedback == null || videoFeedback == null) {
72-
log.error("[종합평가] STT 또는 Video 피드백 없음 - answerId: {}", answerId);
62+
Optional<AnalysisFeedbacks> feedbacksOpt = findAnalysisFeedbacks(answerId);
63+
if (feedbacksOpt.isEmpty()) {
64+
failSession(answerId, "분석 결과가 누락되어 종합 평가를 생성할 수 없습니다.");
7365
return;
7466
}
7567

7668
InterviewAnswer answer = answerRepository.findByIdWithQuestionAndSession(answerId).orElse(null);
77-
if (answer == null) {
78-
return;
79-
}
80-
81-
String question = answer.getInterviewQuestion().getQuestion();
82-
83-
CombinedFeedbackResult result = feedbackGenerator.generateCombined(
84-
question,
85-
sttFeedback.getFeedback(),
86-
videoFeedback.getFeedback()
87-
);
69+
if (answer == null) return;
8870

89-
answerPersistenceService.saveCombinedFeedback(answerId, result);
90-
log.info("[종합평가] 완료 - answerId: {}, score: {}", answerId, result.score());
71+
saveCombinedFeedback(answerId, answer, feedbacksOpt.get());
72+
tryGenerateFinalFeedback(answer);
9173

92-
checkAndGenerateFinal(answer);
9374
} catch (Exception e) {
9475
log.error("[종합평가] 실패 - answerId: {}, error: {}", answerId, e.getMessage(), e);
95-
sendFailureToSession(answerId, "종합 평가 생성 중 오류가 발생했습니다.");
76+
failSession(answerId, "종합 평가 생성 중 오류가 발생했습니다.");
9677
}
9778
}
9879

99-
private void checkAndGenerateFinal(InterviewAnswer answer) {
80+
private Optional<AnalysisFeedbacks> findAnalysisFeedbacks(UUID answerId) {
81+
List<InterviewFeedback> feedbacks = feedbackRepository.findByInterviewAnswerId(answerId);
82+
83+
InterviewFeedback stt = feedbacks.stream()
84+
.filter(f -> f.getFeedbackType() == FeedbackType.STT)
85+
.findFirst()
86+
.orElse(null);
87+
88+
InterviewFeedback video = feedbacks.stream()
89+
.filter(f -> f.getFeedbackType() == FeedbackType.VIDEO)
90+
.findFirst()
91+
.orElse(null);
92+
93+
if (stt == null || video == null) {
94+
log.error("[종합평가] STT 또는 Video 피드백 없음 - answerId: {}", answerId);
95+
return Optional.empty();
96+
}
97+
98+
return Optional.of(new AnalysisFeedbacks(stt, video));
99+
}
100+
101+
private void saveCombinedFeedback(UUID answerId, InterviewAnswer answer, AnalysisFeedbacks feedbacks) {
102+
String question = answer.getInterviewQuestion().getQuestion();
103+
104+
CombinedFeedbackResult result = feedbackGenerator.generateCombined(
105+
question,
106+
feedbacks.stt().getFeedback(),
107+
feedbacks.video().getFeedback()
108+
);
109+
110+
answerPersistenceService.saveCombinedFeedback(answerId, result);
111+
log.info("[종합평가] 완료 - answerId: {}, score: {}", answerId, result.score());
112+
}
113+
114+
private void tryGenerateFinalFeedback(InterviewAnswer answer) {
100115
InterviewSession session = answer.getInterviewQuestion().getInterviewSession();
101116
if (session == null) {
102117
log.warn("[최종평가] 세션 없음 - answerId: {}", answer.getId());
103118
return;
104119
}
105120

106121
UUID sessionId = session.getId();
122+
if (!isFinalFeedbackReady(sessionId, session.getTotalQuestionCount())) return;
123+
124+
List<InterviewQuestion> questions = questionRepository.findByInterviewSessionId(sessionId);
125+
126+
Map<UUID, InterviewAnswer> answerMap = answerRepository.findBySessionId(sessionId).stream()
127+
.collect(Collectors.toMap(a -> a.getInterviewQuestion().getId(), a -> a));
128+
129+
Map<UUID, List<InterviewFeedback>> feedbackMap = feedbackRepository.findAllBySessionId(sessionId).stream()
130+
.collect(Collectors.groupingBy(f -> f.getInterviewAnswer().getId()));
131+
132+
FinalFeedbackData data = buildFinalData(questions, answerMap, feedbackMap);
133+
FinalFeedbackResult finalResult = feedbackGenerator.generateFinal(data.promptInput());
134+
135+
completeSession(session, data, finalResult);
136+
}
137+
138+
private boolean isFinalFeedbackReady(UUID sessionId, int totalQuestionCount) {
107139
long combinedCount = feedbackRepository.countBySessionIdAndFeedbackType(sessionId, FeedbackType.COMBINED);
108140

109-
if (combinedCount < session.getTotalQuestionCount()) {
110-
log.info("[최종평가] 아직 모든 질문 완료되지 않음 - sessionId: {}, {}/{}", sessionId, combinedCount, session.getTotalQuestionCount());
111-
return;
141+
if (combinedCount < totalQuestionCount) {
142+
log.info("[최종평가] 아직 모든 질문 완료되지 않음 - sessionId: {}, {}/{}", sessionId, combinedCount, totalQuestionCount);
143+
return false;
112144
}
145+
return true;
146+
}
113147

114-
List<InterviewQuestion> questions = questionRepository.findByInterviewSessionId(sessionId);
115-
148+
private FinalFeedbackData buildFinalData(
149+
List<InterviewQuestion> questions,
150+
Map<UUID, InterviewAnswer> answerMap,
151+
Map<UUID, List<InterviewFeedback>> feedbackMap
152+
) {
116153
StringBuilder promptInput = new StringBuilder();
117154
List<FinalFeedbackResponse.QuestionFeedback> questionFeedbacks = new ArrayList<>();
118155
int totalScore = 0;
119156

120157
for (InterviewQuestion q : questions) {
121-
InterviewAnswer ans = answerRepository.findByInterviewQuestionId(q.getId()).orElse(null);
158+
InterviewAnswer ans = answerMap.get(q.getId());
122159
if (ans == null) continue;
123160

124-
List<InterviewFeedback> answerFeedbacks = feedbackRepository.findByInterviewAnswerId(ans.getId());
161+
List<InterviewFeedback> answerFeedbacks = feedbackMap.getOrDefault(ans.getId(), List.of());
125162

126163
InterviewFeedback combined = answerFeedbacks.stream()
127-
.filter(f -> f.getFeedbackType() == FeedbackType.COMBINED).findFirst().orElse(null);
164+
.filter(f -> f.getFeedbackType() == FeedbackType.COMBINED)
165+
.findFirst()
166+
.orElse(null);
128167
if (combined == null) continue;
129168

130169
String sttFeedbackStr = answerFeedbacks.stream()
131-
.filter(f -> f.getFeedbackType() == FeedbackType.STT).findFirst()
132-
.map(InterviewFeedback::getFeedback).orElse(null);
170+
.filter(f -> f.getFeedbackType() == FeedbackType.STT)
171+
.findFirst()
172+
.map(InterviewFeedback::getFeedback)
173+
.orElse(null);
133174

134175
String videoFeedbackStr = answerFeedbacks.stream()
135-
.filter(f -> f.getFeedbackType() == FeedbackType.VIDEO).findFirst()
136-
.map(InterviewFeedback::getFeedback).orElse(null);
176+
.filter(f -> f.getFeedbackType() == FeedbackType.VIDEO)
177+
.findFirst()
178+
.map(InterviewFeedback::getFeedback)
179+
.orElse(null);
137180

138181
promptInput.append("질문: ").append(q.getQuestion()).append("\n");
139182
promptInput.append("종합 평가: ").append(combined.getFeedback()).append("\n");
@@ -152,23 +195,38 @@ private void checkAndGenerateFinal(InterviewAnswer answer) {
152195
}
153196

154197
int finalScore = questionFeedbacks.isEmpty() ? 0 : totalScore / questionFeedbacks.size();
198+
return new FinalFeedbackData(promptInput.toString(), questionFeedbacks, finalScore);
199+
}
155200

156-
FinalFeedbackResult finalResult = feedbackGenerator.generateFinal(promptInput.toString());
201+
private void completeSession(InterviewSession session, FinalFeedbackData data, FinalFeedbackResult finalResult) {
202+
UUID sessionId = session.getId();
157203

158-
session.complete(finalScore, finalResult.finalFeedback());
204+
session.complete(data.finalScore(), finalResult.finalFeedback());
159205
sessionRepository.save(session);
160206

161207
FinalFeedbackResponse response = new FinalFeedbackResponse(
162208
sessionId,
163-
finalScore,
209+
data.finalScore(),
164210
finalResult.finalFeedback(),
165-
questionFeedbacks
211+
data.questionFeedbacks()
166212
);
167213

168214
sseEmitterManager.send(sessionId, "final-complete", response);
169215
sseEmitterManager.complete(sessionId);
170216

171-
log.info("[최종평가] 완료 - sessionId: {}, finalScore: {}", sessionId, finalScore);
217+
log.info("[최종평가] 완료 - sessionId: {}, finalScore: {}", sessionId, data.finalScore());
218+
}
219+
220+
private void failSession(UUID answerId, String message) {
221+
InterviewAnswer answer = answerRepository.findByIdWithQuestionAndSession(answerId).orElse(null);
222+
if (answer == null || answer.getInterviewQuestion().getInterviewSession() == null) return;
223+
224+
InterviewSession session = answer.getInterviewQuestion().getInterviewSession();
225+
session.fail();
226+
sessionRepository.save(session);
227+
228+
sseEmitterManager.send(session.getId(), "analysis-failed", Map.of("message", message));
229+
sseEmitterManager.complete(session.getId());
172230
}
173231

174232
private void deleteTempFile(Path videoPath) {
@@ -180,15 +238,16 @@ private void deleteTempFile(Path videoPath) {
180238
}
181239
}
182240

183-
private void sendFailureToSession(UUID answerId, String message) {
184-
InterviewAnswer answer = answerRepository.findByIdWithQuestionAndSession(answerId).orElse(null);
185-
if (answer != null && answer.getInterviewQuestion().getInterviewSession() != null) {
186-
InterviewSession session = answer.getInterviewQuestion().getInterviewSession();
187-
session.fail();
188-
sessionRepository.save(session);
241+
private record AnalysisFeedbacks(
242+
InterviewFeedback stt,
243+
InterviewFeedback video
244+
) {
245+
}
189246

190-
sseEmitterManager.send(session.getId(), "analysis-failed", Map.of("message", message));
191-
sseEmitterManager.complete(session.getId());
192-
}
247+
private record FinalFeedbackData(
248+
String promptInput,
249+
List<FinalFeedbackResponse.QuestionFeedback> questionFeedbacks,
250+
int finalScore
251+
) {
193252
}
194-
}
253+
}

src/main/java/io/wisoft/prepair/prepair_api/interview/answer/repository/AnswerRepository.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package io.wisoft.prepair.prepair_api.interview.answer.repository;
22

33
import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewAnswer;
4+
5+
import java.util.List;
46
import java.util.Optional;
57
import java.util.UUID;
68
import org.springframework.data.jpa.repository.JpaRepository;
79
import org.springframework.data.jpa.repository.Query;
10+
import org.springframework.data.repository.query.Param;
811

912
public interface AnswerRepository extends JpaRepository<InterviewAnswer, UUID> {
1013

@@ -14,5 +17,8 @@ public interface AnswerRepository extends JpaRepository<InterviewAnswer, UUID> {
1417
"WHERE a.id = :answerId")
1518
Optional<InterviewAnswer> findByIdWithQuestionAndSession(UUID answerId);
1619

17-
Optional<InterviewAnswer> findByInterviewQuestionId(UUID questionId);
20+
@Query("SELECT a FROM InterviewAnswer a " +
21+
"JOIN FETCH a.interviewQuestion q " +
22+
"WHERE q.interviewSession.id = :sessionId")
23+
List<InterviewAnswer> findBySessionId(@Param("sessionId") UUID sessionId);
1824
}

src/main/java/io/wisoft/prepair/prepair_api/interview/answer/repository/FeedbackRepository.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewFeedback;
44
import io.wisoft.prepair.prepair_api.interview.answer.entity.FeedbackType;
5+
56
import java.util.List;
67
import java.util.Optional;
78
import java.util.UUID;
9+
810
import org.springframework.data.jpa.repository.JpaRepository;
911
import org.springframework.data.jpa.repository.Query;
12+
import org.springframework.data.repository.query.Param;
1013

1114
public interface FeedbackRepository extends JpaRepository<InterviewFeedback, UUID> {
1215
List<InterviewFeedback> findByInterviewAnswerId(UUID answerId);
@@ -16,5 +19,9 @@ public interface FeedbackRepository extends JpaRepository<InterviewFeedback, UUI
1619
"AND f.feedbackType = :feedbackType")
1720
long countBySessionIdAndFeedbackType(UUID sessionId, FeedbackType feedbackType);
1821

19-
Optional<InterviewFeedback> findByInterviewAnswerIdAndFeedbackType(UUID answerId, FeedbackType feedbackType);
22+
@Query("SELECT f FROM InterviewFeedback f " +
23+
"JOIN FETCH f.interviewAnswer a " +
24+
"JOIN FETCH a.interviewQuestion q " +
25+
"WHERE q.interviewSession.id = :sessionId")
26+
List<InterviewFeedback> findAllBySessionId(@Param("sessionId") UUID sessionId);
2027
}

0 commit comments

Comments
 (0)