SDK는 REST API를 감싼 얇은 층이고, 그 아래를 알아야 문제가 났을 때 고칠 수 있습니다. 이 수업에서는 SDK와 HTTP의 관계, 타임아웃과 재시도, 멱등성 같은 연동 기초를 다룹니다.
SDK 아래에는 HTTP가 있다
client.messages.create(...)는 결국 하나의 HTTP 요청입니다. SDK가 해 주는 일은 인증 헤더 붙이기, 본문 직렬화, 응답 역직렬화, 그리고 일부 재시도입니다.
이 사실이 중요한 이유는 SDK가 감춘 것도 우리가 책임져야 하기 때문입니다.
- 요청 본문이 크면 전송 시간이 듭니다. 긴 문서를 매번 보내면 모델이 생각하기 전에 이미 시간이 갑니다.
- 프록시나 방화벽이 중간에 있으면 SDK 설정과 별개로 막힐 수 있습니다.
- 타임아웃은 클라이언트, 프록시, 서버 각각에 있습니다. 가장 짧은 것이 실질 한도입니다.
client = anthropic.Anthropic(
timeout=60.0, # 요청 하나가 기다릴 최대 시간
max_retries=2, # SDK가 자체로 하는 재시도 횟수
)
SDK의 기본 재시도를 모르면 재시도가 이중으로 걸립니다. 우리 코드에서 3회 재시도를 감싸고 SDK가 2회를 하면 최악의 경우 6번 호출됩니다. 어느 층에서 할지 정하고 다른 쪽은 꺼 두는 편이 예측 가능합니다.
타임아웃을 어떻게 잡을 것인가
타임아웃이 너무 짧으면 정상 응답을 끊어 버리고, 너무 길면 장애가 났을 때 요청이 쌓입니다.
기준을 잡는 방법은 실측입니다. 대표적인 요청을 여러 번 돌려 소요 시간 분포를 보고, 상위 구간보다 넉넉하게 잡습니다. 출력이 길수록 오래 걸리므로 max_tokens에 따라 다르게 잡는 것도 방법입니다.
스트리밍을 쓰면 판단이 달라집니다. 첫 조각이 빨리 오므로 전체 완료 시간이 아니라 조각 사이의 간격으로 이상을 감지할 수 있습니다.
한 가지 자주 놓치는 것은 사용자 쪽 타임아웃입니다. 웹 요청 처리기에 30초 제한이 걸려 있는데 모델 호출에 60초를 주면, 우리 타임아웃이 오기 전에 바깥에서 끊깁니다. 그때 모델 호출은 계속 진행 중이고 비용도 발생합니다. 바깥 한도보다 안쪽을 짧게 잡는 것이 원칙입니다.
멱등성과 중복 실행
재시도를 넣으면 같은 작업이 두 번 실행될 수 있습니다. 요청은 실패했다고 판단했는데 서버에서는 처리된 경우입니다.
모델 호출 자체는 대개 부작용이 없어 두 번 불러도 비용만 더 듭니다. 문제는 도구 실행입니다. 이메일 발송이나 결제 같은 도구를 재시도하면 두 번 나갑니다.
def execute_tool(name: str, args: dict, call_id: str):
if name in SIDE_EFFECT_TOOLS:
# 같은 호출 id로 이미 실행했으면 결과만 돌려준다
if cached := results.get(call_id):
return cached
out = do_it(args)
results[call_id] = out
return out
return do_it(args)
도구 호출에는 고유 식별자가 함께 오므로 그것을 키로 쓰면 됩니다. 부작용이 있는 도구는 목록으로 관리하고 그것들만 이 처리를 거치게 하는 편이 단순합니다.
배치 처리에서도 같은 문제가 있습니다. 같은 입력을 두 번 제출하면 두 번 처리됩니다. 제출 전에 이미 보낸 것인지 확인하는 기록이 필요합니다.
실시간 연결과 스트리밍
사용자에게 결과를 흘려 보내려면 서버가 조각을 받아 다시 클라이언트로 밀어야 합니다. 흔히 쓰는 방식은 서버가 보내는 단방향 이벤트 스트림이고, 양방향이 필요하면 웹소켓을 씁니다.
여기서 챙길 것이 몇 가지 있습니다.
- 연결이 끊기면 어디까지 보냈는지 알아야 재개하거나 처음부터 다시 할지 정할 수 있습니다.
- 버퍼링하는 중간 장비가 있으면 조각이 모여서 한꺼번에 도착해 스트리밍의 이점이 사라집니다.
- 사용자가 중간에 취소하면 모델 호출도 중단해야 합니다. 안 그러면 아무도 보지 않는 응답에 비용을 냅니다.
async def stream_to_client(messages, request):
async with aclient.messages.stream(model=MODEL, max_tokens=1024,
messages=messages) as stream:
async for text in stream.text_stream:
if await request.is_disconnected():
break # 사용자가 떠나면 중단한다
yield text
취소 처리가 없으면 사용자가 새로고침을 반복할 때마다 이전 호출이 계속 돌아 비용이 배로 듭니다.
시험 함정
- SDK를 쓰면 HTTP 계층을 신경 쓸 필요가 없다는 서술 — 타임아웃과 프록시, 본문 크기는 여전히 우리 몫입니다.
- 우리 코드와 SDK 양쪽에 재시도를 두는 구성 — 호출 횟수가 곱해집니다.
- 여러 층의 타임아웃 중 가장 긴 값이 실질 한도라는 서술 — 가장 짧은 것이 먼저 끊습니다.
- 바깥 요청 처리기보다 모델 호출 타임아웃을 길게 잡는 설정 — 바깥에서 먼저 끊기고 호출은 계속됩니다.
- 재시도가 모델 호출에만 영향을 준다는 가정 — 부작용이 있는 도구는 중복 실행됩니다.
- 사용자가 이탈해도 스트리밍을 끝까지 진행하는 구현 — 아무도 보지 않는 응답에 비용이 듭니다.
실습 시나리오
실제 시험과 같은 형식의 시나리오 문제입니다.
웹 요청 처리기에 30초 제한이 걸린 서비스에서 모델 호출 타임아웃을 90초로 설정했습니다. 장애 시 나타날 현상으로 가장 적절한 것은 무엇입니까?
빌드 연습 · 타임아웃·재시도·취소를 갖춘 호출 계층 만들기
약 35분1.소요 시간 실측
대표 요청 20건의 소요 시간 분포를 기록합니다.
기대 결과 · 중앙값과 상위 구간이 수치로 남습니다.
2.타임아웃 설정
실측을 근거로 값을 정하고 바깥 제한보다 짧게 맞춥니다.
기대 결과 · 설정 근거가 주석으로 남아 있습니다.
3.재시도 일원화
SDK 재시도를 끄고 우리 층에서만 재시도하도록 정리합니다.
기대 결과 · 총 호출 횟수가 예측 가능합니다.
4.멱등 처리
부작용 도구를 목록으로 관리하고 호출 식별자로 중복을 막습니다.
기대 결과 · 같은 호출을 두 번 실행해도 한 번만 수행됩니다.
5.취소 처리
스트리밍 중 연결이 끊기면 중단하도록 구현합니다.
기대 결과 · 이탈 시 이후 토큰이 생성되지 않습니다.