Skip to content

[FIX/#103] 추천 로직 오류 수정 #106

Merged
seamooll merged 5 commits intomainfrom
fix/#103-recommendation
Feb 11, 2026
Merged

[FIX/#103] 추천 로직 오류 수정 #106
seamooll merged 5 commits intomainfrom
fix/#103-recommendation

Conversation

@seamooll
Copy link
Contributor

🔗 Related Issue

📝 Summary

  • 추천 로직 비동기 전환
  • 응답 구조 개선 및 에러 핸들링 강화
  • DTO 구조 개선
  • 추천 업데이트 로직을 금융 mbti 내부로 통합

🔄 Changes

  • API 변경 (추가/수정)
  • 데이터 및 도메인 변경 (DB, 비즈니스 로직)
  • 설정 또는 인프라 관련 변경
  • 리팩토링

💬 Questions & Review Points

📸 API Test Results (Swagger)

✅ Checklist

  • API 테스트 완료
  • 테스트 결과 사진 첨부
  • 빌드 성공 확인 (./gradlew build)

@seamooll seamooll self-assigned this Feb 11, 2026
@seamooll seamooll linked an issue Feb 11, 2026 that may be closed by this pull request
3 tasks
@gemini-code-assist
Copy link

Summary of Changes

Hello @seamooll, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 Pull Request는 적금 추천 시스템의 아키텍처를 재구성하여 사용자 경험과 시스템 안정성을 향상시키는 데 중점을 둡니다. 기존의 복잡한 비동기 추천 생성 과정을 MBTI 테스트 완료 시점에 동기적으로 처리하도록 변경함으로써, 추천 결과를 더 빠르고 예측 가능하게 제공합니다. 또한, API 응답 DTO를 간소화하고 에러 처리 로직을 명확히 하여 개발 및 유지보수 효율성을 높였습니다.

Highlights

  • 추천 로직 동기화: 기존 비동기 배치 기반의 추천 상품 생성 로직을 동기 방식으로 전환하여, MBTI 테스트 완료 시 즉시 추천이 생성되도록 변경되었습니다.
  • MBTI 서비스에 추천 로직 통합: 금융 MBTI 테스트 제출 시 추천 상품 생성 로직이 FinanceMbtiService 내부에 통합되어, MBTI 결과와 추천 생성이 하나의 흐름으로 처리됩니다.
  • API 응답 구조 및 에러 핸들링 개선: 추천 관련 API의 응답 DTO 구조가 간소화되고, 추천 생성 실패 및 추천 내역 부재 시의 에러 핸들링이 강화되었습니다.
  • 비동기 처리 관련 코드 제거: 비동기 추천 로직을 담당하던 RecommendationAsyncWorker 서비스와 관련 배치 처리 로직 및 설정이 완전히 제거되었습니다.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/main/java/org/umc/valuedi/domain/mbti/service/FinanceMbtiService.java
    • 로깅을 위한 Slf4j 어노테이션이 추가되었습니다.
    • RecommendationService 의존성이 주입되었습니다.
    • submitTest 메서드 내에서 recommendationService.generateAndSaveRecommendations(memberId) 호출이 추가되었으며, 추천 생성 실패 시에도 MBTI 저장은 유지되도록 예외 처리가 적용되었습니다.
  • src/main/java/org/umc/valuedi/domain/savings/controller/RecommendationController.java
    • RecommendationTxServiceRecommendationAsyncWorker 의존성이 제거되었습니다.
    • recommend 엔드포인트가 recommendationService.generateAndSaveRecommendations를 동기적으로 직접 호출하도록 변경되었습니다.
    • recommend 엔드포인트의 반환 타입이 TriggerResponse에서 SavingsListResponse로 변경되었습니다.
  • src/main/java/org/umc/valuedi/domain/savings/controller/RecommendationControllerDocs.java
    • recommend API의 Swagger 문서 요약이 "적금 추천 생성 트리거 API (비동기)"에서 "[개발/수동 갱신용] 적금 추천 생성 API"로 변경되었습니다.
    • API 설명이 동기적 동작과 MBTI 테스트 완료 시 자동 호출됨을 명확히 하도록 업데이트되었으며, 비동기 관련 세부 정보가 제거되었습니다.
    • 응답 코드 및 예시가 동기적 동작과 SavingsListResponse를 직접 반환하는 형태로 수정되었습니다.
    • MBTI404_3GEMINI502_2 에러 코드가 제거되고 SAVINGS500_1로 대체되었습니다.
    • latest15latestTop3 API 설명에서 PENDING/FAILED 상태 관련 내용이 제거되었습니다.
    • latest15latestTop3 예시 응답에서 statusmessage 필드가 제거되었고, 추천 내역이 없을 때 SAVINGS404_2 에러가 추가되었습니다.
  • src/main/java/org/umc/valuedi/domain/savings/converter/SavingsConverter.java
    • toSavingsListResponseDTO 메서드에서 maxPageNo 필드가 제거되고 hasNext 필드가 추가되었습니다.
    • toSavingsDetailResponseDTO 메서드가 SavingProductDetail 중첩 레코드를 제거하고 제품 상세 정보를 SavingsDetailResponse에 직접 포함하도록 리팩토링되었습니다. 또한, 옵션에서 파생된 basicRatemaxRate 필드가 추가되었습니다.
  • src/main/java/org/umc/valuedi/domain/savings/dto/response/SavingsResponseDTO.java
    • SavingsListResponse 레코드에서 maxPageNo, status, message 필드가 제거되고 hasNext 필드가 추가되었습니다.
    • SavingsDetailResponse 레코드가 SavingProductDetail 중첩 레코드를 제거하고 필드를 평탄화하도록 리팩토링되었습니다. basicRatemaxRate 필드가 추가되었습니다.
    • TriggerResponseTriggerDecision 레코드가 제거되었습니다.
  • src/main/java/org/umc/valuedi/domain/savings/exception/code/SavingsErrorCode.java
    • RECOMMENDATION_NOT_FOUND, FILTERED_RECOMMENDATION_NOT_FOUND, RECOMMENDATION_FAILED 에러 코드가 추가되었습니다.
  • src/main/java/org/umc/valuedi/domain/savings/service/RecommendationAsyncWorker.java
    • 비동기 추천 생성을 담당하던 파일이 제거되었습니다.
  • src/main/java/org/umc/valuedi/domain/savings/service/RecommendationService.java
    • RecommendationBatch 임포트가 제거되었습니다.
    • generateAndSaveRecommendations 메서드가 SavingsResponseDTO.SavingsListResponse를 반환하도록 변경되었고, 멱등성 검사 로직이 제거되었습니다.
    • 추천 후보가 없거나 파싱된 항목이 비어있을 경우 SavingsException을 발생시키도록 변경되었습니다.
    • 로깅 메시지가 [RecommendAsync]에서 [Recommend]로 업데이트되었습니다.
    • getRecommendationgetRecommendationTop3 메서드가 상태/메시지를 포함한 SavingsListResponse를 반환하는 대신 SavingsException을 발생시키도록 변경되었습니다.
    • getRecommendationState 헬퍼 메서드가 제거되었습니다.
    • emptyResponse 헬퍼 메서드가 추가되었습니다.
  • src/main/java/org/umc/valuedi/domain/savings/service/RecommendationTxService.java
    • RecommendationBatchRepositoryMemberMbtiTestRepository 의존성이 제거되었습니다.
    • triggerRecommendation, markProcessing, markSuccess, markFailed, findLatestBatch와 관련된 모든 메서드가 제거되었습니다.
  • src/main/java/org/umc/valuedi/global/config/AsyncConfig.java
    • 비동기 추천 로직이 더 이상 사용되지 않으므로 recommendationExecutor 빈 정의가 제거되었습니다.
Activity
  • PR 제목은 추천 로직 오류 수정([FIX/#103] 추천 로직 오류 수정)을 나타냅니다.
  • PR 설명은 추천 로직의 비동기 전환, 응답 구조 개선 및 에러 핸들링 강화, DTO 구조 개선, 그리고 추천 업데이트 로직을 금융 MBTI 내부로 통합하는 변경 사항을 언급합니다.
  • 패치 내용은 비동기 처리 구성 요소 제거 및 추천 로직을 MBTI 서비스에 직접 통합하는 변경 사항과 일치합니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@seamooll seamooll merged commit 1860c74 into main Feb 11, 2026
1 check passed
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This PR refactors the recommendation logic from asynchronous to synchronous, improving API response structure and error handling, aiming to reduce code complexity and enhance user experience. However, these changes, especially the synchronous processing and removal of idempotency checks, introduce a high risk of Denial of Service (DoS) due to thread exhaustion when calling the external Gemini API, and a medium risk of resource exhaustion from redundant recommendation generation. It is strongly recommended to re-implement asynchronous processing and restore idempotency checks to ensure system stability and cost-efficiency. Further improvements could include fast-fail for empty candidate sets, logging enhancements, and consistent API error codes.

// 제미나이 호출
log.info("[RecommendAsync] Gemini request. memberId={}, promptChars={}", memberId, prompt.length());
log.info("[Recommend] Gemini request. memberId={}, promptChars={}", memberId, prompt.length());
String raw = geminiClient.generateText(prompt);

Choose a reason for hiding this comment

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

security-high high

The generateAndSaveRecommendations method performs a synchronous call to the Gemini API via geminiClient.generateText(prompt). This method is called directly from the RecommendationController and FinanceMbtiService, blocking the HTTP worker thread. The GeminiClient uses a small fixed thread pool (size 4) and has a long overall deadline (240s). If multiple concurrent requests are made, the HTTP worker threads will be exhausted while waiting for the limited Gemini threads or the API response, leading to a complete denial of service (DoS) for the entire application. It is highly recommended to revert to an asynchronous processing model or implement a more robust concurrency strategy with shorter timeouts and a larger, dedicated thread pool.


@Transactional
public void generateAndSaveRecommendations(Long memberId) {
public SavingsResponseDTO.SavingsListResponse generateAndSaveRecommendations(Long memberId) {

Choose a reason for hiding this comment

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

security-medium medium

The removal of idempotency checks for recommendation generation introduces a medium risk of resource exhaustion. Without this check, the system can generate duplicate recommendations and make unnecessary, expensive calls to the Gemini API, potentially leading to quota exhaustion, database bloat, and increased server load if the /api/savings/recommendations endpoint is spammed. It is crucial to restore the idempotency check to prevent redundant processing. Consider adding logic to check if recommendations already exist for a given memberMbtiTest before generating new ones, returning existing data if found.

// 멱등성 체크: 이미 해당 MBTI 테스트에 대한 추천이 존재하면, 새로 생성하지 않고 기존 데이터를 반환합니다.
// 참고: recommendationRepository에 existsByMemberMbtiTestId(Long memberMbtiTestId) 메소드 추가가 필요할 수 있습니다.
if (recommendationRepository.existsByMemberMbtiTestId(memberMbtiTest.getId())) {
    log.info("[Recommend] 이미 추천 데이터가 존재하여 기존 데이터를 반환합니다. memberId={}, mbtiTestId={}", memberId, memberMbtiTest.getId());
    return getRecommendation(memberId, null); // rsrvType=null로 전체 조회
}

log.info("[Recommend] MBTI 검사 후 자동 추천 생성 성공. memberId={}", memberId);
} catch (Exception e) {
// 제미나이 호출이 실패해도 MBTI 저장은 유지되도록 예외를 삼키고 로그만 남김
log.error("[Recommend] MBTI 저장에는 성공했으나, 자동 추천 생성 중 오류 발생. memberId={}: {}", memberId, e.getMessage());

Choose a reason for hiding this comment

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

medium

예외를 로깅할 때 e.getMessage()만 사용하면 원인 파악에 필요한 정보가 부족할 수 있습니다. 특히 getMessage()null을 반환하는 경우 디버깅이 어려워집니다. 전체 스택 트레이스를 포함하도록 로거를 사용하는 것이 좋습니다.

Suggested change
log.error("[Recommend] MBTI 저장에는 성공했으나, 자동 추천 생성 중 오류 발생. memberId={}: {}", memberId, e.getMessage());
log.error("[Recommend] MBTI 저장에는 성공했으나, 자동 추천 생성 중 오류 발생. memberId={}", memberId, e);

Comment on lines 63 to 64
Pageable candidatePage = PageRequest.of(0, CANDIDATE_LIMIT);
List<SavingsOption> candidates = savingsOptionRepository.findCandidates(candidatePage);

Choose a reason for hiding this comment

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

medium

추천 상품 후보군(candidates)이 비어있는 경우에 대한 처리가 없습니다. 이 경우 불필요하게 Gemini API를 호출하게 되어 비효율적이며, 결국 RECOMMENDATION_FAILED 예외가 발생합니다.

후보군 조회 직후 리스트가 비어있는지 확인하고, 비어있다면 API 호출 없이 바로 예외를 발생시키는 '빠른 실패(fail-fast)' 로직을 추가하는 것이 좋습니다. candidates 조회 후에 아래 코드를 추가하는 것을 제안합니다.

if (candidates.isEmpty()) {
    log.warn("[Recommend] 추천 상품 후보가 없습니다. memberId={}", memberId);
    throw new SavingsException(SavingsErrorCode.RECOMMENDATION_FAILED, "추천할 상품 후보가 없습니다.");
}

options
);
SavingsOption representativeOption = savings.getSavingsOptionList().stream()
.filter(o -> Integer.valueOf(12).equals(o.getSaveTrm()))

Choose a reason for hiding this comment

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

medium

대표 옵션을 찾을 때 12라는 매직 넘버(magic number)가 사용되었습니다. 코드의 가독성과 유지보수성을 높이기 위해 클래스 레벨에 private static final Integer REPRESENTATIVE_TERM_MONTHS = 12; 와 같이 상수로 정의하여 사용하는 것을 권장합니다.

Suggested change
.filter(o -> Integer.valueOf(12).equals(o.getSaveTrm()))
.filter(o -> REPRESENTATIVE_TERM_MONTHS.equals(o.getSaveTrm()))

SAVINGS_NOT_FOUND(HttpStatus.NOT_FOUND, "SAVINGS404_1", "해당 적금 상품을 찾을 수 없습니다.")
SAVINGS_NOT_FOUND(HttpStatus.NOT_FOUND, "SAVINGS404_1", "해당 적금 상품을 찾을 수 없습니다."),
RECOMMENDATION_NOT_FOUND(HttpStatus.NOT_FOUND, "SAVINGS404_2", "아직 추천받은 내역이 없습니다. 먼저 상품 추천을 진행해 주세요."),
FILTERED_RECOMMENDATION_NOT_FOUND(HttpStatus.NOT_FOUND, "SAVINGS400_1", "해당 필터 조건에 맞는 추천 상품이 없습니다."),

Choose a reason for hiding this comment

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

medium

FILTERED_RECOMMENDATION_NOT_FOUND 에러 코드의 HTTP 상태는 NOT_FOUND(404)이지만, 코드명은 SAVINGS400_1로 불일치합니다. API 응답의 일관성을 위해 HTTP 상태와 코드명을 맞추는 것이 좋습니다. 필터 조건에 맞는 결과가 없는 것은 클라이언트의 요청(필터)에 따른 결과로 볼 수 있으므로, HttpStatus.BAD_REQUEST(400)로 변경하는 것을 제안합니다.

Suggested change
FILTERED_RECOMMENDATION_NOT_FOUND(HttpStatus.NOT_FOUND, "SAVINGS400_1", "해당 필터 조건에 맞는 추천 상품이 없습니다."),
FILTERED_RECOMMENDATION_NOT_FOUND(HttpStatus.BAD_REQUEST, "SAVINGS400_1", "해당 필터 조건에 맞는 추천 상품이 없습니다."),

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FIX] 추천 로직 오류 수정

1 participant