이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- LLM 호출 timeout은 전체 API timeout과 어떻게 나눠야 하는가?
- 어떤 실패에 retry를 적용하고 어떤 실패에는 적용하지 말아야 하는가?
- rate limit은 provider quota 외에 어떤 기준으로 관리해야 하는가?
- circuit breaker와 degrade mode는 AI 기능에서 어떻게 쓰이는가?
개요
LLM API는 일반적인 내부 DB 조회보다 느리고 변동성이 크다. provider rate limit, 긴 생성 시간, 네트워크 지연, token 길이, safety refusal, schema validation 실패가 모두 요청 흐름에 영향을 준다.
따라서 LLM API 연동은 “HTTP client로 호출한다”가 아니라 timeout, retry, rate limit, circuit breaker, fallback, degrade mode를 포함한 외부 시스템 통합으로 봐야 한다.
왜 백엔드 개발자에게 중요한가
AI 기능은 백엔드 서버 리소스를 오래 점유할 수 있다. 동기 API 서버에서 모델 응답을 60초 기다리면 thread, connection, memory, 사용자 세션이 함께 묶인다. 몇 명의 사용자가 긴 요청을 보내는 것만으로도 일반 API 품질이 나빠질 수 있다.
또한 retry가 비용을 증폭시킨다. timeout 1번과 retry 2번은 사용자가 한 번 클릭했어도 provider 호출은 3번이 된다. rate limit 상황에서 모두가 retry하면 provider와 내부 서버 모두 압박을 받는다.
Timeout 설계
timeout은 최소 두 층으로 나눈다.
- provider call timeout: 모델 호출 하나를 얼마나 기다릴 것인가
- user request timeout: 사용자 API 전체를 얼마나 기다릴 것인가
예를 들어 고객-facing 요약 API의 전체 timeout이 12초라면, LLM 호출 timeout을 8초 정도로 두고 나머지 시간은 후처리와 응답 생성에 남겨야 한다. 긴 문서 요약처럼 30초 이상 걸릴 수 있는 작업은 동기 API가 아니라 job으로 분리하는 편이 낫다.
Retry 설계
retry는 실패 유형별로 다르게 적용한다.
| 실패 유형 | retry 판단 |
|---|---|
| 네트워크 일시 오류 | 제한적 retry 가능 |
| provider 5xx | backoff 후 retry 가능 |
| 429 rate limit | retry-after 또는 queue 고려 |
| timeout | 한 번 정도 가능하지만 비용 주의 |
| schema validation failure | 같은 요청 반복보다 repair/fallback 고려 |
| content refusal | retry로 해결되지 않는 경우가 많음 |
| side effect tool 실행 후 실패 | idempotency 없이는 retry 금지 |
retry에는 max attempts, backoff, jitter, timeout budget이 필요하다.
코드로 이해하기
다음은 Java에서 retry를 무제한으로 하지 않고 실패 유형을 제한하는 예시다.
public class RetryingLlmClient implements LlmClient {
private final LlmClient delegate;
public RetryingLlmClient(LlmClient delegate) {
this.delegate = delegate;
}
@Override
public LlmGenerateResult generate(LlmGenerateCommand command) {
int maxAttempts = 3;
RuntimeException last = null;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return delegate.generate(command);
} catch (LlmRateLimitedException | LlmTimeoutException ex) {
last = ex;
if (attempt == maxAttempts) {
break;
}
sleep(backoffMillis(attempt));
} catch (LlmValidationException | LlmRefusalException ex) {
throw ex;
}
}
throw new LlmUnavailableException("LLM request failed after retry", last);
}
private long backoffMillis(int attempt) {
long base = 200L * attempt;
long jitter = ThreadLocalRandom.current().nextLong(50, 150);
return base + jitter;
}
private void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new LlmUnavailableException("retry interrupted", ex);
}
}
}이 코드는 단순화를 위해 Thread.sleep을 사용하지만, 실제 reactive 서버나 worker에서는 non-blocking delay 또는 queue retry를 사용하는 편이 낫다.
Rate Limit 설계
rate limit은 provider가 정한 quota만 보지 않는다.
- 사용자별 분당 요청 수
- 테넌트별 일일 token budget
- 기능별 최대 output token
- 관리자와 일반 사용자 차등 quota
- eval과 production 트래픽 분리
- worker concurrency 제한
provider 429가 발생한 뒤에야 막으면 늦다. 내부에서 먼저 budget과 quota를 관리해야 한다.
Circuit Breaker와 Degrade Mode
provider 장애가 길어질 때 계속 호출하면 서버 자원이 고갈된다. circuit breaker는 실패율이 높을 때 일정 시간 호출을 막고 빠르게 fallback 또는 실패 응답을 준다.
degrade mode는 기능을 낮은 품질로 계속 제공하는 전략이다.
- 실시간 답변 대신 “현재 AI 답변이 지연되고 있습니다”를 보여준다.
- 고급 모델 대신 저렴하고 빠른 모델을 쓴다.
- RAG 답변 대신 관련 문서 목록만 보여준다.
- 요약 생성을 job으로 전환한다.
장애 상황과 대응
rate limit 장애가 나면 다음을 확인한다.
- 특정 tenant가 token을 과도하게 쓰는가?
- retry가 429를 증폭시키는가?
- eval이나 batch 작업이 production quota를 잡아먹는가?
- prompt 변경으로 input token이 늘었는가?
- output token 제한이 풀려 있는가?
timeout 장애가 나면 token 길이, 모델 변경, provider 상태, 네트워크, worker concurrency를 함께 본다.
인프라 협업 포인트
인프라팀과 합의할 항목은 다음이다.
- API gateway timeout과 application timeout
- worker concurrency와 queue depth
- provider별 circuit breaker threshold
- rate limit metric과 alert
- retry storm 방지 정책
- fallback provider 트래픽이 비용을 얼마나 만들 수 있는지
LLM 장애는 외부 장애와 내부 과부하가 섞인다. 인프라팀과 지표 이름을 맞춰야 장애 때 대화가 된다.
개인 프로젝트 최소 기준
- provider call timeout을 둔다.
- retry는 2회 이하로 제한한다.
- 429와 timeout을 구분해 로그를 남긴다.
- 사용자별 일일 호출 제한을 둔다.
- 긴 요청은 거절하거나 비동기로 처리한다.
기업 운영 수준 기준
- tenant별 quota와 비용 예산을 둔다.
- provider별 circuit breaker와 fallback 정책을 운영한다.
- retry, fallback, rate limit, timeout 지표를 대시보드화한다.
- eval/batch 트래픽과 production 트래픽을 분리한다.
- 장애 대응 런북을 작성한다.
실전 팁
- retry 횟수보다 전체 timeout budget이 먼저다. 예를 들어 10초 안에 끝내야 하는 API에서 8초 timeout을 3번 시도할 수는 없다.
- 429는 무작정 즉시 재시도하지 않는다. retry-after, queue, 사용자 안내를 고려한다.
- validation failure는 같은 프롬프트를 다시 던지는 것보다 repair prompt나 fallback schema를 검토한다.
- circuit breaker가 열렸을 때 사용자 메시지도 제품팀과 미리 정한다.
위험 신호!
- timeout 없는 모델 호출이 있다.
- 모든 예외에 같은 retry 정책을 적용한다.
- provider 429가 난 뒤에도 여러 서버 인스턴스가 동시에 retry한다.
- eval 작업이 production quota를 함께 쓴다.
- circuit breaker 없이 provider 장애를 계속 기다린다.
확인 질문
확인 질문
- LLM retry 정책에서 가장 먼저 구분해야 하는 것은 무엇인가?
- 재시도 가능한 일시 장애인지, validation/refusal/side effect처럼 재시도하면 안 되거나 의미 없는 실패인지 구분해야 한다.
- rate limit을 provider quota만으로 보면 부족한 이유는 무엇인가?
- 사용자별, 테넌트별, 기능별 비용 폭증과 내부 리소스 고갈을 막을 수 없기 때문이다.
- degrade mode의 목적은 무엇인가?
- provider 장애나 과부하 상황에서 전체 기능 장애 대신 제한된 품질로 서비스를 유지하는 것이다.
Spring AI 적용 연결
- Spring AI 호출도 일반 외부 API처럼 timeout, retry, rate limit, fallback의 정책 경계를 가져야 한다.
ChatClient호출 경계 설계는 03. Spring Boot 계층 설계와 AI Gateway에서 이어진다. - 장애 감지와 degrade 판단은 11. Observability와 장애 대응 런북과 연결해서 본다.