SiCore TokenWorks
LLM APIAPI Gateway

8341Token工厂:네 곳의 대형 모델API연동 후 스트리밍 지연,근본 원인은 게이트웨이 계층

SiCore TokenWorks Team·2026-10-24

작년 겨울, 지능형 고객 서비스를 만드는 팀의 온라인 문제를 함께 조사한 적이 있다. 그들의 백엔드는 네 개의 대형 모델 API를 동시에 연결했고, 프론트엔드는 SSE로 스트리밍 출력을 구현했다. 서비스 오픈 사흘째부터 사용자들이 "답변이 치약 짜듯 나온다"고 피드백했다. 어떤 때는 한 단락의 텍스트가 멈춰 있다가 갑자기 한 덩어리로 쏟아져 나왔고, 어떤 때는 한 문장이 두 동강 나면서 중간에 정체불명의 빈 블록이 끼어들었다. 백엔드 로그에는 5xx가 없었고, 각 API의 가용성 모니터링은 모두 정상이었다.

이런 문제는 가장 찾기 어렵다. 모든 구간이 정상으로 보이기 때문이다. 나는 그들과 함께 브라우저 Network 패널부터 시작해 한 층씩 파고들었고, 결국 문제가 특정 API에 있는 게 아니라 "네 개를 혼용하는 것" 자체에 있다는 것을 발견했다.

먼저 결론부터: 스트리밍 프로토콜 불통일, 이것이 집계 연동의 숨은 함정

대형 모델 API의 스트리밍 출력은 주류가 모두 SSE(Server-Sent Events)를 사용한다. 하지만 "모두 SSE를 쓴다"는 것이 "동작이 일치한다"는 뜻은 아니다. 네 개 벤더는 청크 단위, 종료 표시, 하트비트 전략에서 각자 다른 구현을 가지고 있고, 프론트엔드가 받는 바이트 스트림은 모두 data:로 시작하는 것처럼 보이지만 파싱 로직은 전혀 재사용할 수 없다. 이것이 바로 끊김과 청크 경계 이상의 근원이다.

계층별점검: 프론트엔드부터 게이트웨이까지, 문제는 어느 계층에 있는가

첫 번째 계층, 프론트엔드. 그들의 SSE 파싱은 표준 EventSource를 사용해 \n\n으로 이벤트를 분리했다. 이것 자체는 문제가 없다.

두 번째 계층, 백엔드 포워딩. 백엔드는 Node로 투명 전달 계층을 만들어 업스트림의 ReadableStream을 응답에 직접 pipe했다. 여기에 첫 번째 지뢰가 묻혀 있었다. 벤더마다 chunk 크기 차이가 큰데, 어떤 곳은 토큰 단위로 보내고 어떤 곳은 몇 개 토큰을 모아서 보낸다. pipe 과정에서 Buffer.concat을 했는데 UTF-8 멀티바이트 문자의 청크 경계 처리를 제대로 하지 않으면 한글 음절이 잘려서 프론트엔드에 깨진 문자 블록으로 렌더링된다.

세 번째 계층, 타임아웃과 재시도. 백엔드는 네 개 벤더에 통일된 30초 타임아웃을 설정했다. 하지만 스트리밍 시나리오에서 "30초 타임아웃"은 첫 바이트 시간을 말하는 것인가, 전체 스트림 시간을 말하는 것인가? 그들은 구분하지 않았고, 그 결과 긴 답변이 중간에 끊겨서 프론트엔드에서는 "말하다가 갑자기 멈춤"으로 나타났다.

네 번째 계층, 오류 코드. 네 개 벤더의 속도 제한, 인증 실패, 콘텐츠 검열 차단이 반환하는 HTTP 상태 코드와 body 구조가 모두 다르다. 백엔드는 통일되게 error.message로 가져왔는데, 결과적으로 두 곳에서는 가져올 수 있었고 두 곳에서는 undefined가 나와서 프론트엔드에 빈 오류 메시지가 표시되었다.

여기까지 조사하니 명확해졌다. 진짜 문제는 특정 API에 버그가 있는 것이 아니라, 네 개의 프로토콜 의미가 비즈니스 계층에 그대로 노출된 것이다.

네 개 벤더의 스트리밍 구현 핵심 차이

비교표를 정리했다. 데이터는 각 벤더의 공식 문서와 실제 패킷 캡처에서 나왔으며, 공식적으로 공개되지 않은 부분은 표시했다:

차원벤더A벤더B벤더C벤더D

종료 표시data: [DONE]finish_reason 필드data: [DONE]event: done 이벤트

하트비트없음약 15초 주석 행없음공식 미공개

청크 단위토큰 단위2-4 토큰 모음토큰 단위불규칙

오류 코드 위치HTTP 상태 코드body.error.codeHTTP 상태 코드body.code

이 표를 보면 왜 프론트엔드 파싱 로직을 재사용할 수 없는지 알 수 있다. 종료 표시가 세 가지 방식이고, 오류 코드가 두 위치에 있으며, 하트비트 전략은 두 곳은 있고 두 곳은 없다. 비즈니스 코드에서 직접 if (chunk.includes('[DONE]'))라고 하면 벤더B를 만났을 때 영원히 끝나지 않고 연결이 타임아웃까지 매달린다.

게이트웨이 계층에서 통합하는 네 가지 설계 포인트

첫째, SSE 이벤트 형식 통일. 게이트웨이는 업스트림 각 벤더의 이벤트를 모두 하나의 구조로 정규화한다: {type: "delta" | "done" | "error", content, code}. 비즈니스 계층은 이 한 세트만 인식하고 업스트림이 누구인지 더 이상 신경 쓰지 않는다. 우리 프로젝트에서 사용한 집계 계층 token8341대형 모델API집계 플랫폼이 바로 이렇게 하는데, 외부에는 하나의 스트리밍 프로토콜만 노출하므로 모델을 전환할 때 비즈니스 코드를 변경할 필요가 없다.

둘째, 인증을 게이트웨이로 수렴. 네 개 벤더의 네 세트 Key가 비즈니스 설정에 흩어져 있으면 교체할 때마다 네 곳을 수정해야 한다. Key를 게이트웨이에 모으면 비즈니스 계층은 내부 token 하나만 받으면 된다. token8341대형 모델API집계 플랫폼 같은 집계 계층의 가치가 바로 여기에 있다. 하나의 Key로 여러 모델을 호출하고, Key 교체와 할당량 제어를 모두 게이트웨이 측에서 완료한다.

셋째, 타임아웃은 두 구간으로 나누어 설정. 첫 바이트 타임아웃(TTFB)과 스트림 유휴 타임아웃을 분리한다. TTFB는 짧게, 예를 들어 10초로 설정해 빠르게 실패하고, 스트림 유휴 타임아웃은 길게, 예를 들어 60초로 설정해 모델이 사고할 시간을 준다. 둘을 30초로 공유하면 반드시 문제가 생긴다.

넷째, 재시도는 멱등 오류에만. 스트리밍 요청은 출력이 시작되면 재시도가 중복 콘텐츠를 초래한다. 게이트웨이 계층은 이를 구분해야 한다. 연결 수립 단계의 실패는 재시도할 수 있고, 스트림이 이미 출력을 시작한 후의 실패는 프론트엔드에 오류를 보고해 사용자가 수동으로 다시 보내도록 해야 한다. 이 경계를 명확히 하지 않으면 "같은 문장이 두 번 말해지는" 현상이 발생한다.

오류 코드 매핑 테이블은 직접 유지해야 한다

네 개 벤더의 속도 제한 코드를 보면, 두 곳은 429를 반환하고, 한 곳은 200을 반환하지만 body에 오류를 담고, 한 곳은 커스텀 코드를 반환한다. 게이트웨이 계층은 반드시 매핑 테이블을 만들어 업스트림의 다양한 오류를 내부 오류 코드로 통일해야 한다. 예를 들어 RATE_LIMIT, AUTH_FAILED, CONTENT_BLOCKED 등이다. 비즈니스 계층은 내부 코드에 따라 폴백하고 더 이상 업스트림 의미를 추측하지 않는다. 이 표에는 지름길이 없고, 벤더마다 패킷을 캡처해 문서와 대조하며 정리해야 한다. token8341대형 모델API집계 플랫폼은 문서에 바로 사용할 수 있는 매핑 참고 자료를 제공해서 수고를 덜어주었다.

어떤 경우에는 직접 연결이 오히려 더 간단하다

인정해야 한다. 모든 시나리오에서 게이트웨이를 도입할 가치가 있는 것은 아니다. 프로젝트가 하나의 모델만 연결하거나 비스트리밍 배치 추론만 한다면, 공식 SDK에 직접 연결하는 것이 코드가 가장 적고, 링크가 가장 짧으며,점검이 가장 간단하다. 게이트웨이 계층은 "다중 모델 통합 연동"이 가져오는 복잡성을 해결하는 것이지, 단일 모델 시나리오에서 게이트웨이를 도입하면 포워딩 계층이 하나 더 늘고 장애 지점이 하나 더 늘어난다. 판단 기준은 명확하다. 연결하는 모델이 두 개를 넘거나 런타임에 모델을 전환해야 할 때만 게이트웨이의 이익이 비용을 능가한다.

마치며

그 장애는 결국 게이트웨이 계층을 추가해 통합하면서 해결되었고, 프론트엔드 SSE 파싱 로직은 네 개 분기에서 하나로 줄었으며, 끊김과 청크 이상도 함께 사라졌다. 돌아보면 문제는 결코 특정 대형 모델 API에 있지 않았고, "여러 벤더의 프로토콜 차이를 비즈니스 계층에 그대로 전달한 것"에 있었다. 여러분도 다중 모델 연동을 하고 있다면, 먼저 스트리밍 프로토콜, 오류 코드, 인증, 타임아웃 이 네 가지를 게이트웨이 계층에서 정렬한 후에 상위 기능을 논하라.

저자: 쑨하오란

발표일: 2026년 10월 25일