실시간 AI 스트리밍: WebSocket으로 완성하는 끊기지 않는 대화

실시간 AI 경험에서 중요한 건 단순히 모델이 얼마나 빨리 답을 내놓느냐가 아닙니다. 사용자가 체감하는 품질은 “정답이 만들어지는 과정을 얼마나 자연스럽게 보여주느냐”에서 더 크게 드러납니다. LLM(대규모 언어모델)은 문장을 한 번에 완성하지 않고 토큰 단위로 조금씩 생성합니다. 브라우저는 이 조각들을 이어 붙여 사용자에게 보여주죠. 검색 엔진처럼 한 번에 결과가 나오는 것과 달리, 대화형 AI에서는 문장이 만들어지는 과정 자체가 곧 UX입니다. 첫 단어가 빠르게 나타나고, 이후 글이 매끄럽게 이어진다면 사용자는 “이 AI가 나와 실시간으로 대화하고 있다”는 몰입감을 느낍니다. 반대로 답변이 한참 있다가 한꺼번에 출력되거나, 순서가 뒤섞여 버리면 “품질이 낮다”고 느끼게 됩니다.

결국 실시간 AI의 품질은 모델의 성능만으로 결정되지 않습니다. 답이 얼마나 정확한지와 동시에, 그 답이 얼마나 끊김 없이 매끄럽게 전달되는지가 함께 작동해야 합니다. 이를 위해선 안정적인 연결, 적절한 속도, 중복 제거 처리 같은 기술적 장치가 필요합니다. 이 글에서는 Bedrock에서 브라우저까지 토큰이 어떻게 전달되는지를 API Gateway와 Lambda 함수를 거처 스트리밍 되는 구조를 중심으로 이 세 가지 축을 살펴보겠습니다.

  • API Gateway: WebSocket 연결 및 세션 관리
  • Lambda: 토큰 스트림 조율(속도/배치/백프레셔)
  • Bedrock: 실제 답변 생성
1. 연결 관리

실시간 토큰 스트리밍은 단순히 요청 한 번에 응답 한 번으로 끝나는 구조가 아니라, 서버와 클라이언트가 연결을 유지한 채 데이터를 연속적으로 흘려보내는 방식을 전제로 합니다.

이 연결을 구현하는 방식은 크게 두 가지로 나눌 수 있습니다.

구분HTTP 기반 APIWebSocket 기반 API
연결 방식요청 → 응답 단발성 (연결 끊김)초기 핸드셰이크 후 지속 연결
데이터 흐름단방향 (서버 → 클라이언트)
※ SSE 사용 시 서버 → 클라이언트 지속 이벤트 전송 가능
양방향 (클라이언트 ↔ 서버 자유롭게 메시지 교환)
제어 기능기본적으로 취소/재요청 불가
(요청 단위로만 동작)
대화 중 취소, 재요청, 상태 관리 가능
활용 사례단순 알림, 단방향 스트리밍 (예: 뉴스 피드, 간단한 답변 스트리밍)실시간 채팅, 협업 서비스, 대규모 양방향 대화 서비스

HTTP 기반 API는 요청–응답 구조로 기본적으로 단방향입니다. 다만 SSE(Server-Sent Events)를 활용하면 HTTP 연결을 계속 열어둔 채 서버가 클라이언트로 이벤트를 흘려보낼 수 있어, 간단한 답변 스트리밍이나 알림 전송에는 충분히 적합합니다.
반면 WebSocket 기반 API는 처음에 한 번 HTTP 핸드셰이크로 연결을 맺고 나면, 이후에는 클라이언트와 서버가 자유롭게 양방향 메시지를 주고받을 수 있습니다. 따라서 대화 중 취소, 답변 재요청, 상태 동기화 같은 제어가 필요한 대규모 대화형 AI 서비스에는 WebSocket이 더 적합합니다.

Amazon API Gateway는 이런 WebSocket API를 제공합니다. 실시간 연결을 유지하려면 클라이언트가 언제 접속하고 끊었는지, 또 어떤 메시지를 주고 받는지 구분해야 합니다. 이를 위해 API Gateway는 기본적으로 세 가지 라우트(routeKey)를 제공합니다.

  • $connect : 클라이언트가 처음으로 WebSocket API에 연결할 때 사용
  • $disconnect : 클라이언트가 API 연결을 끊을 때 사용
  • $default : 다른 키와 일치하지 않을 때 기본적으로 사용

각 라우트는 Lambda 함수와 연결되어, 접속 시 연결 식별자(connectionId)를 기록하거나 종료 시 세션을 정리하는 데 활용됩니다.

실제로 연결된 클라이언트를 식별하려면 매 연결마다 주어지는 $context.connectionId를 유지해야 합니다. 일반적으로 이 값을 DynamoDB 테이블에 저장해 두고, 클라이언트로 메시지를 다시 보낼 때 post_to_connection API 호출에 활용합니다.
문제는 연결이 이미 끊긴 상태인데도 서버가 메시지를 보내려 할 때입니다. 이 경우 API Gateway는 HTTP 410 Gone을 반환합니다. 이 경우 해당 connectionId를 DynamoDB에서 제거하지 않으면, 유령 연결(ghost connection)이 쌓여 불필요한 리소스 점유나 DynamoDB 비용 증가 등으로 이어질 수 있고, 결과적으로 서비스 품질에도 악영향을 줍니다.
운영 단계에서는 DynamoDB 테이블에 TTL(Time To Live)을 두어 오래된 연결을 자동으로 정리하고, CloudWatch 지표로 끊긴 연결 비율을 모니터링하는 패턴이 자주 활용됩니다.

2. 토큰 전송 속도 제어 (Backpressure)

LLM은 답변을 한 번에 완성하지 않고 토큰 단위로 조금씩 생성합니다. Amazon Bedrock의 경우 InvokeModelWithResponseStream API를 사용하면 모델이 만들어내는 토큰을 아주 짧은 간격으로 쏟아내듯 받을 수 있습니다. 이 자체로는 큰 장점이지만, 문제는 모델이 뱉는 속도와 클라이언트가 처리할 수 있는 속도가 항상 같지 않다는 데 있습니다.

브라우저가 토큰을 실시간으로 그려내지 못하거나, 네트워크 지연이 발생하거나, 동시에 연결된 사용자가 많아지면 토큰이 밀리기 시작합니다. 그러면 사용자는 갑자기 화면이 끊기거나 한꺼번에 몰려 출력되는 불편을 겪게 됩니다.
이럴 때 “백프레셔(Backpressure)”라는 개념이 필요합니다. 백프레셔란 데이터를 보내는 쪽(생산자)와 받아 처리하는 쪽(소비자) 사이의 속도 차이를 조율하는 메커니즘을 말합니다. Bedrock은 빠른 속도로 토큰을 만들어내지만, 브라우저가 그걸 그대로 감당하지 못한다면 중간에서 속도를 맞춰주는 완충 장치가 필요한 겁니다.

이 역할을 맡는 게 Lamda 입니다. Lamda는 Bedrock이 만들어낸 토큰을 잠시 모아두었다가, 소비자가 감당할 수 있는 속도에 맞춰 적절한 타이밍에 WebSocket으로 흘려보내는 일을 합니다.

  • Chunk 크기와 전송 주기 조절
    Lamda는 Bedrock이 생성한 토큰을 일정 개수(CHUNK_SIZE) 만큼 모아 두었다가, 일정 간격(INTERVAL)이 지나면 API Gateway의 post_to_connection API를 호출해 WebSocket을 통해 클라이언트로 전달합니다. 첫 토큰은 최대한 빨리 내려보내 “응답이 시작됐다”는 신호를 주고, 이후에는 일정한 간격을 유지하여 자연스러운 리듬을 만듭니다.

    특히 첫 토큰을 얼마나 빨리 내려보내느냐는 사용자의 체감 품질에 큰 영향을 줍니다. 이를 측정하는 지표가 TTFB(Time To First Byte)입니다. TTFB는 사용자가 요청을 보낸 뒤 첫 바이트(=첫 토큰)가 도착하기까지 걸린 시간을 뜻합니다. 일반적으로 1~1.5초 이내에 첫 토큰이 도착하면 사용자는 “빠르게 반응했다”는 긍정적인 인상을 줄 수 있습니다.
  • API Gateway WebSocket의 한계 고려
    메시지 크기가 초과되면 API Gateway는 1009 (Message Too Big) 에러와 함께 연결이 끊어집니다. 따라서 긴 텍스트는 반드시 분할 전송해야 끊김 없는 스트리밍을 보장할 수 있습니다.
3. 중복 제거/순서 보장 (Idempotency & Ordering)

실시간 스트리밍 환경에서는 네트워크 특성상 중복 메시지나 순서 뒤바뀜이 발생할 수 있습니다. 사용자는 대화가 앞뒤가 바뀌거나 같은 문장이 두 번 반복되면 품질이 낮다고 느낍니다. 따라서 토큰을 사용자에게 전달할 때는 “멱등성(같은 메시지는 여러 번 와도 한 번만 반영)”과 “순서 보장”이 꼭 필요합니다.

이 역할은 Lambda가 담당합니다. Bedrock에서 흘러오는 토큰 스트림을 받아, 각 토큰에 메타데이터를 붙인 뒤 API Gateway를 통해 클라이언트로 전달합니다.

{ "type": "answer_start", "messageId": "m123" }
{ "type": "partial", "messageId": "m123", "seq": 1, "text": "오늘은 ", "isFinal": false }
{ "type": "partial", "messageId": "m123", "seq": 2, "text": "맑고 ", "isFinal": false }
{ "type": "final", "messageId": "m123", "seq": 3, "text": "화창합니다.", "isFinal": true }
{ "type": "answer_end",   "messageId": "m123" }
  • 시퀀스 번호 부여
    Lambda에서 Bedrock 토큰을 받을 때마다, 각 메시지에 seq(시퀀스 번호)를 붙여 전송합니다. 클라이언트는 이 번호를 기준으로 메시지를 정렬하여 네트워크 지연으로 순서가 바뀌더라도 올바른 문장을 복원할 수 있습니다.
  • 멱등 키(Idempotency Key)
    (messageId, seq) 조합을 키로 사용하여, 이미 처리한 메시지는 다시 화면에 반영하지 않습니다. 이렇게 하면 중복이 와도 화면에는 한 번만 나타납니다.
  • 시작/종료 이벤트
    답변의 시작과 끝을 명확히 하기 위해 answer_startanswer_end 같은 이벤트를 추가할 수 있습니다.
    • answer_start : 새 대화를 시작 → “응답 중” UI에 표시
    • answer_end : 답변 완료 표시 → 로딩 스피너를 종료/ 채팅 버블 닫기
  • 재연결 후 이어 보내기
    클라이언트가 끊겼다가 다시 연결될 때, 마지막으로 받은 seq를 서버에 전달하면, Lambda가 그 이후 토큰만 다시 보냅니다. 이렇게 하면 사용자는 중간이 끊기지 않고 자연스럽게 이어지는 대화를 경험할 수 있습니다.
결론

실시간 AI 스트리밍 경험은 단순히 빠른 응답을 보여주는 것 이상의 과제입니다. 사용자는 “얼마나 빨리 답이 완성되느냐”보다 “답변이 만들어지는 과정이 얼마나 자연스럽게 이어지느냐”에서 품질을 체감합니다.

이를 위해 이번 글에서는 Amazon Bedrock을 중심으로 한 스트리밍 아키텍처에서 핵심이 되는 세 가지 축을 살펴봤습니다.

  • 안정적인 연결 관리
  • 전송 속도 제어 (Backpressure)
  • 중복 제거와 순서 보장

정리하면, API Gateway는 연결을 붙잡아두고, Lambda는 토큰의 흐름을 조율하며, Bedrock은 실제 답변을 만들어냅니다. 여기에 DynamoDB를 통해 연결 상태와 메시지 메타데이터를 관리하면, 사용자는 “빠르고, 매끄럽고, 중복 없는” 대화를 경험할 수 있습니다.

앞으로 AI와의 상호작용이 점점 더 실시간적이고 자연스러운 형태로 진화할수록, 이 세 가지 요소는 단순한 기술적 고려를 넘어 사용자 경험(UX)의 핵심이 될 것입니다.

참고자료
  • https://aws.amazon.com/ko/blogs/compute/announcing-websocket-apis-in-amazon-api-gateway
  • https://repost.aws/knowledge-center/410-gone-api-gateway
  • https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-execution-service-websocket-limits-table.html
  • https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_InvokeModelWithResponseStream.html
  • https://repost.aws/questions/QUsqK6QQwoQ-a-crTHpkjfEA/aws-bedrock-agents-support-streaming-responses
  • https://docs.aws.amazon.com/ko_kr/amazondynamodb/latest/developerguide/TTL.html
  • https://apuravchauhan.medium.com/understanding-backpressure-in-real-time-streaming-with-websockets-20f504c2d248
  • https://aws.amazon.com/ko/blogs/compute/managing-sessions-of-anonymous-users-in-websocket-api-based-applications
  • https://docs.aws.amazon.com/ko_kr/apigateway/latest/developerguide/apigateway-websocket-api-mapping-template-reference.html

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 항목은 *(으)로 표시합니다