Skip to content

Feat/124 OpenAi 호출 실패 시 Claude Api 호출하는 Fallback구조 도입#142

Merged
HeeMang-Lee merged 6 commits intodevfrom
feat/124
Jun 23, 2025
Merged

Feat/124 OpenAi 호출 실패 시 Claude Api 호출하는 Fallback구조 도입#142
HeeMang-Lee merged 6 commits intodevfrom
feat/124

Conversation

@HeeMang-Lee
Copy link
Copy Markdown
Member

@HeeMang-Lee HeeMang-Lee commented Jun 22, 2025

🔎 작업 내용

  • OpenAI 호출 실패 시 Claude 모델을 통해 Fallback하여 AI 응답을 생성하는 구조를 도입
  • AI 피드백 생성 흐름을 FallbackAiChatClient 중심으로 재구성

🛠️ 변경 사항

  • AiChatClient 인터페이스 도입 및 추상화
  • OpenAiChatClientClaudeChatClient 각각 구현체로 분리
  • FallbackAiChatClient 구현
    • OpenAI 실패 시 Claude로 폴백
    • @Primary 지정으로 기본 AiChatClient로 사용됨
  • AiService에서 기존 ChatClient 대신 AiChatClient 의존성 주입
  • AiConfig에서 OpenAI / Claude 모델 및 ChatClient 빈 설정 분리
  • 통합 테스트(FallbackAiChatClientIntegrationTest) 작성
    • 실제 DB 연동 테스트로 폴백 로직 확인
  • 단위 테스트(FallbackAiChatClientTest) 작성
    • Mock 기반 테스트로 폴백 흐름 검증

🧩 트러블 슈팅

  • 문제: ChatClient가 두 개 빈으로 등록되며 충돌 발생
    • ClaudeChatClient, OpenAiChatClient 모두 ChatClient 빈을 가짐
  • 해결 : @primary 어노테이션 사용으로 FallbackAiChatCleint 우선으로 빈 등록되게 변경

Summary by CodeRabbit

  • 신규 기능
    • OpenAI와 Claude AI를 모두 지원하는 AI 채팅 기능이 추가되었습니다.
    • OpenAI 실패 시 Claude AI로 자동 전환되는 AI 피드백 서비스의 이중화가 도입되었습니다.
  • 버그 수정
    • 퀴즈 카테고리 중복 생성을 방지하는 로직이 테스트 코드에 반영되었습니다.
  • 설정
    • Anthropic Claude AI 관련 설정 항목이 추가되었습니다.
  • 테스트
    • AI 채팅 클라이언트의 이중화 동작을 검증하는 단위 및 통합 테스트가 추가되었습니다.

closed #124

@HeeMang-Lee HeeMang-Lee linked an issue Jun 22, 2025 that may be closed by this pull request
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jun 22, 2025

Walkthrough

AI 챗 클라이언트 계층이 도입되어 OpenAI와 Anthropic Claude 모델을 모두 지원하며, OpenAI 실패 시 Claude로 자동 폴백하는 메커니즘이 추가되었습니다. 관련 서비스, 설정, 테스트 코드가 이에 맞게 수정 및 확장되었으며, 퀴즈 카테고리 관련 리포지토리와 테스트 로직도 개선되었습니다.

Changes

파일/경로 그룹 변경 요약
cs25-entity/.../QuizCategoryRepository.java existsByCategoryType(String categoryType) 메서드 추가
cs25-service/build.gradle, .../application.properties Anthropic(Claude) AI 의존성 및 설정 추가, OpenAI 설정 주석 명확화
cs25-service/.../ai/client/AiChatClient.java AI 챗 클라이언트 인터페이스 신설
cs25-service/.../ai/client/ClaudeChatClient.java
cs25-service/.../ai/client/OpenAiChatClient.java
cs25-service/.../ai/client/FallbackAiChatClient.java
Claude, OpenAI, Fallback AI 챗 클라이언트 구현체 신설 및 폴백 로직 구현
cs25-service/.../ai/config/AiConfig.java AI 모델 및 클라이언트 빈 생성 로직 분리 및 Anthropic 지원 추가
cs25-service/.../ai/service/AiService.java AI 피드백 생성에 FallbackAiChatClient 사용으로 변경
cs25-service/src/test/java/com/example/cs25service/ai/AiQuestionGeneratorServiceTest.java 퀴즈 카테고리 존재 여부 확인 후 조건부 insert로 변경, 엔티티 매니저 동기화 추가
cs25-service/src/test/java/com/example/cs25service/ai/FallbackAiChatClientTest.java
cs25-service/src/test/java/com/example/cs25service/ai/FallbackAiChatClientIntegrationTest.java
FallbackAiChatClient의 폴백 동작 단위 및 통합 테스트 신설

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant AiService
    participant FallbackAiChatClient
    participant OpenAiChatClient
    participant ClaudeChatClient

    User->>AiService: 피드백 요청
    AiService->>FallbackAiChatClient: call(systemPrompt, userPrompt)
    FallbackAiChatClient->>OpenAiChatClient: call(systemPrompt, userPrompt)
    OpenAiChatClient-->>FallbackAiChatClient: Exception 발생
    FallbackAiChatClient->>ClaudeChatClient: call(systemPrompt, userPrompt)
    ClaudeChatClient-->>FallbackAiChatClient: Claude 응답
    FallbackAiChatClient-->>AiService: Claude 응답 반환
    AiService-->>User: 피드백 결과 반환
Loading

Suggested reviewers

  • crocusia
  • Kimyoonbeom

Poem

🐇
AI가 두 개라니, 토끼도 놀라
한 번 실패해도 걱정 마세요
OpenAI가 넘어지면 Claude가 달려가죠
똑똑한 챗봇 친구들,
퀴즈도 피드백도 척척!
오늘도 코드는 한 뼘 더 성장!
🥕✨

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (11)
cs25-service/src/test/java/com/example/cs25service/ai/AiQuestionGeneratorServiceTest.java (1)

52-61: 테스트 설정 개선이 우수합니다.

동적 카테고리 존재 확인과 조건부 삽입으로 테스트의 안정성이 크게 향상되었습니다:

  • 중복 엔티티 삽입 방지
  • EntityManager flush/clear로 적절한 영속성 상태 관리
  • 새로 추가된 existsByCategoryType 메서드의 효과적인 활용

테스트 설정을 더욱 개선하려면 카테고리 목록을 설정 파일이나 상수로 분리하는 것을 고려해보세요:

private static final List<String> REQUIRED_CATEGORIES = List.of(
    "SoftwareDevelopment",
    "SoftwareDesign", 
    "Programming",
    "Database",
    "InformationSystemManagement"
);
cs25-service/src/test/java/com/example/cs25service/ai/FallbackAiChatClientTest.java (1)

14-14: 테스트 메서드 이름을 더 구체적으로 작성하세요.

메서드 이름이 시나리오를 충분히 설명하지 못합니다. 더 명확하고 구체적인 이름을 사용해주세요.

-void openAiFail_thenFallbackToClaude() {
+void shouldFallbackToClaudeWhenOpenAiThrowsException() {
cs25-service/src/main/java/com/example/cs25service/domain/ai/service/AiService.java (1)

13-13: 사용하지 않는 import를 제거하세요.

ChatClient import가 더 이상 사용되지 않으므로 제거해주세요.

-import org.springframework.ai.chat.client.ChatClient;
cs25-service/src/main/java/com/example/cs25service/domain/ai/client/FallbackAiChatClient.java (1)

28-30: Circuit Breaker 패턴 도입을 고려하세요.

현재 구현은 매번 OpenAI를 먼저 시도하므로, OpenAI가 지속적으로 실패하는 상황에서 불필요한 지연이 발생할 수 있습니다. Circuit Breaker 패턴을 도입하여 일정 시간 동안은 Claude를 직접 사용하는 것을 고려해보세요.

Spring Cloud Circuit Breaker나 Resilience4j를 활용할 수 있습니다:

+@CircuitBreaker(name = "openai", fallbackMethod = "fallbackToClaude")
public String call(String systemPrompt, String userPrompt) {
-    try {
        return openAiClient.call(systemPrompt, userPrompt);
-    } catch (Exception e) {
-        log.warn("OpenAI 호출 실패. Claude로 폴백합니다.", e);
-        return claudeClient.call(systemPrompt, userPrompt);
-    }
}

+public String fallbackToclaude(String systemPrompt, String userPrompt, Exception ex) {
+    log.warn("OpenAI 호출 실패. Claude로 폴백합니다.", ex);
+    return claudeClient.call(systemPrompt, userPrompt);
+}
cs25-service/src/test/java/com/example/cs25service/ai/FallbackAiChatClientIntegrationTest.java (2)

98-98: 콘솔 출력을 제거하고 적절한 로깅 또는 어서션으로 대체하세요.

프로덕션 코드에서 System.out.println은 적절하지 않습니다. 테스트에서도 로거를 사용하거나 필요시 제거해주세요.

-System.out.println("📢 Claude 기반 피드백: " + updated.getAiFeedback());
+log.debug("Claude 기반 피드백: {}", updated.getAiFeedback());

46-89: 테스트 데이터 생성 로직을 헬퍼 메서드로 추출하세요.

테스트 설정 코드가 길어서 테스트의 핵심 로직이 가려집니다. 테스트 데이터 생성 로직을 별도 메서드로 추출하여 가독성을 향상시켜주세요.

+private UserQuizAnswer createTestUserQuizAnswer() {
+    QuizCategory category = QuizCategory.builder()
+        .categoryType("네트워크")
+        .parent(null)
+        .build();
+    em.persist(category);
+    
+    // ... 나머지 엔티티 생성 로직
+    
+    return answer;
+}

@Test
@DisplayName("OpenAI 호출 실패 시 Claude로 폴백하여 피드백 생성한다")
void openAiFail_thenUseClaudeFeedback() {
-    // given - 기본 퀴즈, 사용자, 정답 생성
-    QuizCategory category = QuizCategory.builder()...
+    // given
+    UserQuizAnswer answer = createTestUserQuizAnswer();
cs25-service/src/main/java/com/example/cs25service/domain/ai/client/ClaudeChatClient.java (1)

14-20: 입력 파라미터 검증을 추가하는 것을 고려하세요.

현재 null 체크나 빈 문자열 검증이 없습니다. 로버스트한 구현을 위해 입력 검증을 추가하는 것을 고려해보세요.

@Override
public String call(String systemPrompt, String userPrompt) {
+    if (systemPrompt == null || systemPrompt.trim().isEmpty()) {
+        throw new IllegalArgumentException("System prompt는 필수입니다.");
+    }
+    if (userPrompt == null || userPrompt.trim().isEmpty()) {
+        throw new IllegalArgumentException("User prompt는 필수입니다.");
+    }
+    
    return anthropicChatClient.prompt()
        .system(systemPrompt)
        .user(userPrompt)
        .call()
        .content();
}
cs25-service/src/main/java/com/example/cs25service/domain/ai/config/AiConfig.java (3)

36-45: Claude API 키 설정 일관성 확인 필요

Anthropic 모델 설정에서 claudeKey 파라미터를 사용하고 있지만, 프로퍼티 키는 spring.ai.anthropic.api-key입니다. 일관성을 위해 변수명을 anthropicKey로 변경하는 것을 고려하세요.

다음과 같이 수정하여 일관성을 높이세요:

    @Bean
-    public AnthropicChatModel anthropicChatModel(@Value("${spring.ai.anthropic.api-key}") String claudeKey) {
+    public AnthropicChatModel anthropicChatModel(@Value("${spring.ai.anthropic.api-key}") String anthropicKey) {
        AnthropicApi api = AnthropicApi.builder()
-            .apiKey(claudeKey)
+            .apiKey(anthropicKey)
            .build();

21-23: 메서드명의 네이밍 일관성을 개선하세요.

메서드명이 AichatClient로 되어 있는데, Java 네이밍 컨벤션에 따라 aiChatClient로 수정하는 것이 좋습니다.

-public ChatClient AichatClient(OpenAiChatModel chatModel) {
+public ChatClient aiChatClient(OpenAiChatModel chatModel) {
     return ChatClient.create(chatModel);
 }

36-45: API 키 검증 로직을 추가하는 것을 고려하세요.

Anthropic 모델 설정이 올바르게 구성되었지만, API 키가 비어있거나 유효하지 않은 경우에 대한 검증 로직을 추가하면 더 안전합니다.

 @Bean
 public AnthropicChatModel anthropicChatModel(@Value("${spring.ai.anthropic.api-key}") String claudeKey) {
+    if (claudeKey == null || claudeKey.trim().isEmpty()) {
+        throw new IllegalArgumentException("Anthropic API 키가 설정되지 않았습니다.");
+    }
+    
     AnthropicApi api = AnthropicApi.builder()
         .apiKey(claudeKey)
         .build();

     return AnthropicChatModel.builder()
         .anthropicApi(api)
         .build();
 }

OpenAI 모델 설정에도 동일한 검증을 추가하는 것을 고려해보세요.

cs25-service/src/main/java/com/example/cs25service/domain/ai/client/OpenAiChatClient.java (1)

29-32: raw() 메서드의 접근 제어를 고려하세요.

raw() 메서드가 내부 ChatClient를 직접 노출합니다. 이는 캡슐화를 위반할 수 있으며, 클라이언트가 직접 내부 구현에 의존하게 만들 수 있습니다.

사용 사례를 명확히 하고, 필요하다면 더 구체적인 메서드들을 제공하는 것을 고려해보세요:

 @Override
-public ChatClient raw() {
-    return openAiChatClient;
-}
+public ChatClient getChatClient() {
+    // 특정 사용 사례에 대한 문서화 필요
+    return openAiChatClient;
+}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9226662 and 8d2a46a.

📒 Files selected for processing (12)
  • cs25-entity/src/main/java/com/example/cs25entity/domain/quiz/repository/QuizCategoryRepository.java (1 hunks)
  • cs25-service/build.gradle (1 hunks)
  • cs25-service/src/main/java/com/example/cs25service/domain/ai/client/AiChatClient.java (1 hunks)
  • cs25-service/src/main/java/com/example/cs25service/domain/ai/client/ClaudeChatClient.java (1 hunks)
  • cs25-service/src/main/java/com/example/cs25service/domain/ai/client/FallbackAiChatClient.java (1 hunks)
  • cs25-service/src/main/java/com/example/cs25service/domain/ai/client/OpenAiChatClient.java (1 hunks)
  • cs25-service/src/main/java/com/example/cs25service/domain/ai/config/AiConfig.java (2 hunks)
  • cs25-service/src/main/java/com/example/cs25service/domain/ai/service/AiService.java (2 hunks)
  • cs25-service/src/main/resources/application.properties (1 hunks)
  • cs25-service/src/test/java/com/example/cs25service/ai/AiQuestionGeneratorServiceTest.java (1 hunks)
  • cs25-service/src/test/java/com/example/cs25service/ai/FallbackAiChatClientIntegrationTest.java (1 hunks)
  • cs25-service/src/test/java/com/example/cs25service/ai/FallbackAiChatClientTest.java (1 hunks)
🔇 Additional comments (22)
cs25-service/build.gradle (1)

27-27: 의존성 추가가 적절합니다.

Claude AI 모델 지원을 위한 Anthropic 의존성이 올바르게 추가되었으며, 기존 OpenAI 의존성과 버전이 일치하여 일관성을 유지하고 있습니다.

cs25-service/src/main/resources/application.properties (2)

54-54: 설명 주석이 더 명확해졌습니다.

AI 설정 섹션을 "OPEN AI"로 명시하여 Claude 설정과 구분이 명확해졌습니다.


59-61: Claude 모델 버전 확인이 필요합니다.

Claude 설정이 올바르게 추가되었지만, 지정된 모델 claude-3-opus-20240229이 현재 사용 가능한 최신 버전인지 확인이 필요합니다.

Anthropic Claude 3 Opus 모델의 최신 버전과 사용 가능한 모델 목록을 확인해주세요. claude-3-opus-20240229가 여전히 유효한 모델 이름인지 확인이 필요합니다.
cs25-entity/src/main/java/com/example/cs25entity/domain/quiz/repository/QuizCategoryRepository.java (1)

48-48: 리포지토리 메서드 추가가 적절합니다.

existsByCategoryType 메서드가 Spring Data JPA 명명 규칙에 따라 올바르게 추가되었으며, 테스트 설정에서 중복 카테고리 삽입을 방지하는 용도로 적절히 활용됩니다.

cs25-service/src/main/java/com/example/cs25service/domain/ai/client/AiChatClient.java (1)

5-10: AI 클라이언트 인터페이스 설계가 우수합니다.

인터페이스가 잘 설계되어 있습니다:

  • call() 메서드는 AI 호출을 위한 간단하고 직관적인 인터페이스를 제공
  • raw() 메서드는 필요시 원본 ChatClient에 접근할 수 있는 확장성 제공
  • 메서드 시그니처가 명확하고 다양한 AI 제공업체 구현체를 지원하기에 적합
cs25-service/src/test/java/com/example/cs25service/ai/AiQuestionGeneratorServiceTest.java (1)

44-50: 카테고리 타입이 영어로 변경되었습니다.

카테고리 타입이 한국어에서 영어로 변경되어 일관성이 개선되었습니다. 하지만 이 변경이 기존 데이터와 호환되는지 확인이 필요합니다.

기존 데이터베이스의 QuizCategory 테이블에 한국어 카테고리가 존재하는지, 그리고 이 변경이 기존 데이터와의 호환성에 영향을 주는지 확인하는 스크립트:

#!/bin/bash
# 기존 QuizCategory 사용을 찾아 언어 일관성 검증
echo "QuizCategory 관련 코드에서 한국어/영어 카테고리 사용 패턴 검색:"
rg -A 3 -B 3 "QuizCategory\(" --type java
echo -e "\n카테고리 타입 문자열 패턴 검색:"
rg -A 2 -B 2 "(소프트웨어|프로그래밍|데이터베이스|Software|Programming|Database)" --type java
cs25-service/src/main/java/com/example/cs25service/domain/ai/service/AiService.java (2)

21-22: 의존성 주입 방식이 개선되었습니다.

@Qualifier를 사용하여 구체적인 구현체를 지정한 것은 좋은 접근입니다. 이를 통해 여러 AiChatClient 구현체 중에서 원하는 것을 명확히 선택할 수 있습니다.


40-40: FallbackAiChatClient의 call 메서드 구현부를 제대로 확인하기 위해 특수문자를 이스케이프하거나 고정 문자열 검색 옵션을 사용하는 스크립트를 다시 실행해주세요.

#!/bin/bash
# FallbackAiChatClient 파일 경로 찾기
files=$(rg --files-with-matches "class FallbackAiChatClient" -g "*.java")
if [ -z "$files" ]; then
  echo "❌ FallbackAiChatClient 클래스를 찾을 수 없습니다."
  exit 1
fi

echo "✅ FallbackAiChatClient 파일:"
echo "$files"
echo

# 각 파일에서 public String call 선언과 그 후 30줄 출력
for file in $files; do
  echo "---- $file ----"
  rg -nF "public String call" "$file" -A 30
  echo
done
cs25-service/src/main/java/com/example/cs25service/domain/ai/client/FallbackAiChatClient.java (1)

8-11: @primary 어노테이션 사용이 적절합니다.

여러 AiChatClient 구현체 간의 빈 충돌을 해결하기 위한 @Primary 어노테이션 사용이 적절합니다. 이를 통해 기본 구현체로 폴백 클라이언트가 선택됩니다.

cs25-service/src/main/java/com/example/cs25service/domain/ai/client/ClaudeChatClient.java (1)

7-26: 구현이 일관성 있고 간결합니다.

AiChatClient 인터페이스를 올바르게 구현했으며, Spring의 빌더 패턴을 적절히 활용했습니다. 다른 클라이언트 구현체들과 일관된 패턴을 따르고 있어 좋습니다.

cs25-service/src/main/java/com/example/cs25service/domain/ai/client/OpenAiChatClient.java (5)

16-23: 메서드 구현이 올바르게 되어 있습니다

OpenAI ChatClient API를 올바르게 사용하여 시스템 프롬프트와 사용자 프롬프트를 처리하고 있습니다. 응답 내용의 공백 제거도 적절합니다.


9-11: 컴포넌트 스캔 범위 확인 필요

@Component 어노테이션이 적용된 클래스가 올바른 패키지 구조에 위치하는지 확인하세요. 도메인 레이어에 있는 클래스에 @Component를 사용하는 것이 적절한지 검토가 필요합니다.

다음 스크립트로 컴포넌트 스캔 설정을 확인하세요:

#!/bin/bash
# Description: Spring Boot 메인 클래스의 컴포넌트 스캔 설정 확인

# 메인 클래스와 @ComponentScan 설정 찾기
rg -A 5 "@SpringBootApplication|@ComponentScan" --type java

29-32: raw() 메서드의 목적 명확화 필요

raw() 메서드가 내부 ChatClient를 직접 노출하는 것은 캡슐화 원칙에 어긋날 수 있습니다. 이 메서드의 사용 목적과 필요성을 재검토하세요.

다음 스크립트로 raw() 메서드의 사용처를 확인하세요:

#!/bin/bash
# Description: raw() 메서드 사용처 확인

# raw() 메서드 호출 찾기
rg -A 3 "\.raw\(\)" --type java

1-8: 패키지 구조와 임포트가 적절합니다.

도메인 주도 설계 원칙을 따른 패키지 구조가 잘 정의되어 있고, 필요한 의존성들이 적절하게 임포트되어 있습니다.


9-13: 컴포넌트 설정이 올바릅니다.

Spring의 @Component와 Lombok의 @RequiredArgsConstructor를 적절하게 사용하여 의존성 주입을 구현했습니다.

cs25-service/src/main/java/com/example/cs25service/domain/ai/config/AiConfig.java (7)

1-1: 패키지 구조 변경이 적절합니다

AI 관련 설정을 도메인별로 분리한 패키지 구조 변경이 좋습니다. 관심사 분리 원칙에 부합합니다.


25-34: OpenAI 모델 빈 설정이 올바릅니다

OpenAI API 키를 사용하여 ChatModel을 적절히 구성하고 있습니다. 빌더 패턴 사용도 적절합니다.


17-18: ```shell
#!/bin/bash

application.properties에서 spring.ai.openai.api-key 설정 확인

grep -R "spring.ai.openai.api-key" -n cs25-service/src/main/resources/application.properties


---

`1-1`: **패키지 재구성이 적절합니다.**

도메인 주도 설계 원칙에 따라 AI 관련 설정을 `domain.ai.config` 패키지로 이동한 것이 좋습니다.

---

`3-4`: **Anthropic 지원을 위한 임포트가 추가되었습니다.**

Claude 모델 지원을 위한 필요한 임포트들이 적절하게 추가되었습니다.

---

`25-34`: **OpenAI 모델 설정이 적절합니다.**

빌더 패턴을 사용하여 OpenAI 모델을 올바르게 구성했습니다. API 키 주입도 적절하게 처리되었습니다.

---

`25-45`: 아래 스크립트를 실행하여 `FallbackAiChatClient` 클래스 파일별로 `@Primary` 어노테이션 존재 여부를 정확히 확인해 주세요:


```shell
#!/bin/bash
# 1) FallbackAiChatClient 클래스가 정의된 모든 파일 목록 조회
files=$(rg -l "class FallbackAiChatClient")
if [ -z "$files" ]; then
  echo "FallbackAiChatClient 클래스 파일을 찾을 수 없습니다."
  exit 1
fi

# 2) 각 파일에서 @Primary 어노테이션 여부 확인
for file in $files; do
  echo "---- 검사 파일: $file ----"
  rg -n "@Primary" -C2 "$file" || echo "-> @Primary 어노테이션이 없습니다."
done

Comment on lines +13 to +36
@Test
void openAiFail_thenFallbackToClaude() {
// given
OpenAiChatClient openAiMock = mock(OpenAiChatClient.class);
ClaudeChatClient claudeMock = mock(ClaudeChatClient.class);

// OpenAI는 실패하도록 설정
when(openAiMock.call(anyString(), anyString()))
.thenThrow(new RuntimeException("OpenAI failure"));

// Claude는 정상 반환
when(claudeMock.call(anyString(), anyString()))
.thenReturn("Claude 응답입니다.");

FallbackAiChatClient fallbackClient = new FallbackAiChatClient(openAiMock, claudeMock);

// when
String result = fallbackClient.call("시스템 프롬프트", "유저 프롬프트");

// then
assertThat(result).isEqualTo("Claude 응답입니다.");
verify(openAiMock, times(1)).call(anyString(), anyString());
verify(claudeMock, times(1)).call(anyString(), anyString());
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

추가 테스트 시나리오 구현을 권장합니다.

현재 테스트는 OpenAI 실패 시 Claude로 폴백하는 경우만 검증하고 있습니다. 더 포괄적인 테스트 커버리지를 위해 다음 시나리오들을 추가해주세요:

  1. OpenAI 성공 시 Claude가 호출되지 않는 경우
  2. 두 클라이언트 모두 실패하는 경우
  3. Claude 폴백도 실패하는 경우
@Test
void openAiSuccess_thenClaudeNotCalled() {
    // given
    OpenAiChatClient openAiMock = mock(OpenAiChatClient.class);
    ClaudeChatClient claudeMock = mock(ClaudeChatClient.class);
    
    when(openAiMock.call(anyString(), anyString()))
        .thenReturn("OpenAI 응답입니다.");
    
    FallbackAiChatClient fallbackClient = new FallbackAiChatClient(openAiMock, claudeMock);
    
    // when
    String result = fallbackClient.call("시스템 프롬프트", "유저 프롬프트");
    
    // then
    assertThat(result).isEqualTo("OpenAI 응답입니다.");
    verify(openAiMock, times(1)).call(anyString(), anyString());
    verify(claudeMock, never()).call(anyString(), anyString());
}

@Test
void bothClientsFail_thenExceptionThrown() {
    // given
    OpenAiChatClient openAiMock = mock(OpenAiChatClient.class);
    ClaudeChatClient claudeMock = mock(ClaudeChatClient.class);
    
    when(openAiMock.call(anyString(), anyString()))
        .thenThrow(new RuntimeException("OpenAI failure"));
    when(claudeMock.call(anyString(), anyString()))
        .thenThrow(new RuntimeException("Claude failure"));
    
    FallbackAiChatClient fallbackClient = new FallbackAiChatClient(openAiMock, claudeMock);
    
    // when & then
    assertThatThrownBy(() -> fallbackClient.call("시스템 프롬프트", "유저 프롬프트"))
        .isInstanceOf(RuntimeException.class)
        .hasMessage("Claude failure");
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Test
void openAiFail_thenFallbackToClaude() {
// given
OpenAiChatClient openAiMock = mock(OpenAiChatClient.class);
ClaudeChatClient claudeMock = mock(ClaudeChatClient.class);
// OpenAI는 실패하도록 설정
when(openAiMock.call(anyString(), anyString()))
.thenThrow(new RuntimeException("OpenAI failure"));
// Claude는 정상 반환
when(claudeMock.call(anyString(), anyString()))
.thenReturn("Claude 응답입니다.");
FallbackAiChatClient fallbackClient = new FallbackAiChatClient(openAiMock, claudeMock);
// when
String result = fallbackClient.call("시스템 프롬프트", "유저 프롬프트");
// then
assertThat(result).isEqualTo("Claude 응답입니다.");
verify(openAiMock, times(1)).call(anyString(), anyString());
verify(claudeMock, times(1)).call(anyString(), anyString());
}
@Test
void openAiFail_thenFallbackToClaude() {
// given
OpenAiChatClient openAiMock = mock(OpenAiChatClient.class);
ClaudeChatClient claudeMock = mock(ClaudeChatClient.class);
// OpenAI는 실패하도록 설정
when(openAiMock.call(anyString(), anyString()))
.thenThrow(new RuntimeException("OpenAI failure"));
// Claude는 정상 반환
when(claudeMock.call(anyString(), anyString()))
.thenReturn("Claude 응답입니다.");
FallbackAiChatClient fallbackClient = new FallbackAiChatClient(openAiMock, claudeMock);
// when
String result = fallbackClient.call("시스템 프롬프트", "유저 프롬프트");
// then
assertThat(result).isEqualTo("Claude 응답입니다.");
verify(openAiMock, times(1)).call(anyString(), anyString());
verify(claudeMock, times(1)).call(anyString(), anyString());
}
@Test
void openAiSuccess_thenClaudeNotCalled() {
// given
OpenAiChatClient openAiMock = mock(OpenAiChatClient.class);
ClaudeChatClient claudeMock = mock(ClaudeChatClient.class);
when(openAiMock.call(anyString(), anyString()))
.thenReturn("OpenAI 응답입니다.");
FallbackAiChatClient fallbackClient = new FallbackAiChatClient(openAiMock, claudeMock);
// when
String result = fallbackClient.call("시스템 프롬프트", "유저 프롬프트");
// then
assertThat(result).isEqualTo("OpenAI 응답입니다.");
verify(openAiMock, times(1)).call(anyString(), anyString());
verify(claudeMock, never()).call(anyString(), anyString());
}
@Test
void bothClientsFail_thenExceptionThrown() {
// given
OpenAiChatClient openAiMock = mock(OpenAiChatClient.class);
ClaudeChatClient claudeMock = mock(ClaudeChatClient.class);
when(openAiMock.call(anyString(), anyString()))
.thenThrow(new RuntimeException("OpenAI failure"));
when(claudeMock.call(anyString(), anyString()))
.thenThrow(new RuntimeException("Claude failure"));
FallbackAiChatClient fallbackClient = new FallbackAiChatClient(openAiMock, claudeMock);
// when & then
assertThatThrownBy(() -> fallbackClient.call("시스템 프롬프트", "유저 프롬프트"))
.isInstanceOf(RuntimeException.class)
.hasMessage("Claude failure");
}
🤖 Prompt for AI Agents
In
cs25-service/src/test/java/com/example/cs25service/ai/FallbackAiChatClientTest.java
around lines 13 to 36, the current test only covers the scenario where OpenAI
fails and the fallback to Claude succeeds. Add additional test methods to cover
these cases: 1) OpenAI succeeds and Claude is not called, 2) both OpenAI and
Claude clients fail, and 3) the fallback to Claude also fails. For each, mock
the clients accordingly, invoke the fallback client, and assert the expected
behavior or exceptions.

Comment on lines +21 to +24
} catch (Exception e) {
log.warn("OpenAI 호출 실패. Claude로 폴백합니다.", e);
return claudeClient.call(systemPrompt, userPrompt);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

예외 처리 범위를 구체화하세요.

모든 Exception을 잡아서 폴백하는 것은 너무 광범위할 수 있습니다. 특정 AI 관련 예외만 폴백 대상으로 하는 것을 고려해보세요.

-} catch (Exception e) {
+} catch (RuntimeException | java.io.IOException e) {
     log.warn("OpenAI 호출 실패. Claude로 폴백합니다.", e);
     return claudeClient.call(systemPrompt, userPrompt);

또는 AI 서비스 특화 예외를 정의하여 사용하는 것도 좋은 방법입니다.

🤖 Prompt for AI Agents
In
cs25-service/src/main/java/com/example/cs25service/domain/ai/client/FallbackAiChatClient.java
around lines 21 to 24, the catch block currently catches all Exceptions which is
too broad. Narrow the exception handling to catch only specific AI-related
exceptions that should trigger the fallback to Claude. Consider defining and
using a custom AI service exception class to encapsulate these specific errors
and catch that instead of the generic Exception.

Comment on lines +44 to +99
void openAiFail_thenUseClaudeFeedback() {
// given - 기본 퀴즈, 사용자, 정답 생성
QuizCategory category = QuizCategory.builder()
.categoryType("네트워크")
.parent(null)
.build();
em.persist(category);

Quiz quiz = Quiz.builder()
.type(QuizFormatType.SUBJECTIVE)
.question("HTTP와 HTTPS의 차이를 설명하시오.")
.answer("HTTPS는 보안이 강화된 프로토콜이다.")
.commentary("HTTPS는 SSL/TLS를 통해 데이터 암호화를 제공한다.")
.category(category)
.level(QuizLevel.NORMAL)
.build();
em.persist(quiz);

Subscription subscription = Subscription.builder()
.category(category)
.email("fallback@test.com")
.startDate(LocalDate.now().minusDays(1))
.endDate(LocalDate.now().plusDays(30))
.subscriptionType(Set.of())
.build();
em.persist(subscription);

User user = User.builder()
.email("fallback@test.com")
.name("fallback_user")
.socialType(SocialType.KAKAO)
.role(Role.USER)
.subscription(subscription)
.build();
em.persist(user);

UserQuizAnswer answer = UserQuizAnswer.builder()
.user(user)
.quiz(quiz)
.userAnswer("HTTPS는 HTTP보다 빠르다.")
.aiFeedback(null)
.isCorrect(null)
.subscription(subscription)
.build();
em.persist(answer);

// when - AI 피드백 호출
var response = aiService.getFeedback(answer.getId());

// then - Claude로부터 받은 피드백이 저장됨
UserQuizAnswer updated = userQuizAnswerRepository.findById(answer.getId()).orElseThrow();

assertThat(updated.getAiFeedback()).isNotBlank();
assertThat(updated.getIsCorrect()).isNotNull();
System.out.println("📢 Claude 기반 피드백: " + updated.getAiFeedback());
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

폴백 동작을 명시적으로 검증하는 로직을 추가하세요.

현재 테스트는 AI 피드백이 생성되는지만 확인하지만, 실제로 폴백이 발생했는지는 검증하지 않습니다. 테스트 이름에서 약속한 동작을 명시적으로 검증해보세요.

테스트를 더 구체적으로 만들기 위해 다음 방법들을 고려해보세요:

  1. Mock을 사용하여 OpenAI 클라이언트가 실패하도록 강제
  2. 로그 캡처를 통해 폴백 로그가 출력되는지 확인
  3. 응답 내용이 Claude 특성을 가지는지 검증
+@MockBean
+@Qualifier("openAiChatClient")
+private OpenAiChatClient openAiClient;

// 테스트 메서드 내에서
+when(openAiClient.call(anyString(), anyString()))
+    .thenThrow(new RuntimeException("OpenAI 서비스 점검 중"));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void openAiFail_thenUseClaudeFeedback() {
// given - 기본 퀴즈, 사용자, 정답 생성
QuizCategory category = QuizCategory.builder()
.categoryType("네트워크")
.parent(null)
.build();
em.persist(category);
Quiz quiz = Quiz.builder()
.type(QuizFormatType.SUBJECTIVE)
.question("HTTP와 HTTPS의 차이를 설명하시오.")
.answer("HTTPS는 보안이 강화된 프로토콜이다.")
.commentary("HTTPS는 SSL/TLS를 통해 데이터 암호화를 제공한다.")
.category(category)
.level(QuizLevel.NORMAL)
.build();
em.persist(quiz);
Subscription subscription = Subscription.builder()
.category(category)
.email("fallback@test.com")
.startDate(LocalDate.now().minusDays(1))
.endDate(LocalDate.now().plusDays(30))
.subscriptionType(Set.of())
.build();
em.persist(subscription);
User user = User.builder()
.email("fallback@test.com")
.name("fallback_user")
.socialType(SocialType.KAKAO)
.role(Role.USER)
.subscription(subscription)
.build();
em.persist(user);
UserQuizAnswer answer = UserQuizAnswer.builder()
.user(user)
.quiz(quiz)
.userAnswer("HTTPS는 HTTP보다 빠르다.")
.aiFeedback(null)
.isCorrect(null)
.subscription(subscription)
.build();
em.persist(answer);
// when - AI 피드백 호출
var response = aiService.getFeedback(answer.getId());
// then - Claude로부터 받은 피드백이 저장됨
UserQuizAnswer updated = userQuizAnswerRepository.findById(answer.getId()).orElseThrow();
assertThat(updated.getAiFeedback()).isNotBlank();
assertThat(updated.getIsCorrect()).isNotNull();
System.out.println("📢 Claude 기반 피드백: " + updated.getAiFeedback());
}
// --- add to your test class (e.g. above existing @Autowired fields) ---
@MockBean
@Qualifier("openAiChatClient")
private OpenAiChatClient openAiClient;
// --- then update the test method itself ---
@Test
void openAiFail_thenUseClaudeFeedback() {
// given - 기본 퀴즈, 사용자, 정답 생성
QuizCategory category = QuizCategory.builder()
.categoryType("네트워크")
.parent(null)
.build();
em.persist(category);
Quiz quiz = Quiz.builder()
.type(QuizFormatType.SUBJECTIVE)
.question("HTTP와 HTTPS의 차이를 설명하시오.")
.answer("HTTPS는 보안이 강화된 프로토콜이다.")
.commentary("HTTPS는 SSL/TLS를 통해 데이터 암호화를 제공한다.")
.category(category)
.level(QuizLevel.NORMAL)
.build();
em.persist(quiz);
Subscription subscription = Subscription.builder()
.category(category)
.email("fallback@test.com")
.startDate(LocalDate.now().minusDays(1))
.endDate(LocalDate.now().plusDays(30))
.subscriptionType(Set.of())
.build();
em.persist(subscription);
User user = User.builder()
.email("fallback@test.com")
.name("fallback_user")
.socialType(SocialType.KAKAO)
.role(Role.USER)
.subscription(subscription)
.build();
em.persist(user);
UserQuizAnswer answer = UserQuizAnswer.builder()
.user(user)
.quiz(quiz)
.userAnswer("HTTPS는 HTTP보다 빠르다.")
.aiFeedback(null)
.isCorrect(null)
.subscription(subscription)
.build();
em.persist(answer);
// Force OpenAI client to fail so that fallback to Claude is exercised
when(openAiClient.call(anyString(), anyString()))
.thenThrow(new RuntimeException("OpenAI 서비스 점검 중"));
// when - AI 피드백 호출
var response = aiService.getFeedback(answer.getId());
// then - Claude로부터 받은 피드백이 저장됨
UserQuizAnswer updated = userQuizAnswerRepository.findById(answer.getId()).orElseThrow();
assertThat(updated.getAiFeedback()).isNotBlank();
assertThat(updated.getIsCorrect()).isNotNull();
System.out.println("📢 Claude 기반 피드백: " + updated.getAiFeedback());
}
🤖 Prompt for AI Agents
In
cs25-service/src/test/java/com/example/cs25service/ai/FallbackAiChatClientIntegrationTest.java
between lines 44 and 99, the test method openAiFail_thenUseClaudeFeedback does
not explicitly verify that the fallback to Claude AI actually occurred. To fix
this, modify the test to mock the OpenAI client to simulate a failure, then
assert that the fallback logic is triggered by checking for specific
fallback-related logs or flags. Additionally, verify that the AI feedback
response contains characteristics unique to Claude to confirm the fallback was
used. This will ensure the test explicitly validates the fallback behavior as
intended.

Comment on lines +24 to +26
} catch (Exception e) {
throw new AiException(AiExceptionCode.INTERNAL_SERVER_ERROR);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

예외 처리에서 원본 예외 정보 손실

모든 예외를 AiException으로 변환하면서 원본 예외 정보가 손실됩니다. 디버깅과 로깅을 위해 원본 예외를 포함하는 것을 고려하세요.

다음과 같이 수정하여 원본 예외 정보를 보존하세요:

        } catch (Exception e) {
-            throw new AiException(AiExceptionCode.INTERNAL_SERVER_ERROR);
+            throw new AiException(AiExceptionCode.INTERNAL_SERVER_ERROR, e);
        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (Exception e) {
throw new AiException(AiExceptionCode.INTERNAL_SERVER_ERROR);
}
} catch (Exception e) {
throw new AiException(AiExceptionCode.INTERNAL_SERVER_ERROR, e);
}
🤖 Prompt for AI Agents
In
cs25-service/src/main/java/com/example/cs25service/domain/ai/client/OpenAiChatClient.java
around lines 24 to 26, the catch block throws a new AiException without
preserving the original exception, causing loss of the root cause information.
Modify the throw statement to include the caught exception as the cause in the
AiException constructor or as a parameter, so the original exception details are
retained for debugging and logging purposes.

Comment on lines +21 to 23
public ChatClient AichatClient(OpenAiChatModel chatModel) {
return ChatClient.create(chatModel);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

메서드 이름 규칙 불일치

AichatClient 메서드명이 Java 네이밍 컨벤션에 맞지 않습니다. 메서드명은 소문자로 시작해야 합니다.

다음과 같이 수정하세요:

    @Bean
-    public ChatClient AichatClient(OpenAiChatModel chatModel) {
+    public ChatClient aiChatClient(OpenAiChatModel chatModel) {
        return ChatClient.create(chatModel);
    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public ChatClient AichatClient(OpenAiChatModel chatModel) {
return ChatClient.create(chatModel);
}
@Bean
public ChatClient aiChatClient(OpenAiChatModel chatModel) {
return ChatClient.create(chatModel);
}
🤖 Prompt for AI Agents
In
cs25-service/src/main/java/com/example/cs25service/domain/ai/config/AiConfig.java
around lines 21 to 23, the method name `AichatClient` does not follow Java
naming conventions as it starts with an uppercase letter. Rename the method to
start with a lowercase letter, for example, change `AichatClient` to
`aichatClient`.

Comment on lines +15 to +27
@Override
public String call(String systemPrompt, String userPrompt) {
try {
return openAiChatClient.prompt()
.system(systemPrompt)
.user(userPrompt)
.call()
.content()
.trim();
} catch (Exception e) {
throw new AiException(AiExceptionCode.INTERNAL_SERVER_ERROR);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

예외 처리를 개선하고 로깅을 추가하세요.

현재 모든 예외를 AiException으로 변환하고 있지만, 원본 예외 정보가 손실됩니다. 디버깅과 모니터링을 위해 로깅을 추가하는 것을 권장합니다.

 @Override
 public String call(String systemPrompt, String userPrompt) {
     try {
         return openAiChatClient.prompt()
             .system(systemPrompt)
             .user(userPrompt)
             .call()
             .content()
             .trim();
     } catch (Exception e) {
+        log.error("OpenAI 호출 실패: {}", e.getMessage(), e);
         throw new AiException(AiExceptionCode.INTERNAL_SERVER_ERROR);
     }
 }

추가로 로거 필드를 클래스에 추가해야 합니다:

+import lombok.extern.slf4j.Slf4j;

+@Slf4j
 @Component
 @RequiredArgsConstructor
 public class OpenAiChatClient implements AiChatClient {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
public String call(String systemPrompt, String userPrompt) {
try {
return openAiChatClient.prompt()
.system(systemPrompt)
.user(userPrompt)
.call()
.content()
.trim();
} catch (Exception e) {
throw new AiException(AiExceptionCode.INTERNAL_SERVER_ERROR);
}
}
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component
@RequiredArgsConstructor
public class OpenAiChatClient implements AiChatClient {
@Override
public String call(String systemPrompt, String userPrompt) {
try {
return openAiChatClient.prompt()
.system(systemPrompt)
.user(userPrompt)
.call()
.content()
.trim();
} catch (Exception e) {
log.error("OpenAI 호출 실패: {}", e.getMessage(), e);
throw new AiException(AiExceptionCode.INTERNAL_SERVER_ERROR);
}
}
}
🤖 Prompt for AI Agents
In
cs25-service/src/main/java/com/example/cs25service/domain/ai/client/OpenAiChatClient.java
around lines 15 to 27, the current exception handling catches all exceptions and
throws a new AiException without preserving the original exception details or
logging the error. To fix this, add a logger field to the class, log the caught
exception with an appropriate error message inside the catch block, and pass the
original exception as the cause when throwing the new AiException to retain the
stack trace for debugging.

@HeeMang-Lee HeeMang-Lee changed the title Feat/124 Feat/124 OpenAi 호출 실패 시 Claude Api 호출하는 Fallback구조 도입 Jun 22, 2025
Copy link
Copy Markdown
Collaborator

@Kimyoonbeom Kimyoonbeom left a comment

Choose a reason for hiding this comment

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

수고하셨습니다

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.

Claude 도입 설정 및 리팩토링

3 participants