LLM을 붙인 제품의 백엔드는 일반 CRUD 백엔드와 안정성 모델이 다르다. 일반 백엔드는 "DB가 살아 있으면 같은 입력에 같은 출력"이 기본 전제다. AI 제품 백엔드는 그렇지 않다. - 응답이 비결정적이다 — 같은 입력에도 모델 출력이 매번 달라진다. 그래서 "정상 응답"의 정의 자체를 우리가 따로 만들어야 한다. - 지연이 크고 가변적이다 — 모델...
LLM을 붙인 제품의 백엔드는 일반 CRUD 백엔드와 안정성 모델이 다르다. 일반 백엔드는 "DB가 살아 있으면 같은 입력에 같은 출력"이 기본 전제다. AI 제품 백엔드는 그렇지 않다.
그래서 AI 제품 백엔드의 안정성은 "장애가 안 나게 한다"가 아니라 "비결정성·가변 지연·실시간 비용·외부 의존을 전제로 깔고, 나빠질 때 어디서 끊고 어떻게 우아하게 내려갈지를 설계한다" 가 핵심이다. 이 문서는 그 설계를 지연·비용·권한·관측·실패 처리 다섯 축으로 정리한다.
도구 호출 자체의 설계(레지스트리, 스키마 검증, dispatcher)는 LLM Tool Calling과 Agent Workflow 설계에서, 일반 분산 시스템 resilience 스택(Timeout/Retry/Circuit Breaker/Bulkhead/Backpressure)은 Resilience 패턴 가이드에서 다룬다. 이 문서는 그 둘을 AI 제품 운영 관점에서 묶는 허브 역할을 한다.
AI 제품 백엔드의 안정성은 다음 다섯 가지를 각각 "예산"과 "실패 정책"으로 설계하는 일이다.
| 축 | 핵심 질문 | 대표 통제 수단 |
|---|---|---|
| 지연 | 사용자가 얼마나 기다리게 둘 것인가 | 시간 예산, 스트리밍, 계층 timeout |
| 비용 | 한 요청에 얼마까지 쓸 것인가 | 토큰/호출/누적 비용 가드, 캐싱, 모델 폴백 |
| 권한 | 모델이 무엇을 건드리게 둘 것인가 | 사용자 컨텍스트 권한 검증, 도구 노출 제한 |
| 관측 | 무엇이 왜 실패했는지 사후에 알 수 있는가 | correlation id, audit JSONL, LLM 특화 메트릭 |
| 실패 처리 | 나빠질 때 어떻게 내려갈 것인가 | 폴백 사다리, 재시도, 사람 에스컬레이션 |
다섯 축은 독립적이지 않다. 지연 예산이 비용 예산을 제약하고, 실패 처리는 관측이 없으면 설계할 수 없다. 그래서 한 곳(보통 요청 단위 budget 객체)에 모아두는 것이 실무에서 가장 덜 깨진다.
모델 호출은 느리다. 그러므로 "언제까지 기다리고, 어디서 포기할지"를 먼저 정한다.
모델/도구 호출은 최소 세 계층의 timeout을 분리한다.
전체 응답이 8초 걸려도 첫 토큰이 400ms에 나오면 사용자 체감은 전혀 다르다. 스트리밍(SSE 또는 WebSocket)은 안정성 설계의 일부다. 단, 스트리밍 중간에 실패하면 "이미 일부를 보여준 상태"라는 새 실패 모드가 생긴다. 부분 출력 후 끊겼을 때 어떻게 마감할지(에러 토큰 주입, 재생성 버튼, 마지막 문장 절단 표시)를 미리 정한다.
요청 시작
├─ 첫 토큰까지 timeout: 5s (안 오면 폴백)
├─ 전체 완료 timeout: 30s
└─ 요청 wall-clock budget: 40s (도구 호출 포함 전체)AI 백엔드에서 비용 폭주는 "느린 장애"다. 모니터링 그래프보다 다음 달 청구서에서 먼저 보인다. 그래서 비용을 런타임 가드로 다룬다.
한 사용자 발화당 다음을 모두 건다.
가드에 걸리면 예외가 아니라 정해진 폴백 응답 + 사람 에스컬레이션으로 내려간다. "예산 초과로 죽었습니다"가 사용자에게 가서는 안 된다.
비용과 안정성을 동시에 잡는 두 수단.
모델은 "무엇을 할 수 있는지"를 알 필요가 없고, 알게 해서도 안 된다. 권한은 항상 현재 인증된 사용자 컨텍스트에서 검증한다. 자세한 dispatcher 구조는 Tool Calling 문서에 있으므로 여기서는 안정성 관점의 원칙만 짚는다.
권한이 안정성 주제인 이유는, 권한 사고가 데이터 유출/오작동이라는 가장 비싼 장애로 직결되기 때문이다.
AI 백엔드는 "왜 이 답이 나왔는가"를 사후에 재구성할 수 없으면 개선도 사고 대응도 못 한다. 일반 관측성 기본기와 SLO·에러 예산·인시던트 대응에 더해, AI 특화 항목을 남긴다.
한 사용자 발화를 하나의 correlation id로 묶어 audit JSONL에 적재한다.
일반 RED(Rate/Error/Duration) 메트릭에 더해 본다.
audit는 운영뿐 아니라 모델 평가의 입력이다. 이 데이터로 어떤 분류가 자주 틀리고 어떤 도구의 인자 환각이 잦은지를 정량으로 본다. 평가 루프 설계는 LLM 평가 프레임워크와 연결된다.
실패는 예외가 아니라 일정 확률로 항상 일어나는 사건이다. 핵심은 "전파를 어디서 끊고, 어떻게 한 단계씩 우아하게 내려갈지"다.
가장 좋은 응답에서 최소한의 응답까지 단계를 미리 정의한다.
1. 큰 모델 + 전체 도구 (정상)
2. 작은 모델 + 전체 도구 (큰 모델 429/지연 시)
3. 작은 모델 + 캐시된 결과만 (외부 도구 장애 시)
4. 템플릿/규칙 기반 응답 (모델 전체 장애 시)
5. 사람 상담사로 에스컬레이션 (위 전부 실패 또는 민감 케이스)각 단계 전환 조건(어떤 신호에서 한 칸 내려가는가)을 코드로 명시한다. "되는 데까지 해보다가 터지면 500"이 가장 나쁜 패턴이다.
여러 외부 호출 중 일부만 성공한 경우, observation 페이로드에 부분 성공을 명시해 모델이 "모두 완료"라고 단정하지 않게 한다.
{ "sms": "sent", "push": "queue_failed" }모델 답변은 "문자로는 보냈고 앱 알림은 잠시 후 다시 시도하겠다"가 되어야 한다.
AI 제품에서 "사람에게 넘긴다"는 장애 처리의 마지막 수단이 아니라 설계된 정상 경로 중 하나다. 다음을 미리 정한다.
에스컬레이션 경로가 없는 AI 백엔드는 나쁜 케이스에서 무조건 사용자에게 실패를 떠넘긴다.
// 안티패턴: 종료 조건이 "모델이 끝났다고 할 때"뿐
while (!response.isFinal()) {
response = llm.next();
dispatch(response.toolCall());
}모델이 같은 도구를 반복 호출하면 토큰·비용·지연이 무한히 늘어난다. 장애가 청구서로 드러난다.
for (int step = 0; step < ctx.maxSteps(); step++) {
LlmResponse r = session.next(ctx.budget()); // 토큰·시간·비용 예산 주입
if (r.isFinal()) return AgentReply.text(r.text());
dispatcher.dispatch(r.toolCall(), ctx.toToolContext());
if (ctx.budget().exhausted()) break; // 예산 가드
}
return AgentReply.escalate("budget_exceeded"); // 우아한 마감// 안티패턴
String answer = llm.complete(prompt); // 제공자 5xx면 그대로 예외 → 사용자 500
return answer;폴백 사다리를 탄다. 큰 모델 실패 → 작은 모델 → 캐시된 결과 → 템플릿 응답 → 사람 에스컬레이션 순으로 한 칸씩 내려간다. 어느 단계에서 내려갔는지 audit에 남긴다.
spring-boot-starter-web, spring-boot-starter-validation, resilience4j-retry, resilience4j-circuitbreaker, micrometer-tracinglogs/ai-audit.jsonl에 줄 단위 JSON으로 적재하고 jq로 폴백 발동율·평균 토큰을 집계한다.손으로 굴려보며 실패 모드를 익히는 데 좋은 시나리오.
각 시나리오는 fake LLM 기반 통합 테스트로 묶어 회귀 보호한다. 모델 응답이 바뀌어도 안정성 가드가 동작하는지를 결정적으로 검증할 수 있다.
시니어 백엔드 관점에서 스스로 던져볼 질문과 답의 뼈대.
비결정성·가변 지연·실시간 토큰 비용·외부 모델 의존을 전제로 깔아야 한다는 점. 그래서 "정상 응답의 정의"와 "나빠질 때 내려가는 단계"를 우리가 직접 설계한다.
요청 단위 step/토큰/시간/비용 가드를 한 budget 객체에 모으고, 초과 시 폴백·에스컬레이션으로 내려간다. 모델 폴백 사다리와 캐싱으로 평소 비용을 낮추되, 폴백 방향은 재시도 총비용으로 판단한다.
폴백 사다리로 한 칸씩 우아하게 내려간다. 재시도는 부작용 여부로 가르고, 부작용 도구는 idempotency key가 있을 때만 재시도한다. 부분 성공은 정직하게 모델에 전달한다.
correlation id로 한 발화를 묶어 분류·노출 도구·tool call·검증·실행·토큰·비용·최종 답변까지 audit JSONL에 남긴다. 폴백 발동율·검증 거절율·요청당 비용을 LLM 특화 메트릭으로 본다. 이 데이터는 모델 평가의 입력이기도 하다.