LLM Client Adapter와 Provider 경계
3줄 요약
- LLM API는 컨트롤러에서 바로 호출하지 않고 adapter/client 계층으로 감싸야 한다.
- provider별 SDK와 응답 형식은 바뀔 수 있으므로 서비스 계층은 내부 계약에 의존하는 편이 안전하다.
- 백엔드에서는 모델 호출을 외부 시스템 호출로 보고 timeout, 예외 변환, logging, trace를 경계에서 처리한다.
핵심 정리
- LLM provider는 결제사, SMS, 지도 API 같은 외부 시스템이다. 장애, latency, quota, 응답 형식 변경을 전제로 설계해야 한다.
- Controller가 SDK를 직접 호출하면 테스트, fallback, provider 교체, 로깅, 예외 변환이 어려워진다.
- 내부 서비스는
LlmClient 같은 포트에 의존하고, provider 구현은 adapter로 숨긴다.
- 응답 DTO는 provider 원본을 그대로 노출하지 않는다. 내부에서 필요한 text, usage, finish reason, refusal, raw id 정도를 명시적으로 매핑한다.
- 모델 호출 경계에서는 request id, prompt version, model, token usage, elapsed time, error type을 남긴다.
헷갈리는 지점
- LLM SDK가 편하므로 서비스에서 바로 써도 된다고 생각하기 쉽다. 샘플 코드가 대부분 그렇게 보이기 때문이다.
- 핵심은 샘플 코드와 운영 경계는 다르다는 점이다.
- provider 교체, fallback, 장애 대응을 하려면 adapter 경계가 필요하다.
- 모델 응답을 그대로 프론트에 내려도 된다고 생각하기 쉽다. 처음에는 text만 필요하기 때문이다.
- 핵심은 사용량, 거절, 중단, schema 실패 같은 운영 정보도 응답 해석에 필요하다는 점이다.
확인 질문
- LLM 호출을 adapter로 감싸야 하는 이유는 무엇인가?
- provider SDK 변경, 예외 변환, timeout, fallback, 테스트, 관측성을 한 경계에서 관리하기 위해서다.
- 서비스 계층이 provider 원본 응답에 직접 의존하면 어떤 문제가 생기는가?
- provider 교체와 응답 형식 변경에 서비스 로직이 흔들리고, 장애 대응 정책을 일관되게 적용하기 어렵다.
Timeout Retry Rate Limit Circuit Breaker
3줄 요약
- LLM 호출은 긴 latency와 외부 quota를 가지므로 timeout, retry, rate limit, circuit breaker를 기본 설계에 넣어야 한다.
- retry는 일시 장애와 rate limit에만 제한적으로 적용하고, side effect가 있는 tool 실행에는 idempotency 없이 적용하지 않는다.
- circuit breaker와 degrade mode는 provider 장애가 전체 백엔드 장애로 번지는 것을 막는다.
핵심 정리
- timeout은 모델 호출 timeout과 전체 API timeout을 나눠서 본다. 모델 호출이 전체 요청 시간을 모두 잡아먹으면 사용자 경험과 서버 리소스가 함께 나빠진다.
- retry는 네트워크 일시 실패, 429, 5xx 같은 재시도 가능한 실패에 적용한다. validation 실패나 정책 거절은 같은 요청을 반복해도 해결되지 않을 수 있다.
- rate limit은 provider quota만의 문제가 아니다. 사용자별, 테넌트별, 기능별, 서비스 전체 예산을 함께 제한해야 한다.
- circuit breaker는 provider 장애가 길어질 때 빠르게 실패하거나 fallback으로 우회하게 해 서버 자원 고갈을 막는다.
- retry storm을 막으려면 exponential backoff, jitter, max attempts가 필요하다.
헷갈리는 지점
- retry를 많이 넣으면 안정성이 높아진다고 생각하기 쉽다. 일시 장애에는 실제로 효과가 있기 때문이다.
- 핵심은 retry가 트래픽과 비용을 증폭시킨다는 점이다.
- rate limit 상황에서 무제한 retry는 장애를 더 키운다.
- timeout을 길게 잡으면 성공률이 올라간다고 생각하기 쉽다. 모델이 늦게라도 답할 수 있기 때문이다.
- 핵심은 서버 thread, connection, 사용자 대기 시간이 함께 늘어난다는 점이다.
- API 용도에 맞는 실패 시간과 비동기 전환 기준을 정해야 한다.
확인 질문
- retry를 적용하기 전에 확인해야 할 것은 무엇인가?
- 실패가 재시도 가능한지, side effect가 없는지, 비용과 rate limit을 감당할 수 있는지 확인해야 한다.
- circuit breaker의 목적은 무엇인가?
- 외부 provider 장애가 내부 서버 자원 고갈과 연쇄 장애로 번지는 것을 막는 것이다.
Streaming과 비동기 Queue 처리
3줄 요약
- streaming은 사용자가 빨리 반응을 보기 좋지만, 연결 끊김과 partial output 저장 문제를 설계해야 한다.
- 긴 작업, 대량 요약, 배치 RAG 색인은 동기 API보다 queue와 worker로 분리하는 편이 안전하다.
- 백엔드 서버는 사용자 요청을 접수하고 상태를 관리하며, worker는 모델 호출과 후처리를 담당하게 나눌 수 있다.
핵심 정리
- streaming은 first token latency를 줄이지만 전체 작업이 빨라지는 것은 아니다.
- streaming 중 클라이언트 연결이 끊기면 모델 호출을 계속할지 취소할지, partial output을 저장할지 버릴지 정해야 한다.
- queue 기반 처리는 API 서버의 리소스를 보호하고 재시도와 상태 추적을 쉽게 만든다.
- 작업이 오래 걸리면
202 Accepted와 job id를 반환하고, 클라이언트가 상태를 조회하거나 websocket/SSE로 결과를 받게 한다.
- worker에는 concurrency 제한, rate limit, dead letter queue, idempotency key가 필요하다.
헷갈리는 지점
- streaming을 쓰면 장애가 줄어든다고 생각하기 쉽다. 사용자는 빨리 응답을 보기 때문이다.
- 핵심은 streaming은 UX 개선이지 장애 대응 자체가 아니라는 점이다.
- 연결 끊김, 부분 저장, 재시도 정책은 별도로 필요하다.
- queue를 넣으면 모든 문제가 해결된다고 생각하기 쉽다. 서버 부하가 분리되기 때문이다.
- 핵심은 queue도 운영 대상이라는 점이다.
- 적체, 중복 처리, DLQ, 작업 취소, 상태 조회를 함께 설계해야 한다.
확인 질문
- streaming 중 연결이 끊겼을 때 정해야 할 정책은 무엇인가?
- 모델 호출 취소 여부, partial output 저장 여부, 사용자에게 보여줄 상태, 재시도 가능성을 정해야 한다.
- 긴 AI 작업을 queue로 분리하는 이유는 무엇인가?
- API 서버 리소스를 보호하고, 작업 상태, 재시도, rate limit, 실패 처리를 독립적으로 관리하기 위해서다.
비용 Token Budget Prompt Caching Secret 관리
3줄 요약
- LLM 비용은 요청 수뿐 아니라 input token, output token, retry, eval, embedding, logging에 의해 늘어난다.
- token budget은 기능별, 사용자별, 테넌트별로 제한해야 하며 prompt caching이 가능하도록 입력 구조를 설계할 수 있다.
- API key와 prompt/context 로그는 민감 자산이므로 secret 관리와 마스킹 정책이 필요하다.
핵심 정리
- 비용 폭증은 단순히 사용자가 많아서만 생기지 않는다. 긴 context, retry storm, 무제한 output, batch eval, 중복 indexing도 비용을 만든다.
- token budget은 요청 전 추정, 요청 후 사용량 기록, quota 차감으로 나눠 관리한다.
- prompt caching은 고정 prefix가 반복될수록 유리하다. 매 요청마다 앞부분이 달라지면 캐시 효율이 떨어진다.
- API key는 코드, 노트, 로그, 프론트엔드 번들에 들어가면 안 된다.
- 모델 로그에는 개인정보와 내부 문서가 섞일 수 있으므로 원문 저장을 기본값으로 삼지 않는다.
헷갈리는 지점
- 저렴한 모델을 쓰면 비용 문제가 해결된다고 생각하기 쉽다. 단가가 낮기 때문이다.
- 핵심은 retry, 긴 context, 대량 트래픽이 단가 차이를 쉽게 이긴다는 점이다.
- 기능별 budget과 token 관측성이 먼저 필요하다.
- prompt caching은 자동으로 항상 이득이라고 생각하기 쉽다. 이름이 캐시이기 때문이다.
- 핵심은 입력 구조가 캐시 친화적이어야 한다는 점이다.
- 고정 instruction과 자주 바뀌는 user input을 분리해야 한다.
확인 질문
- LLM 비용을 기능별로 관리하려면 무엇을 기록해야 하는가?
- model, prompt version, input token, output token, retry count, user 또는 tenant, 기능명을 기록해야 한다.
- API key 관리에서 가장 피해야 할 것은 무엇인가?
- 클라이언트 코드, 공개 저장소, 일반 로그에 key가 노출되는 것이다.
Spring Boot와 Python Worker 통합
3줄 요약
- Java/Spring Boot API 서버와 Python AI worker를 분리하면 백엔드 안정성과 AI 생태계 활용을 함께 얻을 수 있다.
- 동기 응답이 필요한 얇은 기능은 Spring adapter에서 직접 호출하고, 긴 작업은 queue를 통해 worker로 넘기는 하이브리드 구조가 실용적이다.
- 통합의 핵심은 언어가 아니라 job 계약, idempotency, 상태 저장, 관측성이다.
핵심 정리
- Spring Boot는 인증, 권한, 트랜잭션, API 계약, 사용자 요청 관리에 강하다.
- Python은 AI SDK, RAG, eval, 데이터 처리 생태계가 풍부하다.
- 둘을 무리하게 한 런타임에 넣기보다 API 서버와 worker를 분리하면 장애 격리와 배포 독립성이 좋아진다.
- job payload에는 user id, tenant id, prompt version, model profile, idempotency key, callback 또는 result location이 들어간다.
- worker 결과는 DB나 object storage에 저장하고, API 서버가 상태를 조회해 사용자에게 보여준다.
헷갈리는 지점
- AI 기능이 있으니 전체 서버를 Python으로 바꿔야 한다고 생각하기 쉽다. Python 예제가 많기 때문이다.
- 핵심은 기존 백엔드의 강점을 유지하면서 AI 작업을 분리할 수 있다는 점이다.
- 인증, 권한, 트랜잭션은 Spring에서, 모델 호출과 RAG 파이프라인은 worker에서 맡길 수 있다.
- worker를 분리하면 복잡도만 늘어난다고 생각하기 쉽다. 배포 대상이 하나 더 생기기 때문이다.
- 핵심은 긴 AI 작업이 API 서버를 점유하는 비용과 비교해야 한다는 점이다.
- 고객-facing 서비스에서는 장애 격리가 복잡도를 정당화할 수 있다.
확인 질문
- Spring Boot와 Python worker를 나누는 실전 이유는 무엇인가?
- API 안정성, 인증/권한 관리, AI 생태계 활용, 장애 격리, 긴 작업 처리 때문이다.
- 두 런타임을 연결할 때 가장 중요한 계약은 무엇인가?
- job payload, idempotency key, 상태 저장, 실패 코드, trace id 계약이다.