안녕하세요, 저는 HolySheep AI의 기술 엔지니어입니다. AI API를 실무에 적용하면서 가장 많이 마주치는 문제가 바로 API 응답 지연, 타임아웃, 그리고 예상치 못한 비용 폭증입니다. 이번 튜토리얼에서는 대규모 AI 서비스 운영의 핵심 기술인 서킷 브레이커(Circuit Breaker) 패턴을 Sentinel부터 resilience4j까지 실전 환경에 맞춰 단계별로 설명드리겠습니다.

왜 AI API에 서킷 브레이커가 필요한가?

AI API를 호출할 때 발생할 수 있는 문제 상황:

저는 실제로 한 프로젝트에서 AI API 호출 실패 시 장애가 연쇄적으로扩散되는 것을 경험했습니다. 서킷 브레이커 도입 전에는 1초 이하던 응답이 5초 이상으로 폭증하는 문제가 발생했죠.

서킷 브레이커란 무엇인가?

서킷 브레이커는 electrical 차단기처럼 작동합니다. 전기 회로에 과부하가 걸리면 차단기가 자동으로 전원을 끊어 큰 사고를 방지하죠. 소프트웨어에서도 같은 원리입니다.

세 가지 상태 이해하기

Sentinel vs resilience4j 비교

특징Sentinelresilience4j
원래 플랫폼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