Skip to content

feat: AI 로직 개선 및 서킷 브레이커를 통한 LLM API 이중화 구현#180

Merged
yooooonshine merged 9 commits intodevelopfrom
feature/178-implement-gpt-duplexing
Sep 22, 2025
Merged

feat: AI 로직 개선 및 서킷 브레이커를 통한 LLM API 이중화 구현#180
yooooonshine merged 9 commits intodevelopfrom
feature/178-implement-gpt-duplexing

Conversation

@yooooonshine
Copy link
Contributor

@yooooonshine yooooonshine commented Sep 22, 2025

개요

LLM 서비스의 안정성과 가용성을 향상시키기 위해 GPT와 Claude API의 이중화를 구현하고, 서킷 브레이커 패턴을 통해 장애 상황에서의 자동 페일오버를 지원하게 했습니다.

구현 내용

LLM API 이중화 시스템

문제 상황

GPT API를 활용하여 채팅을 하였으나, 종종 GPT에 장애가 발생하여 먹통이 되는 문제가 발생했다.

GPT와의 채팅을 통한 이미지 생성 기능이 핵심 기능이라 장애가 나면 서비스에 큰 지장이 생기므로 이를 보완할 필요가 생겼다.

이에 대한 해결책을 알아보는 도중
https://kdh1226.tistory.com/33
해당 글을 통해 Circuit Breaker를 활용한 이중화 방식을 알게 되었다.

이를 통해 서킷 브레이커를 도입하고자 하였으며,
다만 장애상황을 어떻게 판단할지가 관건이었다.

장애상황은 어떻게 판단해야할까?

GPT는 요청에 따라 응답시간이 제각각이라 타임아웃 판단이 어려웠다.

따라서 타임아웃을 제외한 에러를 처리하고자 했다.

이에 따라

  • HTTP 상태 코드
  • 네트워크 장애
  • API 내부 에러
    등을 고려했다.

GPT 에러코드를 찾아본 결과

  • 503
    • 서버 과부하, GPT 트래픽 증가
  • 429
    • 사용 한도 초과
  • 502
    • Bad Gateway로 서버 간 통신 오류, 해결 방법이 제한적.
  • 500
    • Open AI 오류
      등의 에러코드가 존재했다.
      기타 통신 자체가 안되어 네트워크 장애가 발생할 수도 있었다.

이에 따라
CustomException과 IOException이 일정 이상 발생하면 CircuitBreaker가 동작되게 하였다.

적용

image

최근 10번 중 5번 이상 실패하면 서킷 브레이커가 동작하게 하였다.
그리고 특성 상 GPT부하가 있으면 장기간 유지되므로 1시간 텀을 둬서 Half-Open으로 전환되게 했다.

image

테스트를 위해 에러가 1개 났을 때 Circuit Breaker가 동작하게 하였다.
image

로그를 확인해보니 정상적으로 Circuit Breaker가 동작했다.

결과

  • 장애 감지: 50% 실패율 달성 시 서킷 오픈
  • 자동 페일오버: Primary 실패 시 Secondary LLM으로 자동 전환
  • 복구 메커니즘: 1시간 후 Half-Open 상태로 전환하여 복구 시도

기타 변경 사항

  • New Features
    • GPT 호출 장애 시 Claude로 자동 대체 및 회로 차단기 도입으로 AI 챗 안정성 향상
    • AI 요청 시 비동기처리를 통해 응답시간 개선
    • AI 서버 응답 형식 변경에 따라, 리스너 로직을 수정
  • Bug Fixes
    • 비어있는 채팅(텍스트·이미지 모두 없음) 요청에 대한 명확한 오류 처리 추가
  • Performance
    • AI 요청 비동기 스레드풀과 이미지 처리 비동기 풀 스레드풀 분리
  • Chores
    • 불필요한 의존성/임포트 정리 및 DTO 구조 정돈

Summary by CodeRabbit

  • 신기능
    • GPT 장애 시 Claude로 자동 대체
    • 이미지 미생성 시 텍스트 답변으로 응답 전송
  • 개선
    • 회로 차단기 도입으로 API 안정성 향상
    • 작업용 실행기 분리(일반/이미지/GPT)로 처리량 및 응답성 개선
    • SSE 알림 신뢰성 및 이미지 결과/메타데이터 전달 일관성 강화
  • 버그 수정
    • 채팅 텍스트와 이미지가 모두 비어있는 요청을 차단하고 명확한 오류 메시지 제공
  • 기타
    • 관련 라이브러리 추가 및 불필요한 코드 정리

- AI 서버가 필요로 하는 정보와 응답 내용이 변경되었다. 응답은 순수 채팅만 올 수 있어 이에 맞게 수정하였고, 응답에서 원본 이미지 여부를 추가했다.
- SSE 요청을 보낼 때 DTO와 DLQ 처리할 떄 DTO가 다른 문제 수정
- LLM 이중화를 위해 API사를 두개로 확장했다.
- 서킷 브레이커를 사용해 API에서 장애가 발생하면 대체 API를 호출하게 하였다.
- LLM 요청이 길어 이를 비동기로 끊어냈다. 이에 따라 비동기 스레드 풀을 변경하였다.
@yooooonshine yooooonshine linked an issue Sep 22, 2025 that may be closed by this pull request
@coderabbitai
Copy link

coderabbitai bot commented Sep 22, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

LLM 호출을 프로바이더화하고 CircuitBreaker(Resilience4j)와 Claude 폴백을 추가했으며, 이미지 요청/응답 DTO를 재구성하고 비동기 실행기(이미지·GPT 전용) 및 에러코드·속성 변경을 적용했습니다. 리스너는 이미지 생성 성공/실패로 분기해 저장·SSE 전송 경로를 분리합니다.

Changes

Cohort / File(s) Summary
빌드/의존성
build.gradle
resilience4j-spring-boot3:2.2.0, resilience4j-circuitbreaker:2.2.0 추가.
비동기 실행기 구성
src/.../common/async/AsyncConfig.java, src/.../domain/image/util/S3ImageUtil.java
imageTaskExecutor, gptTaskExecutor 빈 추가 및 기존 async 큐 용량 축소(1,000,000 → 2000). S3 삭제 메서드에 @Async("imageTaskExecutor") 지정.
Resilience4j 설정
src/.../common/config/Resilience4jConfig.java
CircuitBreakerRegistry 빈 추가(설정: failureRateThreshold=50, slidingWindowSize=10, minimumNumberOfCalls=10, waitDurationInOpenState=1h, 특정 예외/결과 기록).
에러코드 변경
src/.../common/error/ErrorCode.java
EMPTY_CHAT_MESSAGE_EXCEPTION(BAD_REQUEST, AC-002) 추가.
속성(Claude)
src/.../common/property/property/AiProperties.java
Claude 내부 클래스 추가 및 private Claude claude = new Claude(); 필드 추가(apiKey, model).
검증·클린업
src/.../ai/aiChat/dto/ChatMessageRequest.java, src/.../common/amqp/service/MessageQueueService.java, src/.../aiServer/listener/DlqListener.java
@NotBlank 제거(텍스트), 사용하지 않는 imports 제거 등 정리.
요청 검증 강화
src/.../ai/aiChat/service/AiChatMessageService.java
텍스트와 이미지가 모두 비어있으면 EMPTY_CHAT_MESSAGE_EXCEPTION 던지도록 validateRequestMessageNotEmpty 추가.
DTO 재편 (이미지 중심)
src/.../aiServer/dto/AiChatRequestDto.java (삭제), src/.../aiServer/dto/ImageResultResponse.java (삭제), src/.../aiServer/dto/AiImageRequestMessageDto.java, src/.../aiServer/dto/AiImageResponseMessageDto.java
채팅용 DTO 삭제, 이미지 요청/응답 DTO를 프롬프트·대화·이미지 경로·isImageGenerated 기반 분기 등으로 확장.
LLM DTO 추가/이동
src/.../dto/llm/claude/*, src/.../dto/llm/gpt/*
Claude용 ClaudeRequest, ClaudeResponse, ClaudeMessage 추가. 기존 GPT DTO 패키지를 ...dto.llm.gpt로 이동.
LLM 프로바이더 계층 추가
src/.../aiServer/service/llm/AiProvider.java, .../GptProvider.java, .../ClaudeProvider.java
AiProvider 인터페이스 도입 및 GptProvider/ClaudeProvider 구현 추가(WebClient 호출·오류 매핑·파싱).
AiChatService 리팩터링
src/.../aiServer/service/AiChatService.java
생성자/주입 대상 변경(GptProvider, ClaudeProvider, CircuitBreakerRegistry), CircuitBreaker로 GPT 호출 래핑, 실패 시 Claude로 폴백, 상태 전이 로깅 추가.
AiServerService & Listener 변경
src/.../aiServer/service/AiServerService.java, src/.../aiServer/listener/AiImageCreatedListener.java
요청/응답 DTO 교체(AiImageRequestMessageDto/AiImageResponseMessageDto), SSE 전송을 AiResponseSseService로 통일, 리스너에 이미지 생성 성공/실패 분기 및 엔티티 필드 확장, 비동기 처리(@Async) 및 실패 상태 업데이트.
기타 정리
여러 파일
사용하지 않는 import 제거 등 경미한 정리 작업.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant S as AiChatService
  participant CB as CircuitBreaker
  participant G as GptProvider
  participant A as ClaudeProvider

  C->>S: prompt(systemPrompt, userPrompt)
  activate S
  S->>CB: 요청(데코레이트된 Supplier)
  CB->>G: GptProvider.prompt()
  alt GPT 성공
    G-->>CB: 응답 텍스트
    CB-->>S: 정상 결과
    S-->>C: 텍스트 반환
  else GPT 실패/차단
    CB-->>S: 예외
    S->>A: ClaudeProvider.prompt() (폴백)
    A-->>S: 폴백 텍스트
    S-->>C: 폴백 텍스트 반환
  end
  deactivate S
Loading
sequenceDiagram
  autonumber
  participant MQ as MessageQueue
  participant L as AiImageCreatedListener
  participant R as Repository
  participant SSE as AiResponseSseService

  MQ-->>L: AiImageResponseMessageDto
  activate L
  L->>R: 메시지/방 조회
  alt message.isImageGenerated == true
    L->>R: AiChatImageEntity 저장 (imageName, extension, fromStyleImage 등)
    L->>R: AiChatMessageEntity 저장(이미지 포함)
    L->>SSE: 성공 SSE 전송 (ChatMessageResponse with imageUrl)
  else 이미지 미생성
    L->>R: 실패 응답 메시지 저장 (textContext 저장, image 없음)
    L->>SSE: 실패 SSE 전송 (ChatMessageResponse without image)
  end
  deactivate L
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

나는 토끼, 회로를 살금살금 건너요 🐇
GPT가 지치면 Claude로 살짝 폴백,
이미지 오면 저장하고 SSE로 깡충—
못 오면 텍스트로 위로 한 줄,
스레드 풀은 분주히, 큐는 조용히.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed 제목은 PR의 핵심 변경사항(LLM API 이중화(GPT/Claude), Resilience4j 기반 서킷 브레이커 도입, 비동기 처리 및 AI 로직 개선)을 간결하게 요약하고 있어 한눈에 의도를 파악하기에 적절합니다. 문구가 명확하고 불필요한 잡음(파일 목록·이모지 등)이 없어 히스토리 스캔 시 유용합니다.
Description Check ✅ Passed PR 설명은 개요, 문제 정의, 적용 방법, 결과 및 기타 변경 사항을 상세히 기술해 구현 의도와 테스트 결과를 잘 전달하고 있어 전반적으로 완성도가 높습니다. 다만 저장소의 템플릿이 요구하는 섹션명("## 작업사항")과 본문에서 사용한 헤더("구현 내용", "기타 변경 사항")가 정확히 일치하지 않아 템플릿 검증 기준에는 일부 미흡합니다.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9ad4eb5 and 43cd412.

📒 Files selected for processing (4)
  • src/main/java/hanium/modic/backend/common/async/AsyncConfig.java (3 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/listener/AiImageCreatedListener.java (5 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/service/llm/ClaudeProvider.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/service/llm/GptProvider.java (1 hunks)

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

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@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: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/main/java/hanium/modic/backend/domain/ai/aiServer/listener/AiImageCreatedListener.java (1)

98-108: 응답 메시지의 senderType 오설정(USER).

AI가 생성한 응답인데 SenderType.USER로 저장 중입니다. UI/로직 혼선을 초래합니다. SenderType.AI로 수정하세요.

-			.senderType(SenderType.USER)
+			.senderType(SenderType.AI)
src/main/java/hanium/modic/backend/domain/ai/aiServer/service/AiServerService.java (3)

100-102: LLM 분류 결과 신뢰 문제: valueOf 직호출 취약.

모델이 공백/따옴표/소문자/설명 텍스트를 섞어 반환하면 IllegalArgumentException로 크래시합니다. 정규화/화이트리스트 후 파싱하세요.

-		String category = aiChatService.prompt(systemPrompt, message);
-		return RequestCategory.valueOf(category);
+		String category = aiChatService.prompt(systemPrompt, message);
+		String normalized = category == null ? "" : category.trim().replaceAll("^[\"']|[\"']$", "").toUpperCase();
+		if (!"IMAGE_GENERATION".equals(normalized) && !"CHAT_GENERATION".equals(normalized)) {
+			log.warn("Unexpected classification result: '{}', fallback to CHAT_GENERATION", category);
+			normalized = "CHAT_GENERATION";
+		}
+		return RequestCategory.valueOf(normalized);

184-188: 대표 이미지 조회 시 빈 목록 처리 누락.

findAllByPostId(postId).get(0)는 빈 결과에서 IndexOutOfBoundsException 발생합니다. 예외 처리/대체 경로가 필요합니다.

-		PostImageEntity postImage = postImageEntityRepository.findAllByPostId(postId).get(0);
+		List<PostImageEntity> images = postImageEntityRepository.findAllByPostId(postId);
+		if (images.isEmpty()) {
+			throw new AppException(ErrorCode.POST_IMAGE_NOT_FOUND);
+		}
+		PostImageEntity postImage = images.get(0);

248-264: AI 컨텍스트 메시지의 시간 순서 보장 필요.

findTopNBy...OrderByMessageOrderDesc가 최신→과거 순이면 LLM에 역순 컨텍스트가 전달됩니다. 응답 품질 저하 우려가 있어 정순으로 정렬해 전달하세요.

-			return aiChatMessageRepository
-				.findTopNByUserIdAndPostIdOrderByMessageOrderDesc(userId, postId, pageable);
+			List<AiChatMessageEntity> list = aiChatMessageRepository
+				.findTopNByUserIdAndPostIdOrderByMessageOrderDesc(userId, postId, pageable);
+			java.util.Collections.reverse(list);
+			return list;

추가 import:

import java.util.Collections;
🧹 Nitpick comments (36)
src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/llm/gpt/ChatGPTResponse.java (5)

5-9: 불필요한 Lombok @Getter import 제거

클래스에 @Data를 사용하므로 @Getter import는 중복입니다.

 import java.util.List;

 import lombok.AllArgsConstructor;
 import lombok.Data;
-import lombok.Getter;
 import lombok.NoArgsConstructor;

23-29: Choice: Lombok 중복 제거 및 SnakeCase/ignoreUnknown 적용

  • @Getter 중복 제거.
  • finishReason 등 camelCase 필드가 snake_case(JSON)와 자동 매핑되도록 보강.
-@Data
-@Getter
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
 public static class Choice {
   private int index;
   private Message message;
   private String finishReason;
 }

31-36: Message: 균일한 매핑 전략 적용 + 주석 표현 완화

  • 다른 클래스와 동일하게 @JsonIgnoreProperties/@JsonNaming 적용을 권장합니다.
  • Line 34 주석은 용도 특정이 강합니다. 일반적으로 모델 응답 텍스트(또는 JSON)일 수 있으므로 표현을 완화해 주세요.
-@Data
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
 public static class Message {
   private String role;
-  private String content; // JSON 형식의 감정 분석 결과를 담음
+  private String content; // 모델 응답 텍스트(또는 JSON) 직렬화 문자열
   private String refusal;
 }

38-59: Usage 및 세부 DTO: SnakeCase/ignoreUnknown 적용

OpenAI는 usage 세부 필드를 자주 추가/변경합니다. 알 수 없는 필드 무시와 스네이크케이스 매핑을 클래스별로 명시하면 회귀 위험을 줄일 수 있습니다.

-@Data
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
 public static class Usage {
   private int promptTokens;
   private int completionTokens;
   private int totalTokens;
   private PromptTokensDetails promptTokensDetails;
   private CompletionTokensDetails completionTokensDetails;

-  @Data
+  @Data
+  @JsonIgnoreProperties(ignoreUnknown = true)
+  @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
   public static class PromptTokensDetails {
     private int cachedTokens;
     private int audioTokens;
   }

-  @Data
+  @Data
+  @JsonIgnoreProperties(ignoreUnknown = true)
+  @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
   public static class CompletionTokensDetails {
     private int reasoningTokens;
     private int audioTokens;
     private int acceptedPredictionTokens;
     private int rejectedPredictionTokens;
   }
 }

3-9: Jackson 어노테이션 추가에 필요한 import 보강

위 제안들을 적용하려면 아래 import가 필요합니다.

 import java.util.List;

+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/llm/gpt/ChatGPTRequest.java (1)

5-9: DTO 검증·불변성 강화 제안

  • Bean Validation으로 조기 검증(@notblank, @notempty, @Valid).
  • List 방어적 복사로 외부 변경 차단(List.copyOf).

아래와 같이 보강을 제안합니다.

 package hanium.modic.backend.domain.ai.aiServer.dto.llm.gpt;

 import java.util.List;
+import java.util.Objects;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;

 public record ChatGPTRequest(
-	String model,
-	List<ChatGPTMessage> messages
+	@NotBlank String model,
+	@NotEmpty @Valid List<ChatGPTMessage> messages
 ) {
+	public ChatGPTRequest {
+		Objects.requireNonNull(model, "model");
+		messages = List.copyOf(Objects.requireNonNull(messages, "messages"));
+	}
 }
src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/llm/gpt/ChatGPTMessage.java (1)

3-7: role 문자열 오타 방지·유효값 보장 제안 (비침투, 와이어 포맷 유지)

roleString이라 오타/스펙 이탈 위험이 있습니다. 레코드의 compact constructor로 null 및 허용값 검증을 추가하면 호환성은 유지하면서 방어적입니다. (허용: system/user/assistant/tool)

 public record ChatGPTMessage(
-    String role,
+    String role,
     String content
 ) {
+    public ChatGPTMessage {
+        java.util.Objects.requireNonNull(role, "role");
+        java.util.Objects.requireNonNull(content, "content");
+        if (!java.util.Set.of("system","user","assistant","tool").contains(role)) {
+            throw new IllegalArgumentException("Unsupported role: " + role);
+        }
+    }
 }
src/main/java/hanium/modic/backend/domain/ai/aiChat/dto/ChatMessageRequest.java (1)

9-15: DTO 수준의 빈 값 검증 제거에 따른 경계 검증 재확인

@notblank 제거로 공백 문자열(" ")이 통과할 수 있습니다. 서비스 계층에서 “텍스트 또는 이미지 중 하나는 유효” 검증을 수행한다면, trim 기반 공백 처리까지 포함되는지 확인해 주세요. 필요 시 DTO에 최대 길이 제한만 별도로 두는 것도 방법입니다.

예: service 내부에서

  • null 또는 공백-only 문자열을 동일하게 빈 값으로 취급
  • 이미지 ID 존재 시 텍스트는 빈 값 허용
src/main/java/hanium/modic/backend/common/async/AsyncConfig.java (2)

55-69: Bean 이름과 메서드 이름 불일치

@bean(name = "llmTaskExecutor")인데 메서드명이 gptTaskExecutor입니다. 혼동 방지를 위해 메서드명을 llmTaskExecutor로 맞추는 것이 좋습니다.

-    @Bean(name = "llmTaskExecutor")
-    public ThreadPoolTaskExecutor gptTaskExecutor() {
+    @Bean(name = "llmTaskExecutor")
+    public ThreadPoolTaskExecutor llmTaskExecutor() {

39-53: CallerRunsPolicy 사용 시 호출 스레드 블로킹 주의

포화 시 호출 스레드에서 실행됩니다. 웹 요청/리액터 이벤트 루프에서 호출될 가능성이 있는 경로라면 지연을 유발합니다. 이미지 삭제 경로가 컨트롤러 스레드에서 직접 호출되지 않는지 확인해 주세요.

src/main/java/hanium/modic/backend/domain/image/util/S3ImageUtil.java (2)

70-74: 전용 실행기 지정 적절함

이미지 삭제를 전용 풀로 분리한 점 좋습니다. 예외는 @async(void) 메서드에서 호출자에게 전파되지 않으니, 모니터링/재시도 정책이 필요한지 검토해 주세요(현재 AsyncUncaughtExceptionHandler 로깅만 수행).

필요 시 실패 이벤트를 메트릭/로깅에 추가하거나, 호출자가 필요로 하면 CompletableFuture 반환으로 전환을 고려하세요.


88-112: 배치 삭제에서 입력 검증 완료 확인

개별 경로 검증 로직은 적절합니다. 대량 삭제 실패 시 부분 성공/롤백 전략이 없는 점만 인지해 주세요(S3는 비원자적). 운영상 요구가 있다면 실패 키 목록 로깅/반환을 고려할 수 있습니다.

src/main/java/hanium/modic/backend/common/error/ErrorCode.java (1)

92-93: 에러 메시지 문구 다듬기

자연스러운 문장으로 미세 조정 제안드립니다.

-    EMPTY_CHAT_MESSAGE_EXCEPTION(HttpStatus.BAD_REQUEST, "AC-002", "채팅 메시지랑 이미지가 모두 비어있습니다."),
+    EMPTY_CHAT_MESSAGE_EXCEPTION(HttpStatus.BAD_REQUEST, "AC-002", "채팅 메시지와 이미지가 모두 비어 있습니다."),
src/main/java/hanium/modic/backend/domain/ai/aiServer/service/llm/AiProvider.java (1)

3-5: 동기식 반환 타입이 비동기/스트리밍 전략과 부조화

WebClient 비동기·SSE 스트리밍을 도입했다면 동기 String 반환은 제약이 큽니다. 최소 Mono 오버로드 또는 스트리밍 메서드 도입을 고려해 주세요.

 public interface AiProvider {
-    String prompt(String systemPrompt, String userPrompt);
+    // 동기(레거시) 경로
+    String prompt(String systemPrompt, String userPrompt);
+    // 비동기 경로
+    reactor.core.publisher.Mono<String> promptAsync(String systemPrompt, String userPrompt);
+    // (선택) 토큰 스트리밍
+    // reactor.core.publisher.Flux<String> promptStream(String systemPrompt, String userPrompt);
 }
src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/llm/claude/ClaudeRequest.java (1)

5-10: 옵셔널 필드 직렬화 및 자바 네이밍 정합성

  • max_tokens를 Integer로 두면 미설정 시 필드 생략이 가능합니다.
  • @JsonInclude로 null 필드 제외 직렬화 권장.
  • 자바 쪽 가독성을 위해 maxTokens + @JsonProperty("max_tokens") 패턴도 고려해 볼 수 있습니다.
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+@JsonInclude(JsonInclude.Include.NON_NULL)
 public record ClaudeRequest(
     String model,
-    int max_tokens,
+    @JsonProperty("max_tokens") Integer max_tokens,
     String system,
     List<ClaudeMessage> messages
 ) {
 }
src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/llm/claude/ClaudeResponse.java (2)

10-14: ClaudeContent 타입 중복 정의 — 혼동 방지 리네이밍/공용화 제안

동일 구조의 ClaudeContentClaudeMessage에도 존재합니다. 중복은 유지보수/혼동 비용을 키웁니다. 공용 DTO로 분리하거나 최소한 ClaudeTextContent 등으로 명확히 구분해 주세요.


5-9: 전달 필드 확장 대비: 알 수 없는 필드 무시 설정 권장

업스트림 스키마 확장 시 역직렬화 안전성을 위해 @JsonIgnoreProperties(ignoreUnknown = true) 추가를 권장합니다.

적용 예:

 package hanium.modic.backend.domain.ai.aiServer.dto.llm.claude;

+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import java.util.List;

+@JsonIgnoreProperties(ignoreUnknown = true)
 public record ClaudeResponse(
 	String id,
 	String type,
 	List<ClaudeContent> content
 ) {
src/main/java/hanium/modic/backend/common/config/Resilience4jConfig.java (1)

26-31: recordResult 조건이 실효성이 없음

CB가 감시하는 리턴값은 최종 답변 문자열(String)인데, "\"error\"" 포함 여부로 실패를 판정하는 현재 로직은 사실상 동작하지 않습니다. 예외 기반 판정만 남기거나 별도의 응답 래퍼로 전환하세요.

적용 예(단순 제거):

-			.recordResult(response -> {
-				if (response == null)
-					return true; // 실패
-				return response.toString().contains("\"error\""); // payload 안에 "error" 있으면 실패
-			})
src/main/java/hanium/modic/backend/common/property/property/AiProperties.java (1)

14-28: 필수 설정값 검증 추가 제안(@validated, @notblank)

키/모델 누락 시 런타임 오류를 예방하려면 부트 부팅 시점 검증을 권장합니다.

적용 예:

-import lombok.Getter;
-import lombok.Setter;
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.validation.annotation.Validated;
+import jakarta.validation.constraints.NotBlank;

-@ConfigurationProperties(prefix = "ai")
+@ConfigurationProperties(prefix = "ai")
+@Validated
 public class AiProperties {
@@
 	public static class OpenAi {
-		private String apiKey;
-
-		private String model;
+		@NotBlank private String apiKey;
+		@NotBlank private String model;
 	}
@@
 	public static class Claude {
-		private String apiKey;
-		private String model;
+		@NotBlank private String apiKey;
+		@NotBlank private String model;
 	}
src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/llm/claude/ClaudeMessage.java (2)

9-12: ClaudeContent 중복 정의

ClaudeResponse에도 동일 레코드가 있습니다. 공용 타입으로 통합하거나 네이밍을 분리해 혼동을 줄이세요.


6-7: role 스트링 → enum 전환 검토

타이포 방지 및 스키마 안정성을 위해 enum Role { user, assistant, system } 등으로 강타이핑을 권장합니다.

src/main/java/hanium/modic/backend/domain/ai/aiServer/service/llm/GptProvider.java (1)

61-62: 호출 레벨 타임아웃 추가

네트워크/서버 지연 대비로 퍼블리셔 타임아웃을 걸어주세요.

-			.bodyToMono(String.class)
-			.block();
+			.bodyToMono(String.class)
+			.timeout(java.time.Duration.ofSeconds(60))
+			.block();
src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiChatMessageService.java (3)

95-103: 변수명 오타(nit): textContext → textContent

의미상 textContent가 일관됩니다. 가독성 위해 수정 권장.

-	private void validateRequestMessageNotEmpty(ChatMessageRequest request) {
-		boolean isEmptyTextContext = request.textContent() == null || request.textContent().trim().isEmpty();
+	private void validateRequestMessageNotEmpty(ChatMessageRequest request) {
+		boolean isEmptyTextContent = request.textContent() == null || request.textContent().trim().isEmpty();
 		boolean isEmptyImageId = request.aiChatImageId() == null;
 
-		if (isEmptyTextContext && isEmptyImageId) {
+		if (isEmptyTextContent && isEmptyImageId) {
 			throw new AppException(ErrorCode.EMPTY_CHAT_MESSAGE_EXCEPTION);
 		}
 	}

89-91: 트랜잭션 내 외부 호출 — 사가/사후커밋 전송 권장

DB 트랜잭션 내에서 외부 처리(메시지 발행 등)를 호출하면 롤백 시 일관성 문제가 납니다. 커밋 후 전송으로 전환하세요.

적용 방향:

  • @TransactionalEventListener(phase = AFTER_COMMIT) 또는
  • TransactionSynchronizationManager.registerSynchronization(...)로 커밋 후 aiServerService.processAiRequest(...) 호출

82-85: 중복 조회 최소화

이미 존재 검증(validateAiChatImageExistence) 후 findById를 다시 호출합니다. 단일 조회로 합치면 I/O 1회 절감됩니다.

src/main/java/hanium/modic/backend/domain/ai/aiServer/service/llm/ClaudeProvider.java (2)

68-69: 호출 레벨 타임아웃 추가

퍼블리셔 타임아웃으로 지연 방지.

-			.bodyToMono(String.class)
-			.block();
+			.bodyToMono(String.class)
+			.timeout(java.time.Duration.ofSeconds(60))
+			.block();

28-29: 상수 설정 외부화 제안

MAX_TOKENS를 프로퍼티(ai.claude.max-tokens)로 외부화하면 운영 중 조정이 수월합니다.

src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/AiImageResponseMessageDto.java (1)

5-7: 널 필드 직렬화 제외로 페이로드 단순화

이미지 관련 필드는 조건부입니다. @JsonInclude(Include.NON_NULL)로 불필요한 null 필드 송출을 줄이세요.

적용 예:

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;

@JsonInclude(Include.NON_NULL)
public record AiImageResponseMessageDto( ... ) { }
src/main/java/hanium/modic/backend/domain/ai/aiServer/service/AiChatService.java (1)

51-56: 논블로킹 주장과 달리 동기(block) 경로.

Provider가 WebClient .block()을 사용하므로 실제로는 동기 I/O입니다. 스레드풀 고갈 위험이 있으니(특히 llmTaskExecutor) 타임아웃/버크헤드 적용 또는 진짜 논블로킹/스트리밍 전환을 고려하세요.

src/main/java/hanium/modic/backend/domain/ai/aiServer/listener/AiImageCreatedListener.java (2)

55-59: 오류 로그 메시지의 ID 잘못 기재.

채팅룸 조회 실패 시 aiChatImageId를 로그에 찍고 있습니다. 실제로는 aiChatRoomId를 찍어야 추적 가능합니다.

- log.error("[AI 이미지 처리 실패] AI 채팅방을 찾을 수 없습니다. aiChatImageId: {}", requestChatMessage.getAiChatImageId());
+ log.error("[AI 이미지 처리 실패] AI 채팅방을 찾을 수 없습니다. aiChatRoomId: {}", requestChatMessage.getAiChatRoomId());

118-121: SSE 전송 전 예외 처리/관찰성 보강 제안.

SSE 전송 실패 시 메시지가 유실될 수 있습니다. 전송 시도/실패 카운터 및 재시도(최소 1회) 또는 대기열 보강을 고려하세요.

src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/AiImageRequestMessageDto.java (2)

13-19: 필드명 가독성(nit): imagesPath → imagePaths.

복수형에 맞게 imagePaths가 직관적입니다. JSON 스키마 소비자 영향이 있다면 호환 alias를 고려하세요.


32-52: JSON 페이로드 최소화/일관성 제안.

null 필드 제거를 위해 @JsonInclude(Include.NON_NULL) 적용을 고려하세요(레코드/내부 레코드에 부착).

src/main/java/hanium/modic/backend/domain/ai/aiServer/service/AiServerService.java (3)

168-173: 원문 응답 전체 로깅 주의.

JSON 파싱 실패 시 원문 전체를 에러 로그로 남깁니다. PII/프롬프트 유출 위험이 있어 길이 제한/샘플링/마스킹을 권장합니다.


139-143: TODO 주석 불일치.

이미 SSE 전송으로 동작 중입니다. TODO 갱신/제거하세요.


151-161: LLM 지시 강제력 보강 제안.

"JSON만 출력" 유도를 위해 system prompt 외에 파서가 관대한 정규화/재시도(파싱 실패 시 재프롬프트) 전략을 고려하세요.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f2de3b1 and 0a1ef4f.

📒 Files selected for processing (27)
  • build.gradle (1 hunks)
  • src/main/java/hanium/modic/backend/common/amqp/service/MessageQueueService.java (0 hunks)
  • src/main/java/hanium/modic/backend/common/async/AsyncConfig.java (3 hunks)
  • src/main/java/hanium/modic/backend/common/config/Resilience4jConfig.java (1 hunks)
  • src/main/java/hanium/modic/backend/common/error/ErrorCode.java (1 hunks)
  • src/main/java/hanium/modic/backend/common/property/property/AiProperties.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiChat/dto/ChatMessageRequest.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiChatMessageService.java (2 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/AiChatRequestDto.java (0 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/AiImageRequestMessageDto.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/AiImageResponseMessageDto.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/ImageResultResponse.java (0 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/llm/claude/ClaudeMessage.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/llm/claude/ClaudeRequest.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/llm/claude/ClaudeResponse.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/llm/gpt/ChatGPTMessage.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/llm/gpt/ChatGPTRequest.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/llm/gpt/ChatGPTResponse.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/llm/gpt/GptChatResponseDto.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/listener/AiImageCreatedListener.java (5 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/listener/DlqListener.java (0 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/service/AiChatService.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/service/AiServerService.java (11 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/service/llm/AiProvider.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/service/llm/ClaudeProvider.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/service/llm/GptProvider.java (1 hunks)
  • src/main/java/hanium/modic/backend/domain/image/util/S3ImageUtil.java (2 hunks)
💤 Files with no reviewable changes (4)
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/listener/DlqListener.java
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/ImageResultResponse.java
  • src/main/java/hanium/modic/backend/common/amqp/service/MessageQueueService.java
  • src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/AiChatRequestDto.java
🧰 Additional context used
🧬 Code graph analysis (3)
src/main/java/hanium/modic/backend/domain/ai/aiServer/service/AiChatService.java (2)
src/main/java/hanium/modic/backend/domain/ai/aiServer/service/llm/ClaudeProvider.java (1)
  • Service (20-82)
src/main/java/hanium/modic/backend/domain/ai/aiServer/service/llm/GptProvider.java (1)
  • Service (18-72)
src/main/java/hanium/modic/backend/domain/ai/aiServer/service/llm/GptProvider.java (2)
src/main/java/hanium/modic/backend/domain/ai/aiServer/service/llm/ClaudeProvider.java (1)
  • Service (20-82)
src/main/java/hanium/modic/backend/domain/ai/aiServer/service/AiChatService.java (1)
  • Slf4j (16-61)
src/main/java/hanium/modic/backend/domain/ai/aiServer/service/llm/ClaudeProvider.java (2)
src/main/java/hanium/modic/backend/domain/ai/aiServer/service/llm/GptProvider.java (1)
  • Service (18-72)
src/main/java/hanium/modic/backend/domain/ai/aiServer/service/AiChatService.java (1)
  • Slf4j (16-61)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (13)
src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/llm/gpt/ChatGPTResponse.java (2)

1-1: 확인 완료 — 잔존 참조 없음

hanium.modic.backend.domain.ai.aiServer.dto.chatGpt를 참조하는 코드는 없습니다. 현재 관련 import는 src/main/java/hanium/modic/backend/domain/ai/aiServer/service/llm/GptProvider.java (12–14행)에서 hanium.modic.backend.domain.ai.aiServer.dto.llm.gpt.*를 사용하고 있습니다. 향후 패키지 소문자화(aiServer → ai.server) 리팩토링 검토 바랍니다.


10-14: Lombok 중복 제거 + JSON 스네이크케이스 매핑/내구성 강화 제안

File: src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/llm/gpt/ChatGPTResponse.java (Lines: 10-14)

  • @DaTa@Getter가 포함되어 있어 중복입니다 — @Getter 제거 제안.
  • OpenAI 응답은 snake_case 필드가 많아 DTO에 @JsonIgnoreProperties(ignoreUnknown = true) 및 @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) 추가로 글로벌 설정 의존도를 낮추고 변경에 견고하게 만드세요.
  • 실행하신 rg 검색이 빈 결과를 반환했습니다(리포지토리에서 글로벌 SNAKE_CASE 설정 미발견). 팀에 글로벌 설정이 있다면 위 어노테이션은 선택적이니 확인 필요.

적용 예시(diff):

 @Data
 @NoArgsConstructor
 @AllArgsConstructor
-@Getter
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
 public class ChatGPTResponse {

필요 import:

+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/llm/gpt/ChatGPTRequest.java (1)

1-1: 패키지 네임스페이스 정리 확인 — 구 패키지 참조 없음

rg 검사 결과, 구 패키지 경로(hanium.modic.backend.domain.ai.aiServer.dto.chatGpt)를 참조하는 파일 없음. ChatGPT 관련 클래스들은 llm.gpt 패키지에서 사용 중(예: src/main/java/hanium/modic/backend/domain/ai/aiServer/service/llm/GptProvider.java).

src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/llm/gpt/ChatGPTMessage.java (1)

1-1: 패키지 이동 OK — 로컬에서 잔여 import/FQCN 및 @JsonTypeInfo 사용 여부 확인

파일: src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/llm/gpt/ChatGPTMessage.java

리포지토리 검색이 실행되지 않아 자동 확인 불가. 로컬에서 아래 명령을 실행해 잔여 FQCN/임포트와 클래스명 기반 직렬화(@JsonTypeInfo/@JsonTypeName, JsonTypeInfo.Id.CLASS 등) 사용 여부를 확인하십시오.

# (리포지토리 루트에서 실행)
git grep -n "hanium.modic.backend.domain.ai.aiServer.dto.chatGpt.ChatGPTMessage" || true
git grep -n "import hanium.modic.backend.domain.ai.aiServer.dto.chatGpt.ChatGPTMessage;" || true

# ripgrep 사용(설정에 따라 --type 필터를 피함)
rg -n -S -C2 'hanium\.modic\.backend\.domain\.ai\.aiServer\.dto\.chatGpt\.ChatGPTMessage' || true
rg -n -S -C2 '\bChatGPTMessage\b' -g '!**/target/**' -g '!**/build/**' -g '!**/node_modules/**' || true

# 직렬화 어노테이션 검색
rg -n -S -C2 '@JsonTypeInfo\(' || true
rg -n -S -C2 'JsonTypeInfo\.Id\.CLASS' || true
rg -n -S -C2 '@JsonTypeName' || true
src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/llm/gpt/GptChatResponseDto.java (1)

1-4: 패키지 리팩터링 일관성 확인 필요

패키지 이동은 적절합니다. 종속 코드가 모두 새 패키지로 따라왔는지 최종 확인 필요합니다. 실행한 rg가 "No files were searched"를 반환해 자동 검증에 실패했습니다. 저장소 루트에서 아래 명령 중 하나를 실행해 출력 결과를 코멘트에 첨부하세요.

# 권장: ripgrep(숨김/무시 파일 포함)
rg -n --hidden -S -uu --glob '!**/build/**' 'hanium\.modic\.backend\.domain\.ai\.aiServer\.dto\.chatGpt|ChatGPTResponse|GptChatResponseDto' || true

# 대안: git으로 추적 파일만 검색
git --no-pager grep -n -e 'hanium.modic.backend.domain.ai.aiServer.dto.chatGpt' -e 'ChatGPTResponse' -e 'GptChatResponseDto' || true
build.gradle (1)

92-95: 중복 선언 아님: resilience4j-spring-boot3:2.2.0는 circuitbreaker 모듈을 전이 의존하지 않으므로, 별도 선언이 필요합니다.

Likely an incorrect or invalid review comment.

src/main/java/hanium/modic/backend/common/config/Resilience4jConfig.java (1)

23-25: 4xx까지 실패 집계 정책 확인 필요

현재 Provider에서 4xx/5xx를 모두 AppException으로 래핑하므로 4xx도 실패로 집계됩니다. 사용자 입력 유발 4xx(금칙어, 토큰 초과 등)를 회로차단 트립 원인으로 볼지 정책 확인 바랍니다.

src/main/java/hanium/modic/backend/common/property/property/AiProperties.java (1)

1-1: 패키지 경로 확인

property.property 이중 패키지 네이밍이 의도인지 확인 부탁드립니다.

src/main/java/hanium/modic/backend/domain/ai/aiServer/service/llm/GptProvider.java (1)

45-60: 4xx/5xx 동일 처리 정책 확인

현재 4xx/5xx 모두 AI_SERVER_ERROR로 매핑됩니다. 4xx는 입력 오류일 수 있어 재시도/CB 정책에 혼선을 줍니다. 분기(예: 4xx→클라이언트 오류) 필요 여부 확인 바랍니다.

src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiChatMessageService.java (1)

50-52: 빈 메시지 가드 추가 — 좋습니다

텍스트/이미지 동시 공백 차단이 명확합니다.

src/main/java/hanium/modic/backend/domain/ai/aiServer/service/llm/ClaudeProvider.java (1)

35-37: 확인 완료 — anthropic-version: 2023-06-01 유지하세요.

공식 Anthropic Messages API 문서와 Claude 3.5 Sonnet 안내에 따르면 해당 값이 권장되며, 2025-09-22 기준으로 유효합니다.

src/main/java/hanium/modic/backend/domain/ai/aiServer/listener/AiImageCreatedListener.java (1)

111-113: 요청 메시지 상태 업데이트 값 검증 필요.

응답 처리 후 요청 메시지를 AiImageStatus.REQUEST로 돌립니다. 의도한 상태 전이(REQUESTING→RESPONSE 등)와 맞는지 확인 필요합니다. 잘못된 상태면 재시도/통계에 오동작이 납니다.

상태 전이 다이어그램 공유 가능하시면 그에 맞춰 패치 제안 드리겠습니다.

src/main/java/hanium/modic/backend/domain/ai/aiServer/dto/AiImageRequestMessageDto.java (1)

24-26: role 값 스키마 확인 필요(SenderType vs "user/assistant").

주석은 "user/assistant"인데 타입은 내부 SenderType(AI/USER)로 보입니다. 소비 측(이미지 생성기)이 "assistant"를 기대하면 불일치로 오작동합니다. 매핑 규칙을 명확히 하세요.

역직렬화 스키마에 맞춰 매핑이 필요하면 알려주세요. 변환 유틸 추가 가능합니다.

Copy link

@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: 3

🧹 Nitpick comments (2)
src/main/java/hanium/modic/backend/common/config/Resilience4jConfig.java (2)

23-23: 주석 오기 수정 제안

‘SocketTime’ → ‘SocketTimeout’ 이 맞습니다. 또한 ‘ConnectTimeoutException’ 등으로 구체화하면 가독성이 좋아집니다.

-                e instanceof java.io.IOException             // SocketTime, ConnectTimeout, IOException 발생 시 실패로 간주
+                e instanceof java.io.IOException             // SocketTimeout, ConnectTimeoutException 등 네트워크/타임아웃 계열 예외를 실패로 간주

33-34: 구성 외부화 및 인스턴스 네이밍 권장

운영/스테이징 튜닝을 위해 application.yml로 외부화하고, primary/secondary를 명시적 인스턴스로 두면 관측/메트릭이 선명해집니다.

예시(application.yml):

resilience4j:
  circuitbreaker:
    configs:
      llm-default:
        sliding-window-type: COUNT_BASED
        sliding-window-size: 20
        minimum-number-of-calls: 10
        failure-rate-threshold: 50
        wait-duration-in-open-state: 60s
        permitted-number-of-calls-in-half-open-state: 10
        automatic-transition-from-open-to-half-open-enabled: true
    instances:
      llm-primary:
        base-config: llm-default
      llm-secondary:
        base-config: llm-default
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a1ef4f and 9ad4eb5.

📒 Files selected for processing (1)
  • src/main/java/hanium/modic/backend/common/config/Resilience4jConfig.java (1 hunks)
🔇 Additional comments (1)
src/main/java/hanium/modic/backend/common/config/Resilience4jConfig.java (1)

18-21: CB 파라미터가 PR 요약과 불일치 — 설계값(60s/20/Half‑Open 10)으로 수정 필요

설계(요약)와 상이합니다. 60s, window 20, Half‑Open 허용 10회, 자동 Half‑Open 전환을 반영해 주세요.

-            .failureRateThreshold(50)                        // 실패율 50% 이상이면 Open
-            .slidingWindowSize(10)                           // 최근 10번 요청 기준
-            .minimumNumberOfCalls(10)                        // 최소 10번 이후부터 판단
-            .waitDurationInOpenState(Duration.ofHours(1)) // Open 유지 시간
+            .failureRateThreshold(50)                          // 실패율 50% 이상이면 Open
+            .slidingWindowSize(20)                             // 최근 20번 요청 기준
+            .minimumNumberOfCalls(10)                          // 최소 10번 이후부터 판단
+            .waitDurationInOpenState(Duration.ofSeconds(60))   // Open 유지 시간
+            .permittedNumberOfCallsInHalfOpenState(10)         // Half-Open 허용 호출 수
+            .automaticTransitionFromOpenToHalfOpenEnabled(true) // 60초 후 자동 Half-Open 시도

@yooooonshine yooooonshine changed the title feat: 서킷 브레이커를 통한 LLM API 이중화 구현 및 비동기 처리 feat: AI 로직 개선 및 서킷 브레이커를 통한 LLM API 이중화 구현 및 비동기 처리 Sep 22, 2025
@yooooonshine yooooonshine changed the title feat: AI 로직 개선 및 서킷 브레이커를 통한 LLM API 이중화 구현 및 비동기 처리 feat: AI 로직 개선 및 서킷 브레이커를 통한 LLM API 이중화 구현 Sep 22, 2025
@yooooonshine yooooonshine merged commit 65a7516 into develop Sep 22, 2025
1 of 2 checks passed
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.

GPT 이중화 구현

1 participant