[FastAPI] 30명 동시 접속 RAG 챗봇, 왜 FastAPI 비동기(Async)가 필수일까?

2025. 12. 18. 01:31·ServerDev/FastAPI

[FastAPI] 30명 동시 접속 RAG 챗봇, 왜 FastAPI 비동기(Async)가 필수일까?

일반적인 웹 서비스의 응답 속도는 0.1초 내외로 매우 빠릅니다. 하지만 LLM(거대 언어 모델) + RAG(검색 증강 생성) 기반의 챗봇은 이야기가 다릅니다. 답변 하나를 만드는 데 짧게는 3초, 길게는 30초 이상 걸리기 때문입니다.
만약 30명이 동시에 질문을 던진다면 서버 내부에서는 어떤 일이 벌어질까요? 왜 여기서는 '비동기(Async)'가 선택이 아닌 필수인지 , 멀티쓰레드와 싱글쓰레드의 정확한 차이가 무엇인지, 어떤식으로 요청을 접수시켜야될지? 기록해보려 합니다.


1. 상황 분석: 30명의 접속, 응답 시간 30초

RAG 챗봇의 프로세스는 보통 3단계를 거칩니다.

  1. 질문 임베딩: 질문을 벡터로 변환 (CPU/GPU 연산)
  2. 문서 검색 (Vector DB): 관련 지식 찾기 (I/O 작업)
  3. 답변 생성 (LLM): 프롬프트 조합 후 답변 생성 (가장 오래 걸리는 I/O 구간)

문제는 3번입니다. LLM이 답변을 '추론'하는 동안 서버는 아무것도 못 하고 하염없이 기다려야 합니다.


2. 일반 함수(def) vs 비동기 함수(async def)

상황 A: def chat_bot() (멀티쓰레드 방식)

FastAPI는 기본적으로 약 40개의 쓰레드 풀(Thread Pool)을 가집니다.

  • 동작: 30명이 동시에 접속하면 쓰레드 30개가 생성되어 각각 유저 1명씩 붙잡습니다.
  • 상태:
  • 쓰레드 1: "OpenAI 응답 기다리는 중..." (30초 대기 )
  • 쓰레드 2: "Vector DB 검색 중..." (3초 대기 )
  • 결과: 30명까지는 어찌어찌 버팁니다. 하지만 50명이 접속하면? 41번째 사람은 앞사람 요리가 끝날 때까지 접속조차 못 하고 무한 대기(Pending) 상태에 빠집니다. 메모리 점유율도 수직 상승합니다.

상황 B: async def chat_bot() (비동기 방식)

단 1개의 메인 쓰레드(Event Loop)가 모든 일을 처리합니다.

  • 동작:
  1. 유저 1 요청 받음 → LLM에 요청 전달 → "결과 나오면 알려줘!" 하고 다음 유저에게 이동.
  2. 유저 2 요청 받음 → DB에 요청 전달 → 다음 유저에게 이동.
  3. 0.01초 만에 30명의 요청 접수 완료!
  • 결과: 서버는 1개의 쓰레드만 쓰면서도 수백 명의 요청을 동시에 관리합니다. LLM 답변이 도착하는 순서대로 유저에게 즉시 전달합니다.

3. [시각화] RAG 챗봇의 동시 처리 흐름 (Async 기준)

 

 

그림 중앙의 FastAPI Web Server를 보시면, 수많은 요청(Request)을 한 몸으로 다 받고 있습니다.

  • 동기(Sync)라면 요청 1개가 들어와서 워커가 일을 끝낼 때까지 서버가 그 자리에 멈춰 서서 기다려야 합니다.
  • 비동기(Async)인 이유 FastAPI는 요청을 받자마자 내부의 Task Queue(작업 대기열)에 집어넣고, 바로 다음 유저의 요청을 받으러 달려갑니다. "주문서만 빨리 접수하고, 실제 요리는 주방(Worker)에 맡기는 방식"

2. 메모리 관리 (Worker Process)

그림 하단의 Worker Process 1, 2, 3이 핵심입니다.

  • 표현 방법: "메모리 효율화 및 모델 공유"
  • 설계: 30명의 유저가 있다고 해서 LLM 모델을 30개 띄우는 게 아닙니다. 모델은 공유 메모리 공간에 한 번만 로드하고, 여러 개의 워커(Worker)들이 이를 참조하여 연산만 수행합니다. 이렇게 해야 서버의 RAM이 터지지 않습니다.

3. 큐(Queue)와 워커(Worker)의 관계

그림에서 화살표가 Task Queue에서 Worker로 이어지는 부분이 바로 **부하 분산(Load Balancing)**입니다.

  • 큐(Queue): "줄 세우기"입니다. 요청이 한꺼번에 100개가 들어와도 서버가 죽지 않게 **메시지 브로커(Redis 등)**가 안전하게 요청들을 보관합니다.
  • 워커(Worker): "실제 일꾼"입니다. GPU 성능에 맞춰 워커 개수를 조절합니다. 예를 들어 GPU가 1대라면 워커를 적절히 제한하여 연산 병목을 관리합니다.

"이 아키텍처의 핵심은 접수(FastAPI)와 처리(Worker)의 분리.
비동기 이벤트 루프는 요청을 Queue에 쌓는 데 집중하고,  LLM 추론은 별도의 Worker Process가 담당
덕분에 메모리는 아끼고, 동시 접속자는 더 많이 수용


4. 주의해야 할 3가지 진짜 병목 포인트

FastAPI를 비동기로 잘 짰어도, 다음 3곳에서 문제가 터질 수 있음.

  1. LLM API Rate Limit (속도 제한): 30개 요청을 한꺼번에 보내면 OpenAI에서 "너무 빠르다"며 429 Error를 뱉을 수 있습니다. (재시도 로직 필수)
  2. Local LLM (GPU) 고갈: 서버에서 직접 모델을 돌린다면 VRAM이 터지거나 추론 속도가 급격히 느려집니다. (vLLM 같은 서빙 프레임워크 권장)
  3. 타임아웃 (Timeout): 30초 이상 걸리면 브라우저나 Nginx가 연결을 끊어버립니다. 이를 해결하기 위해 StreamingResponse가 필수입니다.

5. StreamingResponse 구현

글자가 타닥타닥 찍히는 스트리밍 방식

from fastapi.responses import StreamingResponse
import asyncio

# 반드시 비동기 라이브러리(AsyncOpenAI, httpx)를 사용해야 합니다.
async def chat_stream(msg):
    # 1. 비동기 검색 (예시)
    docs = await vector_db.asearch(msg) 

    # 2. 비동기 LLM 호출 (스트리밍 모드)
    async for chunk in openai.achat_completion(docs, stream=True):
        content = chunk.choices[0].delta.content
        if content:
            yield content

@app.post("/chat")
async def chat(request: Request):
    return StreamingResponse(chat_stream(request.msg), media_type="text/event-stream")

요약 및 결론

  1. 30명 동시 접속은 RAG 서비스에서 결코 가벼운 작업이 아님 ㅜ
  2. def 대신 async def를 사용하여 서버 자원(쓰레드) 고갈을 방지해야 함.
  3. 사용자 경험을 위해 StreamingResponse를 도입!
  4. 주의: 내부 로직에서 requests나 time.sleep 같은 동기 함수를 쓰면 비동기의 효과가 사라지니 반드시 httpx나 asyncio.sleep을 사용해야 합니다.

도움이 되셨나요? 다음 포스팅에서는 vLLM을 활용한 로컬 모델 서빙 최적화 를 기록해보려합니다. 궁금한 점은 댓글로 남겨주세요!

'ServerDev > FastAPI' 카테고리의 다른 글

[FastAPI 운영 로그 설계] 로그 포맷과 클라이언트 IP  (0) 2025.12.27
[FastAPI - SSE Streaming] FastAPI SSE 스트리밍(StreamingResponse) 실전 적용기: 동기 generator를 비동기로 “진짜 스트리밍” 만들기  (0) 2025.12.19
[FastAPI/AI] OCR, LLM(Ollama), RAG 구현 시 절대 BackgroundTasks만 믿으면 안 되는 이유  (0) 2025.12.13
[FastAPI] 미들웨어 - 작동 원리부터 ELK 연동을 위한 JSON 구조화 로깅까지  (0) 2025.12.13
[FastAPI] 미들웨어(Middleware) - 개념부터 커스텀 구현까지  (0) 2025.12.12
'ServerDev/FastAPI' 카테고리의 다른 글
  • [FastAPI 운영 로그 설계] 로그 포맷과 클라이언트 IP
  • [FastAPI - SSE Streaming] FastAPI SSE 스트리밍(StreamingResponse) 실전 적용기: 동기 generator를 비동기로 “진짜 스트리밍” 만들기
  • [FastAPI/AI] OCR, LLM(Ollama), RAG 구현 시 절대 BackgroundTasks만 믿으면 안 되는 이유
  • [FastAPI] 미들웨어 - 작동 원리부터 ELK 연동을 위한 JSON 구조화 로깅까지
yeseul-kim01
yeseul-kim01
  • yeseul-kim01
    슬 개발일지
    yeseul-kim01
  • 전체
    오늘
    어제
    • 분류 전체보기 (79)
      • 자격증 (1)
        • 정보보안기사 (0)
      • DevOps (17)
        • Docker (6)
        • Kubernetes (1)
        • GitHub Actions (0)
        • AWS (4)
        • Monitoring (1)
        • Nginx (1)
        • GCP (3)
      • ServerDev (34)
        • SpringBoot (13)
        • DJango (5)
        • FastAPI (14)
        • Next (0)
        • Flask (0)
        • Database (2)
      • Algorithm (2)
        • BFS (0)
        • DFS (1)
        • 다익스트라 (0)
      • CS (8)
      • Data Engineering (1)
      • AI&MLOps (2)
      • Architecture (6)
      • Software Engineering (0)
        • Library Packaging (0)
      • Project (5)
        • docx-generator (0)
        • speak-note (2)
        • ms-serving (1)
        • keyshield (2)
      • ProgrammingLanguages (3)
        • Python (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    동시성제어
    KServe
    실무일기-백엔드편
    di
    아키텍처
    NLP부트캠프
    FastAPI - CORS 마스터
    실시간시스템
    docker
    SpeakNote
    STT
    프로젝트기록-KeyShield
    MLops
    아키텍처설계
    KeyShield
    grpc
    multipartfile
    Kubernetes
    하이브리드아키텍처
    트러블슈팅
    비동기처리
    depends
    rag
    프로젝트기록-speaknote
    실무일기-인프라편
    백엔드
    멀티모듈
    SpringBoot
    FastAPI
    Django
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
yeseul-kim01
[FastAPI] 30명 동시 접속 RAG 챗봇, 왜 FastAPI 비동기(Async)가 필수일까?
상단으로

티스토리툴바