본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 09. 10:11

내가 AI 스트리밍(AI Streaming)을 망쳤던 방식과 이를 피하는 방법

요약

AI 모델을 활용한 실시간 코드 리뷰 어시스턴트 구축 과정에서 겪은 스트리밍 구현 실패 사례와 해결책을 다룹니다. 동기식 처리, 잘못된 버퍼링, 백프레셔 관리 미흡 문제를 분석하고 비동기 제너레이터와 asyncio를 통한 최적화 방법을 제시합니다.

핵심 포인트

  • 동기식 REST API 호출은 AI 응답 지연 시 타임아웃을 유발함
  • 스트림 데이터를 전체 수집 후 전송하면 스트리밍의 이점이 사라짐
  • 백프레셔 미처리 시 메모리 증가 및 연결 오류 발생 가능
  • asyncio와 비동기 제너레이터를 활용한 SSE 구현이 효과적임

저는 AI 모델을 사용하여 실시간으로 개선 사항을 제안하는 코드 리뷰 어시스턴트를 구축해 왔습니다. 아이디어는 간단했습니다. 코드 블록을 붙여넣으면 어시스턴트가 마치 IDE를 위한 ChatGPT 클라이언트처럼 토큰(token) 단위로 피드백을 스트리밍(streaming)해 주는 것이었습니다. 무엇이 잘못될 수 있었을까요?

결과적으로, 거의 모든 것이 잘못되었습니다. 첫 번째 버전은 단일 사용자에게는 잘 작동했지만, 더 많은 동시 세션(concurrent sessions)을 추가하자마자 전체 시스템이 무너졌습니다. 응답은 끊겼고, UI는 얼어붙었으며, 때로는 스트림이 문장 중간에 그냥 끊겨버리기도 했습니다. 이것이 오늘 제가 공유하고자 하는 이야기입니다.

문제점

저는 표준 REST 엔드포인트(endpoint)를 가진 Flask 웹 앱을 가지고 있었습니다. 프론트엔드(frontend)가 코드를 POST하면, 제 백엔드(backend)는 AI API(예: https://ai.interwestinfo.com/v1/completions)를 호출하고, 전체 응답을 기다린 다음, 이를 JSON으로 다시 보냈습니다. 단순하고, 동기적(synchronous)이었으며, 잘못된 방식이었습니다.

# 잘못된 버전: 전체 응답을 기다림
@app.route('/review', methods=['POST'])
def review_code():
...

AI가 30초 이상 걸리지 않는 한 작동은 했습니다. 하지만 그 이상 걸리면 프론트엔드에서 타임아웃(timeout)이 발생했습니다. 사용자들은 앱이 느리다고 불평했습니다. 저는 스트리밍(streaming)이 필요하다는 것을 알고 있었습니다.

시도했지만 실패했던 것들

저의 첫 번째 시도는 순진했습니다. AI 제공업체의 스트리밍 API(streaming API)를 사용했지만, 클라이언트에 보내기 전에 여전히 전체 스트림을 버퍼(buffer)에 모았습니다. 이는 목적에 어긋나는 행동이었습니다. 백엔드는 여전히 완료를 기다렸고, 클라이언트는 끝날 때까지 아무런 진행 상황도 볼 수 없었습니다.

# 여전히 도움이 되지 않음: 모든 것을 먼저 수집함
response = requests.post(
    ... ,
...

그 다음에는 백프레셔(backpressure)를 적절히 처리하지 않고 서버 전송 이벤트(Server-Sent Events, SSE)를 시도했습니다. AI 스트림이 제 Python 백엔드가 브라우저로 전달할 수 있는 속도보다 더 빠르게 토큰을 밀어 넣었습니다. 메모리 사용량은 늘어났고, 연결은 정체되었으며, 곳곳에서 BrokenPipeError가 나타나기 시작했습니다.

# 순진한 SSE: 백프레셔 없음, 에러 처리 없음
def event_stream():
    response = requests.post(url, stream=True)
...

이 방식은 사용자 한 명에게는 작동했지만, 10개의 동시 연결이 발생하자 Python의 GIL (Global Interpreter Lock)과 스레드 처리 문제로 인해 이벤트 루프 (event loops)가 마비되었습니다. 저는 더 나은 아키텍처 (architecture)가 필요했습니다.

결국 성공한 방식

몇 번의 시행착오 끝에, 저는 **비동기 제너레이터 (async generators)**와 asyncio를 사용하여 관심사를 분리하는 접근 방식을 선택했습니다. 핵심 아이디어는 다음과 같습니다:

  1. 클라이언트가 SSE (Server-Sent Events) 연결을 설정합니다.
  2. 백엔드에서 AI API로부터 토큰 (tokens)을 스트리밍하는 비동기 태스크 (asynchronous task)를 생성합니다.
  3. 수신된 각 토큰은 열려 있는 SSE 연결을 통해 즉시 클라이언트로 전달됩니다.
  4. 백프레셔 (Backpressure)는 적은 양의 토큰 윈도우를 버퍼링하고, 최대 크기가 지정된 asyncio.Queue를 사용하여 관리합니다.

백엔드 코드 (Python 및 FastAPI)

FastAPI는 비동기 스트리밍 (async streaming)을 기본적으로 지원하며, 이는 매우 혁신적입니다.

import json
import httpx
from fastapi import FastAPI, Request
...

프론트엔드 (JavaScript)

const eventSource = new EventSource('/review/stream', { method: 'POST', body: JSON.stringify({ code }) });
eventSource.onmessage = (event) => {
    const data = JSON.parse(event.data);
...

이 방식은 잘 작동합니다. 토큰들이 생성되는 즉시 UI에 나타납니다. 버퍼링 지연도 없고, 메모리 폭발 (memory blowups)도 없습니다. 하지만 완벽하지는 않습니다.

교훈 (어려운 과정을 통해 배운 것들)

  • 스트리밍은 만능 해결책(silver bullet)이 아닙니다. 만약 클라이언트가 각 토큰에 대해 무거운 처리(예: 구문 강조(syntax highlighting) 또는 보안 스캐닝)를 수행해야 한다면, 지연 시간(latency)이 발생할 것입니다. 클라이언트에서 토큰을 배치(batching)하여 배치 단위로 처리하는 것을 고려하십시오.
  • 연결 신뢰성은 악몽과 같습니다. SSE 연결은 특히 불안정한 네트워크에서 끊어집니다. 클라이언트에 재시도 로직(retry logic)이 필요하며, 우아하게 재개할 수 있는 방법(예: last_token ID와 함께 요청을 보내는 방식)이 필요합니다.
  • 동시 스트림 관리. 많은 스트림이 있을 때, 비동기(async) 서버는 많은 열린 연결을 처리해야 합니다. 적절한 워커 모델(예: uvloop)을 갖춘 FastAPI + uvicorn 조합이 필수적입니다. 또한, 속도 제한(rate limiting)을 피하기 위해 동시 AI API 호출을 제한하는 세마포어(semaphore)를 추가했습니다.
  • 오류 전파(Error propagation)는 까다롭습니다. AI API가 스트림 중간에 오류를 반환하면, 이를 무시할지, 재시도할지, 아니면 전체 스트림을 중단할지 결정해야 합니다. 저는 특별한 오류 토큰을 보내고 클라이언트가 결정하도록 하는 방식을 선택했습니다.

이 방식을 사용하지 말아야 할 때

  • 실시간 피드백이 필요하지 않다면, 그냥 표준 요청-응답(request-response) 방식을 사용하십시오. 더 단순하고 관리해야 할 구성 요소도 적습니다.
  • AI 모델이 매우 빠르다면(2초 미만), 스트리밍의 오버헤드(SSE 헤더, 파싱 등)가 그만한 가치가 없을 수 있습니다.
  • SSE를 잘 처리하지 못하는 브라우저(구형 IE 등)를 반드시 지원해야 한다면, 폴링(polling)이나 웹소켓(WebSockets)으로 대체하십시오.

다음에 다시 한다면 다르게 할 점

  1. 스트리밍이 아닌 프로토타입으로 시작하기. 로직을 먼저 올바르게 구현한 다음 최적화하십시오. 저는 첫날부터 과잉 엔지니어링(over-engineered)을 했습니다.
  2. 이벤트 기반 아키텍처(event-driven architecture) 사용하기. 앱 서버에서 직접 SSE를 제공하는 대신, Redis pub/sub에 토큰을 게시하고 별도의 워커가 SSE 연결에 쓰도록 하겠습니다. 이렇게 하면 확장성(scaling)을 분리할 수 있습니다.
  3. 실제 네트워크 환경에서 테스트하기. 저는 localhost에서만 테스트했기 때문에 지연 및 끊김 문제를 가릴 수 있었습니다. tc와 같은 도구를 사용하여 패킷 손실(packet loss)을 시뮬레이션하십시오.

AI 응답을 스트리밍(Streaming)하는 것은 제대로 작동할 때는 만족스럽지만, 동시성(concurrency)과 신뢰성(reliability)의 늪에 빠지기 쉽습니다. 저는 여전히 제 구현 방식을 미세 조정(tweaking)하고 있습니다. 여러분은 어떠신가요? 프로덕션(production) 환경에서 부분적인 AI 응답을 처리하기 위해 어떤 접근 방식을 사용하시나요?

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0