스트리밍 UI 복원력 설계: 부분 응답 복구와 지수 백오프 재시도 패턴
Claude 스트리밍 응답 중 네트워크 단절이 발생하면 사용자는 빈 화면을 보게 된다. 부분 수신된 텍스트를 보존하고 중단 지점부터 재개하는 복구 전략과, 529·529·overloaded 오류에 특화된 재시도 로직을 설계한다.
스트리밍 실패의 세 가지 시나리오
프로덕션에서 스트리밍이 중단되는 주요 원인과 특성은 다음과 같다.
| 시나리오 | HTTP 상태 | 재시도 가능 | 평균 발생 빈도 | |---|---|---|---| | 네트워크 타임아웃 | 408/504 | 즉시 가능 | 0.3~0.8% | | 서버 과부하 | 529 | 대기 후 가능 | 트래픽 피크 시 2~5% | | 스트림 도중 연결 끊김 | N/A (소켓 닫힘) | 부분 복구 필요 | 0.1~0.5% |
핵심 문제: 스트림 도중 단절 시 이미 수신한 텍스트를 버리고 처음부터 재요청하면 비용이 2배가 되고 사용자 경험도 저하된다.
부분 응답 보존 + 재개 패턴
수신된 텍스트를 누적 저장하고, 재시도 시 messages에 부분 응답을 assistant 턴으로 삽입해 이어서 생성을 요청한다. Claude는 이전 맥락을 이어받아 중복 없이 계속 생성한다.
import anthropic
import time
from typing import Generator
client = anthropic.Anthropic()
def stream_with_resume(
messages: list,
max_retries: int = 3,
model: str = "claude-opus-4-5"
) -> Generator[str, None, None]:
partial_text = ""
attempt = 0
while attempt <= max_retries:
try:
# 부분 수신된 텍스트가 있으면 assistant 턴으로 삽입
effective_messages = messages.copy()
if partial_text:
effective_messages.append(
{"role": "assistant", "content": partial_text}
)
with client.messages.stream(
model=model,
max_tokens=2048,
messages=effective_messages
) as stream:
for text_chunk in stream.text_stream:
partial_text += text_chunk
yield text_chunk
return # 정상 완료
except (anthropic.APIStatusError, anthropic.APIConnectionError) as e:
status = getattr(e, 'status_code', 0)
attempt += 1
if status == 529 or status == 503:
# 과부하: 지수 백오프 (2^attempt * 1s, 최대 30s)
wait = min(2 ** attempt, 30)
print(f"과부하 오류. {wait}s 대기 후 재시도 ({attempt}/{max_retries})")
time.sleep(wait)
elif status in (408, 504) or isinstance(e, anthropic.APIConnectionError):
# 타임아웃/연결 오류: 짧은 대기
time.sleep(1)
else:
raise # 4xx 클라이언트 오류는 재시도 불필요
if attempt > max_retries:
raise RuntimeError(f"최대 재시도 초과. 부분 수신: {len(partial_text)}자")
운영 설계 원칙과 트레이드오프
부분 재개의 한계: Claude가 부분 텍스트를 이어받을 때 문장 중간에서 시작하면 의미적 일관성이 깨질 수 있다. 안전한 재개 지점은 문장 종료 기호(. ? !) 또는 단락 경계까지만 partial_text로 유지하고 나머지는 버리는 방식이 낫다.
비용 트레이드오프: 부분 재개 시 이전 partial_text가 입력 토큰으로 재과금된다. partial_text가 500토큰 미만이면 재개가 유리하고, 1500토큰을 넘으면 처음부터 재시도하는 편이 비용상 비슷해진다.
운영 체크리스트:
- [ ] 스트림 오류율을
stream_error_rate메트릭으로 분리 추적 (타임아웃 vs 과부하 vs 연결 끊김) - [ ] 재시도 횟수 > 1인 요청 비율이 1% 초과 시 알람
- [ ] partial_text 재개 시 토큰 수 로깅으로 비용 이중과금 규모 파악
- [ ] UI 레이어에서 재시도 중임을 사용자에게 표시 (스피너 유지)
- [ ] 최대 재시도 초과 시 partial_text를 사용자에게 표시하고 "계속 생성" 버튼 제공