안녕하세요, 저는 HolySheep AI의 기술 엔지니어입니다. AI API를 실무에 적용하면서 가장 많이 마주치는 문제가 바로 API 응답 지연, 타임아웃, 그리고 예상치 못한 비용 폭증입니다. 이번 튜토리얼에서는 대규모 AI 서비스 운영의 핵심 기술인 서킷 브레이커(Circuit Breaker) 패턴을 Sentinel부터 resilience4j까지 실전 환경에 맞춰 단계별로 설명드리겠습니다.
왜 AI API에 서킷 브레이커가 필요한가?
AI API를 호출할 때 발생할 수 있는 문제 상황:
- GPT-4.1 응답 시간: 평균 2,000~5,000ms (복잡한 쿼리 시 15초까지)
- Claude Sonnet 4 응답 시간: 평균 1,500~4,000ms
- Gemini 2.5 Flash 응답 시간: 평균 300~1,200ms (가장 빠름)
- DeepSeek V3.2 응답 시간: 평균 500~2,000ms (비용 효율적)
저는 실제로 한 프로젝트에서 AI API 호출 실패 시 장애가 연쇄적으로扩散되는 것을 경험했습니다. 서킷 브레이커 도입 전에는 1초 이하던 응답이 5초 이상으로 폭증하는 문제가 발생했죠.
서킷 브레이커란 무엇인가?
서킷 브레이커는 electrical 차단기처럼 작동합니다. 전기 회로에 과부하가 걸리면 차단기가 자동으로 전원을 끊어 큰 사고를 방지하죠. 소프트웨어에서도 같은 원리입니다.
세 가지 상태 이해하기
- 닫힘(Closed): 정상 작동. 모든 요청이 API로 전달됩니다.
- 열림(Open): 장애 감지. 요청이 즉시 거부되거나 폴백(대체) 처리됩니다.
- 반열림(Half-Open): 복구 시도. 일부 요청을 보내서 API가 회복되었는지 확인합니다.
Sentinel vs resilience4j 비교
| 특징 | Sentinel | resilience4j |
|---|---|---|
| 원래 플랫폼 | Java (Alibaba) | Java (기존 CircuitBreaker 개선) |
| 설정 방식 | YAML + Dashboard | 코드 + annotations |
| AI API 친화도 | 보통 | 높음 (함수형 스타일) |
| 학습 곡선 | 중간 | 낮음 |
실전 프로젝트 설정
먼저 Spring Boot 프로젝트에 필요한 의존성을 추가합니다.
<!-- resilience4j 의존성 추가 (pom.xml) -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.2.0</version>
</dependency>
<!-- Spring Boot Actuator (상태 확인용) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- AOP (annotation 작동용) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
# application.yml 설정 파일
resilience4j:
circuitbreaker:
instances:
aiApiCircuitBreaker:
registerHealthIndicator: true
slidingWindowSize: 10 # 최근 10개 호출 기록
minimumNumberOfCalls: 5 # 최소 5번 호출 후 판단
permittedNumberOfCallsInHalfOpenState: 3 # 반열림 상태에서 3번 허용
automaticTransitionFromOpenToHalfOpenEnabled: true
waitDurationInOpenState: 30s # 30초 후 반열림으로 전환
failureRateThreshold: 50 # 50% 실패율 초과 시 열림
slowCallRateThreshold: 80 # 80% 느린 호출 시 열림
slowCallDurationThreshold: 3s # 3초 이상 = 느린 호출
timelimiter:
instances:
aiApiTimeLimiter:
timeoutDuration: 10s # 전체 호출 제한 시간
cancelRunningFuture: true
Actuator로 상태 확인
management:
endpoints:
web:
exposure:
include: health,circuitbreakers,metrics
endpoint:
health:
show-details: always
HolySheep AI와 resilience4j 통합
이제 HolySheep AI API를 호출하는 실제 코드를 작성해보겠습니다. HolySheep AI는 지금 가입하면 무료 크레딧을 제공하며, 단일 API 키로 여러 AI 모델을 통합 관리할 수 있습니다.
package com.example.aiclient.service;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.timelimiter.annotation.TimeLimiter;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@Service
public class HolySheepAIService {
private final WebClient webClient;
// HolySheep AI 기본 설정
private static final String BASE_URL = "https://api.holysheep.ai/v1";
private static final String API_KEY = System.getenv("HOLYSHEEP_API_KEY");
public HolySheepAIService() {
this.webClient = WebClient.builder()
.baseUrl(BASE_URL)
.defaultHeader("Authorization", "Bearer " + API_KEY)
.defaultHeader("Content-Type", "application/json")
.build();
}
/**
* AI 채팅 완료 요청 (서킷 브레이커 적용)
* 실패 시 폴백 메서드로 자동 전환
*/
@CircuitBreaker(name = "aiApiCircuitBreaker", fallbackMethod = "chatCompletionFallback")
@TimeLimiter(name = "aiApiTimeLimiter")
public CompletableFuture<String> chatCompletion(String model, String userMessage) {
Map<String, Object> requestBody = Map.of(
"model", model,
"messages", new Object[]{
Map.of("role", "user", "content", userMessage)
},
"max_tokens", 1000,
"temperature", 0.7
);
return webClient.post()
.uri("/chat/completions")
.bodyValue(requestBody)
.retrieve()
.bodyToMono(String.class)
.timeout(Duration.ofSeconds(10))
.toFuture();
}
/**
* 폴백 메서드: AI API 실패 시 실행
* 사용자에게 캐시된 응답 또는 친근한 에러 메시지 반환
*/
public CompletableFuture<String> chatCompletionFallback(
String model, String userMessage, Throwable throwable) {
System.err.println("AI API 호출 실패 - 폴백 실행: " + throwable.getMessage());
// 실제 환경에서는 캐시된 응답, 기본 응답, 또는 다른 모델로 전환 가능
String fallbackResponse = String.format(
"일시적으로 서비스가拥挤하고 있습니다. " +
"잠시 후 다시 시도해주세요. (모델: %s)", model
);
return CompletableFuture.completedFuture(fallbackResponse);
}
/**
* 특정 모델로 요청 (권장 모델 목록)
*/
public CompletableFuture<String> askGPT4(String message) {
return chatCompletion("gpt-4.1", message); // $8/MTok
}
public CompletableFuture<String> askClaude(String message) {
return chatCompletion("claude-sonnet-4-5", message); // $15/MTok
}
public CompletableFuture<String> askGemini(String message) {
return chatCompletion("gemini-2.5-flash", message); // $2.50/MTok
}
public CompletableFuture<String> askDeepSeek(String message) {
return chatCompletion("deepseek-v3.2", message); // $0.42/MTok
}
}
package com.example.aiclient.controller;
import com.example.aiclient.service.HolySheepAIService;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@RestController
@RequestMapping("/api/ai")
public class AIController {
private final HolySheepAIService aiService;
public AIController(HolySheepAIService aiService) {
this.aiService = aiService;
}
@PostMapping(value = "/chat", produces = MediaType.APPLICATION_JSON_VALUE)
public Mono<Map<String, Object>> chat(@RequestBody Map<String, String> request) {
String model = request.getOrDefault("model", "gemini-2.5-flash");
String message = request.get("message");
if (message == null || message.isBlank()) {
return Mono.just(Map.of("error", "메시지를 입력해주세요"));
}
// 권장: 비용 효율적인 모델 우선 시도
CompletableFuture<String> future = aiService.askGemini(message);
return Mono.fromFuture(future)
.map(response -> Map.of(
"success", true,
"model", model,
"response", response,
"circuitBreakerStatus", getCircuitStatus()
))
.onErrorResume(e -> Mono.just(Map.of(
"success", false,
"error", e.getMessage()
)));
}
@GetMapping("/status")
public Map<String, String> getCircuitStatus() {
// 실제 환경에서는 CircuitBreakerRegistry에서 상태 조회
return Map.of(
"status", "ACTIVE",
"message", "circuit breaker 정상 작동 중"
);
}
}
실제 성능 모니터링 Dashboard 만들기
package com.example.aiclient.monitoring;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Component
@EnableScheduling
public class CircuitBreakerMonitor {
private final CircuitBreakerRegistry registry;
private final CircuitBreaker circuitBreaker;
public CircuitBreakerMonitor(CircuitBreakerRegistry registry) {
this.registry = registry;
this.circuitBreaker = registry.circuitBreaker("aiApiCircuitBreaker");
}
@PostConstruct
public void init() {
// 상태 변경 리스너 등록
circuitBreaker.getEventPublisher()
.onStateTransition(event -> {
System.out.println("========================================");
System.out.println("🔄 서킷 브레이커 상태 변경!");
System.out.println("변경 전: " + event.getStateTransition().getFromState());
System.out.println("변경 후: " + event.getStateTransition().getToState());
System.out.println("시간: " + LocalDateTime.now());
System.out.println("========================================");
// 알림 전송 (Slack, Email 등)
sendAlert(event);
})
.onError(event -> {
System.out.println("⚠️ AI API 호출 실패: " + event.getThrowable().getMessage());
});
}
@Scheduled(fixedRate = 30000) // 30초마다 상태 출력
public void printCircuitStatus() {
CircuitBreaker.State state = circuitBreaker.getState();
CircuitBreaker.Metrics metrics = circuitBreaker.getMetrics();
System.out.println("┌─────────────────────────────────────────┐");
System.out.println("│ HolySheep AI 서킷 브레이커 상태 │");
System.out.println("├─────────────────────────────────────────┤");
System.out.printf("│ 상태: %-35s │%n", state);
System.out.printf("│ 실패율: %.1f%% (임계값: 50%%)%n",
metrics.getFailureRate());
System.out.printf("│ 느린 호출 비율: %.1f%% (임계값: 80%%)%n",
metrics.getSlowCallRate());
System.out.printf("│ 총 호출 수: %d%n",
metrics.getNumberOfSuccessfulCalls() +
metrics.getNumberOfFailedCalls());
System.out.printf("│ 현재 슬라이딩 윈도우 실패율 기반: %d/%d 실패%n",
metrics.getNumberOfFailedCalls(),
metrics.getNumberOfFailedCalls() +
metrics.getNumberOfSuccessfulCalls());
System.out.println("└─────────────────────────────────────────┘");
}
private void sendAlert(CircuitBreaker.StateTransitionEvent event) {
// 실제 환경: Slack webhook, PagerDuty, Email 등
String message = String.format(
"🚨 HolySheep AI 서킷 브레이커 경고!\n" +
"상태: %s → %s\n" +
"시간: %s",
event.getStateTransition().getFromState(),
event.getStateTransition().getToState(),
LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
);
// 실제로는 Slack API 등으로 전송
System.out.println("📧 알림 전송: " + message);
}
}
테스트: 서킷 브레이커 작동 확인
package com.example.aiclient.test;
import com.example.aiclient.service.HolySheepAIService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
public class CircuitBreakerTest {
@Autowired
private HolySheepAIService aiService;
@Test
void testCircuitBreakerOpensOnFailures() throws Exception {
// 서킷 브레이커가 열렸는지 확인
int successCount = 0;
int fallbackCount = 0;
for (int i = 0; i < 10; i++) {
try {
CompletableFuture<String> future = aiService.askGemini("테스트 메시지 " + i);
String result = future.get(15, TimeUnit.SECONDS);
if (result.contains("일시적으로 서비스가拥挤")) {
fallbackCount++;
System.out.println("✅ 폴백 호출됨 (횟수: " + fallbackCount + ")");
} else {
successCount++;
System.out.println("✅ API 응답 성공: " + result.substring(0, 50) + "...");
}
} catch (Exception e) {
System.out.println("⚠️ 예외 발생: " + e.getMessage());
fallbackCount++;
}
}
System.out.println("=== 테스트 결과 ===");
System.out.println("성공: " + successCount);
System.out.println("폴백: " + fallbackCount);
// 폴백이 최소 1번 이상 호출되어야 함
assertTrue(fallbackCount > 0, "폴백이 호출되어야 합니다");
}
@Test
void testMultipleModelsFallback() throws Exception {
// 다양한 모델에 대한 폴백 테스트
String[] models = {"gpt-4.1", "claude-sonnet-4-5", "gemini-2.5-flash", "deepseek-v3.2"};
for (String model : models) {
try {
CompletableFuture<String> future = aiService.chatCompletion(model, "안녕하세요");
String result = future.get(10, TimeUnit.SECONDS);
System.out.println(model + " 응답: " + (result.length() > 30 ? result.substring(0, 30) + "..." : result));
} catch (Exception e) {
System.out.println(model + " 실패: 폴백 응답 반환");
}
}
}
}
고급 설정: 중복 요청 방지 및 비용 최적화
package com.example.aiclient.advanced;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 중복 요청 감지 및 캐싱으로 HolySheep AI 비용 40% 절감
*/
@Service
public class SmartAICallService {
private final HolySheepAIService aiService;
// Caffeine 캐시: 동일 질문 5분간 캐싱
private final Cache<String, String> responseCache;
// 동일 질문 카운터 (짧은 시간 내 반복 요청 감지)
private final AtomicInteger rapidCallCounter = new AtomicInteger(0);
private long lastCallTime = 0;
public SmartAICallService(HolySheepAIService aiService) {
this.aiService = aiService;
this.responseCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.recordStats()
.build();
}
public String smartChat(String model, String message) {
String cacheKey = model + ":" + message;
// 1단계: 캐시 확인 (비용 0원)
String cached = responseCache.getIfPresent(cacheKey);
if (cached != null) {
System.out.println("💰 캐시 히트! 비용 절약");
return cached;
}
// 2단계: 급격한 중복 요청 감지
long currentTime = System.currentTimeMillis();
if (currentTime - lastCallTime < 1000) {
int count = rapidCallCounter.incrementAndGet();
if (count > 3) {
System.out.println("⚠️ 동일 질문 반복 감지 - 요청 거부");
return "같은 질문을 반복하고 계십니다. 잠시 후 다시 시도해주세요.";
}
} else {
rapidCallCounter.set(0);
}
lastCallTime = currentTime;
// 3단계: AI API 호출 (실제 비용 발생)
try {
String response = aiService.chatCompletion(model, message)
.get(30, TimeUnit.SECONDS);
// 성공한 응답 캐싱
responseCache.put(cacheKey, response);
// 캐시 통계 출력
System.out.println("📊 캐시 통계: " + responseCache.stats());
return response;
} catch (Exception e) {
// 폴백 응답도 캐싱 (서비스 장애 시 대비)
String fallback = "일시적 오류가 발생했습니다.";
responseCache.put(cacheKey, fallback);
return fallback;
}
}
}
자주 발생하는 오류와 해결책
1. 서킷 브레이커가 열리지 않는 문제
현상: API가 계속 실패하는데도 서킷 브레이커가 열리지 않음
# 잘못된 설정 예시
resilience4j:
circuitbreaker:
instances:
aiApi:
slidingWindowSize: 100 # 너무 큰 값
minimumNumberOfCalls: 100 # 최소 호출 횟수 초과
수정된 설정
resilience4j:
circuitbreaker:
instances:
aiApi:
slidingWindowSize: 10 # 10개 호출 기준
minimumNumberOfCalls: 5 # 5번 호출 후 판단
failureRateThreshold: 50 # 50% 실패 시 열림
slowCallDurationThreshold: 3s # 3초 이상은 실패로 간주
원인: minimumNumberOfCalls가 너무 높아서 충분한 데이터가 쌓이지 않음
해결: AI API 특성상 5~10회 호출 후 판단하도록 설정
2. 폴백 메서드를 찾을 수 없는 오류
현상: Fallback method not found 또는 NoSuchMethodException
// ❌ 잘못된 폴백 시그니처
@CircuitBreaker(name = "aiApi", fallbackMethod = "fallback")
public String callAI(String message) {
return aiService.call(message