-
Notifications
You must be signed in to change notification settings - Fork 0
refactor: AllAnalysisCompletedHandler 메서드 책임 분리 및 N+1 쿼리 개선 #69
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,22 +1,19 @@ | ||||||
| package io.wisoft.prepair.prepair_api.interview.answer.event; | ||||||
|
|
||||||
| import io.wisoft.prepair.prepair_api.interview.answer.dto.CombinedFeedbackResult; | ||||||
| import java.io.IOException; | ||||||
| import java.nio.file.Files; | ||||||
| import java.nio.file.Path; | ||||||
| import io.wisoft.prepair.prepair_api.interview.answer.dto.FinalFeedbackResult; | ||||||
| import io.wisoft.prepair.prepair_api.interview.answer.dto.FinalFeedbackResponse; | ||||||
| import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewAnswer; | ||||||
| import io.wisoft.prepair.prepair_api.interview.answer.entity.InterviewFeedback; | ||||||
| import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion; | ||||||
| import io.wisoft.prepair.prepair_api.interview.session.entity.InterviewSession; | ||||||
| import io.wisoft.prepair.prepair_api.interview.answer.entity.FeedbackType; | ||||||
| import io.wisoft.prepair.prepair_api.interview.answer.repository.AnswerRepository; | ||||||
| import io.wisoft.prepair.prepair_api.interview.answer.repository.FeedbackRepository; | ||||||
| import io.wisoft.prepair.prepair_api.interview.question.repository.QuestionRepository; | ||||||
| import io.wisoft.prepair.prepair_api.interview.session.repository.SessionRepository; | ||||||
| import io.wisoft.prepair.prepair_api.interview.answer.service.AnswerPersistenceService; | ||||||
| import io.wisoft.prepair.prepair_api.interview.answer.service.FeedbackGenerator; | ||||||
| import io.wisoft.prepair.prepair_api.interview.question.entity.InterviewQuestion; | ||||||
| import io.wisoft.prepair.prepair_api.interview.question.repository.QuestionRepository; | ||||||
| import io.wisoft.prepair.prepair_api.interview.session.entity.InterviewSession; | ||||||
| import io.wisoft.prepair.prepair_api.interview.session.repository.SessionRepository; | ||||||
| import io.wisoft.prepair.prepair_api.common.support.SseEmitterManager; | ||||||
| import lombok.RequiredArgsConstructor; | ||||||
| import lombok.extern.slf4j.Slf4j; | ||||||
|
|
@@ -25,10 +22,15 @@ | |||||
| import org.springframework.stereotype.Component; | ||||||
| import org.springframework.transaction.annotation.Transactional; | ||||||
|
|
||||||
| import java.io.IOException; | ||||||
| import java.nio.file.Files; | ||||||
| import java.nio.file.Path; | ||||||
| import java.util.ArrayList; | ||||||
| import java.util.List; | ||||||
| import java.util.Map; | ||||||
| import java.util.Optional; | ||||||
| import java.util.UUID; | ||||||
| import java.util.stream.Collectors; | ||||||
|
|
||||||
| @Slf4j | ||||||
| @Component | ||||||
|
|
@@ -48,92 +50,133 @@ public class AllAnalysisCompletedHandler { | |||||
| @Transactional | ||||||
| public void handle(AllAnalysisCompletedEvent event) { | ||||||
| UUID answerId = event.answerId(); | ||||||
|
|
||||||
| deleteTempFile(event.videoPath()); | ||||||
|
|
||||||
| if (event.hasFailed()) { | ||||||
| log.error("[종합평가] 분석 실패로 종합평가 생략 - answerId: {}", answerId); | ||||||
| sendFailureToSession(answerId, "분석 중 오류가 발생했습니다."); | ||||||
| failSession(answerId, "분석 중 오류가 발생했습니다."); | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| try { | ||||||
| List<InterviewFeedback> feedbacks = feedbackRepository.findByInterviewAnswerId(answerId); | ||||||
|
|
||||||
| InterviewFeedback sttFeedback = feedbacks.stream() | ||||||
| .filter(f -> f.getFeedbackType() == FeedbackType.STT) | ||||||
| .findFirst().orElse(null); | ||||||
|
|
||||||
| InterviewFeedback videoFeedback = feedbacks.stream() | ||||||
| .filter(f -> f.getFeedbackType() == FeedbackType.VIDEO) | ||||||
| .findFirst().orElse(null); | ||||||
|
|
||||||
| if (sttFeedback == null || videoFeedback == null) { | ||||||
| log.error("[종합평가] STT 또는 Video 피드백 없음 - answerId: {}", answerId); | ||||||
| Optional<AnalysisFeedbacks> feedbacksOpt = findAnalysisFeedbacks(answerId); | ||||||
| if (feedbacksOpt.isEmpty()) { | ||||||
| failSession(answerId, "분석 결과가 누락되어 종합 평가를 생성할 수 없습니다."); | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| InterviewAnswer answer = answerRepository.findByIdWithQuestionAndSession(answerId).orElse(null); | ||||||
| if (answer == null) { | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| String question = answer.getInterviewQuestion().getQuestion(); | ||||||
|
|
||||||
| CombinedFeedbackResult result = feedbackGenerator.generateCombined( | ||||||
| question, | ||||||
| sttFeedback.getFeedback(), | ||||||
| videoFeedback.getFeedback() | ||||||
| ); | ||||||
| if (answer == null) return; | ||||||
|
|
||||||
| answerPersistenceService.saveCombinedFeedback(answerId, result); | ||||||
| log.info("[종합평가] 완료 - answerId: {}, score: {}", answerId, result.score()); | ||||||
| saveCombinedFeedback(answerId, answer, feedbacksOpt.get()); | ||||||
| tryGenerateFinalFeedback(answer); | ||||||
|
|
||||||
| checkAndGenerateFinal(answer); | ||||||
| } catch (Exception e) { | ||||||
| log.error("[종합평가] 실패 - answerId: {}, error: {}", answerId, e.getMessage(), e); | ||||||
| sendFailureToSession(answerId, "종합 평가 생성 중 오류가 발생했습니다."); | ||||||
| failSession(answerId, "종합 평가 생성 중 오류가 발생했습니다."); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| private void checkAndGenerateFinal(InterviewAnswer answer) { | ||||||
| private Optional<AnalysisFeedbacks> findAnalysisFeedbacks(UUID answerId) { | ||||||
| List<InterviewFeedback> feedbacks = feedbackRepository.findByInterviewAnswerId(answerId); | ||||||
|
|
||||||
| InterviewFeedback stt = feedbacks.stream() | ||||||
| .filter(f -> f.getFeedbackType() == FeedbackType.STT) | ||||||
| .findFirst() | ||||||
| .orElse(null); | ||||||
|
|
||||||
| InterviewFeedback video = feedbacks.stream() | ||||||
| .filter(f -> f.getFeedbackType() == FeedbackType.VIDEO) | ||||||
| .findFirst() | ||||||
| .orElse(null); | ||||||
|
|
||||||
| if (stt == null || video == null) { | ||||||
| log.error("[종합평가] STT 또는 Video 피드백 없음 - answerId: {}", answerId); | ||||||
| return Optional.empty(); | ||||||
| } | ||||||
|
|
||||||
| return Optional.of(new AnalysisFeedbacks(stt, video)); | ||||||
| } | ||||||
|
|
||||||
| private void saveCombinedFeedback(UUID answerId, InterviewAnswer answer, AnalysisFeedbacks feedbacks) { | ||||||
| String question = answer.getInterviewQuestion().getQuestion(); | ||||||
|
|
||||||
| CombinedFeedbackResult result = feedbackGenerator.generateCombined( | ||||||
| question, | ||||||
| feedbacks.stt().getFeedback(), | ||||||
| feedbacks.video().getFeedback() | ||||||
| ); | ||||||
|
|
||||||
| answerPersistenceService.saveCombinedFeedback(answerId, result); | ||||||
| log.info("[종합평가] 완료 - answerId: {}, score: {}", answerId, result.score()); | ||||||
| } | ||||||
|
|
||||||
| private void tryGenerateFinalFeedback(InterviewAnswer answer) { | ||||||
| InterviewSession session = answer.getInterviewQuestion().getInterviewSession(); | ||||||
| if (session == null) { | ||||||
| log.warn("[최종평가] 세션 없음 - answerId: {}", answer.getId()); | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| UUID sessionId = session.getId(); | ||||||
| if (!isFinalFeedbackReady(sessionId, session.getTotalQuestionCount())) return; | ||||||
|
|
||||||
| List<InterviewQuestion> questions = questionRepository.findByInterviewSessionId(sessionId); | ||||||
|
|
||||||
| Map<UUID, InterviewAnswer> answerMap = answerRepository.findBySessionId(sessionId).stream() | ||||||
| .collect(Collectors.toMap(a -> a.getInterviewQuestion().getId(), a -> a)); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| Map<UUID, List<InterviewFeedback>> feedbackMap = feedbackRepository.findAllBySessionId(sessionId).stream() | ||||||
| .collect(Collectors.groupingBy(f -> f.getInterviewAnswer().getId())); | ||||||
|
|
||||||
| FinalFeedbackData data = buildFinalData(questions, answerMap, feedbackMap); | ||||||
| FinalFeedbackResult finalResult = feedbackGenerator.generateFinal(data.promptInput()); | ||||||
|
|
||||||
| completeSession(session, data, finalResult); | ||||||
| } | ||||||
|
|
||||||
| private boolean isFinalFeedbackReady(UUID sessionId, int totalQuestionCount) { | ||||||
| long combinedCount = feedbackRepository.countBySessionIdAndFeedbackType(sessionId, FeedbackType.COMBINED); | ||||||
|
|
||||||
| if (combinedCount < session.getTotalQuestionCount()) { | ||||||
| log.info("[최종평가] 아직 모든 질문 완료되지 않음 - sessionId: {}, {}/{}", sessionId, combinedCount, session.getTotalQuestionCount()); | ||||||
| return; | ||||||
| if (combinedCount < totalQuestionCount) { | ||||||
| log.info("[최종평가] 아직 모든 질문 완료되지 않음 - sessionId: {}, {}/{}", sessionId, combinedCount, totalQuestionCount); | ||||||
| return false; | ||||||
| } | ||||||
| return true; | ||||||
| } | ||||||
|
|
||||||
| List<InterviewQuestion> questions = questionRepository.findByInterviewSessionId(sessionId); | ||||||
|
|
||||||
| private FinalFeedbackData buildFinalData( | ||||||
| List<InterviewQuestion> questions, | ||||||
| Map<UUID, InterviewAnswer> answerMap, | ||||||
| Map<UUID, List<InterviewFeedback>> feedbackMap | ||||||
| ) { | ||||||
| StringBuilder promptInput = new StringBuilder(); | ||||||
| List<FinalFeedbackResponse.QuestionFeedback> questionFeedbacks = new ArrayList<>(); | ||||||
| int totalScore = 0; | ||||||
|
|
||||||
| for (InterviewQuestion q : questions) { | ||||||
| InterviewAnswer ans = answerRepository.findByInterviewQuestionId(q.getId()).orElse(null); | ||||||
| InterviewAnswer ans = answerMap.get(q.getId()); | ||||||
| if (ans == null) continue; | ||||||
|
|
||||||
| List<InterviewFeedback> answerFeedbacks = feedbackRepository.findByInterviewAnswerId(ans.getId()); | ||||||
| List<InterviewFeedback> answerFeedbacks = feedbackMap.getOrDefault(ans.getId(), List.of()); | ||||||
|
|
||||||
| InterviewFeedback combined = answerFeedbacks.stream() | ||||||
| .filter(f -> f.getFeedbackType() == FeedbackType.COMBINED).findFirst().orElse(null); | ||||||
| .filter(f -> f.getFeedbackType() == FeedbackType.COMBINED) | ||||||
| .findFirst() | ||||||
| .orElse(null); | ||||||
| if (combined == null) continue; | ||||||
|
|
||||||
| String sttFeedbackStr = answerFeedbacks.stream() | ||||||
| .filter(f -> f.getFeedbackType() == FeedbackType.STT).findFirst() | ||||||
| .map(InterviewFeedback::getFeedback).orElse(null); | ||||||
| .filter(f -> f.getFeedbackType() == FeedbackType.STT) | ||||||
| .findFirst() | ||||||
| .map(InterviewFeedback::getFeedback) | ||||||
| .orElse(null); | ||||||
|
|
||||||
| String videoFeedbackStr = answerFeedbacks.stream() | ||||||
| .filter(f -> f.getFeedbackType() == FeedbackType.VIDEO).findFirst() | ||||||
| .map(InterviewFeedback::getFeedback).orElse(null); | ||||||
| .filter(f -> f.getFeedbackType() == FeedbackType.VIDEO) | ||||||
| .findFirst() | ||||||
| .map(InterviewFeedback::getFeedback) | ||||||
| .orElse(null); | ||||||
|
|
||||||
| promptInput.append("질문: ").append(q.getQuestion()).append("\n"); | ||||||
| promptInput.append("종합 평가: ").append(combined.getFeedback()).append("\n"); | ||||||
|
|
@@ -152,23 +195,38 @@ private void checkAndGenerateFinal(InterviewAnswer answer) { | |||||
| } | ||||||
|
|
||||||
| int finalScore = questionFeedbacks.isEmpty() ? 0 : totalScore / questionFeedbacks.size(); | ||||||
| return new FinalFeedbackData(promptInput.toString(), questionFeedbacks, finalScore); | ||||||
| } | ||||||
|
|
||||||
| FinalFeedbackResult finalResult = feedbackGenerator.generateFinal(promptInput.toString()); | ||||||
| private void completeSession(InterviewSession session, FinalFeedbackData data, FinalFeedbackResult finalResult) { | ||||||
| UUID sessionId = session.getId(); | ||||||
|
|
||||||
| session.complete(finalScore, finalResult.finalFeedback()); | ||||||
| session.complete(data.finalScore(), finalResult.finalFeedback()); | ||||||
| sessionRepository.save(session); | ||||||
|
|
||||||
| FinalFeedbackResponse response = new FinalFeedbackResponse( | ||||||
| sessionId, | ||||||
| finalScore, | ||||||
| data.finalScore(), | ||||||
| finalResult.finalFeedback(), | ||||||
| questionFeedbacks | ||||||
| data.questionFeedbacks() | ||||||
| ); | ||||||
|
|
||||||
| sseEmitterManager.send(sessionId, "final-complete", response); | ||||||
| sseEmitterManager.complete(sessionId); | ||||||
|
|
||||||
| log.info("[최종평가] 완료 - sessionId: {}, finalScore: {}", sessionId, finalScore); | ||||||
| log.info("[최종평가] 완료 - sessionId: {}, finalScore: {}", sessionId, data.finalScore()); | ||||||
| } | ||||||
|
|
||||||
| private void failSession(UUID answerId, String message) { | ||||||
| InterviewAnswer answer = answerRepository.findByIdWithQuestionAndSession(answerId).orElse(null); | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
| if (answer == null || answer.getInterviewQuestion().getInterviewSession() == null) return; | ||||||
|
|
||||||
| InterviewSession session = answer.getInterviewQuestion().getInterviewSession(); | ||||||
| session.fail(); | ||||||
| sessionRepository.save(session); | ||||||
|
|
||||||
| sseEmitterManager.send(session.getId(), "analysis-failed", Map.of("message", message)); | ||||||
| sseEmitterManager.complete(session.getId()); | ||||||
| } | ||||||
|
|
||||||
| private void deleteTempFile(Path videoPath) { | ||||||
|
|
@@ -180,15 +238,16 @@ private void deleteTempFile(Path videoPath) { | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| private void sendFailureToSession(UUID answerId, String message) { | ||||||
| InterviewAnswer answer = answerRepository.findByIdWithQuestionAndSession(answerId).orElse(null); | ||||||
| if (answer != null && answer.getInterviewQuestion().getInterviewSession() != null) { | ||||||
| InterviewSession session = answer.getInterviewQuestion().getInterviewSession(); | ||||||
| session.fail(); | ||||||
| sessionRepository.save(session); | ||||||
| private record AnalysisFeedbacks( | ||||||
| InterviewFeedback stt, | ||||||
| InterviewFeedback video | ||||||
| ) { | ||||||
| } | ||||||
|
|
||||||
| sseEmitterManager.send(session.getId(), "analysis-failed", Map.of("message", message)); | ||||||
| sseEmitterManager.complete(session.getId()); | ||||||
| } | ||||||
| private record FinalFeedbackData( | ||||||
| String promptInput, | ||||||
| List<FinalFeedbackResponse.QuestionFeedback> questionFeedbacks, | ||||||
| int finalScore | ||||||
| ) { | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
handle메서드에@Transactional이 선언되어 있어, 내부 로직 수행 중 런타임 예외가 발생하면 해당 트랜잭션은 rollback-only로 마킹됩니다. 이 경우catch블록에서 호출하는failSession메서드 내의 DB 저장 로직(sessionRepository.save)이 정상적으로 반영되지 않아, 세션 상태가FAILED로 변경되지 않는 문제가 발생할 수 있습니다. 비즈니스 로직의 실패 여부와 관계없이 세션 상태를 업데이트하려면 트랜잭션 경계를 적절히 분리하거나failSession을 별도의 트랜잭션(예:Propagation.REQUIRES_NEW)으로 처리하는 것을 권장합니다.