FastAPI는 Python 기반이라 AI 모델 서빙용으로 많이 사용됩니다. (우선 제가 보통 FastAPI 를 그런 용도로 쓰기 때문에...ㅎㅎ)
개발하다 보면 자연스럽게 이런 욕심이 생깁니다.
"사용자가 PDF를 올리면, BackgroundTasks로 넘겨서 OCR 돌리고 벡터 DB에 넣으면 되겠지?"
결론부터 말씀드리면, 토이 프로젝트면 괜찮지만 , 실무라면 서버가 위험해지더라구욥.... (갑자기 끊김..)
단순한 I/O 작업(이메일)과 CPU/GPU 집약적 작업(AI Inference)의 차이 때문일까요? 그런 거 같습니다.
1. AI 작업, 기다리기??
BackgroundTasks가 가볍고 좋은 이유는 주로 I/O Bound(네트워크 대기, 디스크 쓰기) 작업에 최적화되어 있기 때문입니다. 하지만 AI 모델은 CPU/GPU Bound입니다.
시나리오별 겪어본 위험 요소들
① OCR (Tesseract, PaddleOCR)
- 상황: 사용자가 영수증 이미지를 업로드하면 텍스트를 추출함.
- 문제: 이미지 전처리(OpenCV)와 OCR 모델 추론은 CPU 코어 하나를 100% 점유합니다.
- 결과: OCR이 돌아가는 3초 동안, FastAPI 서버는 멈춥니다(Hang). 다른 사용자의 로그인 요청조차 처리되지 않습니다. (GIL 문제)
② LLM 통신 (Ollama, ChatGPT API)
- 상황: "이 문서 요약해줘" 요청을 받고 Ollama에게 전송.
- 문제: Ollama가 로컬에 떠 있다면 GPU VRAM을 공유합니다. API 서버가 메모리를 많이 쓰면 OOM(Out Of Memory)으로 둘 다 죽습니다.
- 결과: 응답 생성에 30초가 걸린다면, 그동안 워커 스레드 하나가 계속 잡혀있게 됩니다. 요청이 몰리면 스레드 풀 고갈(Starvation)로 서버가 먹통이 됩니다.
③ RAG (PDF 파싱 + Embedding)
- 상황: 100페이지 PDF를 텍스트로 쪼개고(Chunking), 임베딩 모델(Sentence-BERT)을 돌림.
- 문제: PDF 파싱은 엄청난 메모리(RAM)를 순간적으로 소비합니다.
- 결과: API 서버 프로세스의 메모리 사용량이 급증하여, OS에 의해 강제 종료(Kill) 당할 수 있습니다.
2. 아키텍처 패턴: 어떻게 해결해야 할까?
AI 기능을 포함할 때는 "API 서버"와 "추론 워커"를 물리적으로 분리
패턴 1: Celery + Redis (비동기 큐)
가장 추천하는 방식!
- FastAPI: "OCR 요청 들어왔네? Redis 큐에 작업 던져놓고(Publish) 사용자에겐 '접수됨' 응답."
- Redis: 작업 대기열 관리.
- Celery Worker (별도 프로세스/서버):
- 큐에서 작업을 꺼내서 OCR/LLM 수행.
- 이때 GPU가 있는 별도 서버에서 돌릴 수도 있음.
- 작업이 끝나면 DB에 결과 저장.
패턴 2: Model Serving 분리 (Microservices)
AI 모델만 전담하는 별도의 API 서버를 띄우는 방식입니다. (TorchServe, TensorFlow Serving, 혹은 FastAPI로 만든 추론 서버)
- Main API: 사용자의 요청을 받음.
- Inference API: Main API가
aiohttp등을 통해 추론 서버로 요청을 보냄. - 장점: Main API 서버가 가벼워짐. 추론 서버가 죽어도 메인 서비스(로그인, 조회 등)는 살아있음.
해당 아키텍처를 하기 전 까지는
직접 만든 Custom Queue(파이썬 리스트나 asyncio.Queue를 이용한 백그라운드 루프) ,,는 좋지 않더라구요.
1. 서버가 죽으면 데이터가 날아감 (내구성)
직접 만든 큐
- 파이썬 변수(메모리)에 작업 리스트를 저장
- 배포하려고 서버를 끄거나, OOM(메모리 초과)으로 서버가 터지면? 대기 중이던 사용자 요청이 전부 증발
또 확장성이 좋지 않습니다.
- 사용자가 늘어났을 때 API 서버 컴퓨터의 CPU 를 더 늘려야됨 ㅜ (scale-up..)
++
+ FastAPI와 AI 모델이 같은 메모리를 써서 자원 격리도 안됩니다. (500 에러 위험성 ..)
+ "OCR 서버가 일시적으로 응답이 없으면 3초 뒤에 다시 시도하고, 그래도 안 되면 포기해"라는 로직을 직접 try-except와 while문으로 짜야 함..
Celery + Redis
- 작업 요청이 들어오면 즉시 Redis(디스크 혹은 별도 메모리)에 저장
- OCR 워커 서버가 전원 코드가 뽑혀서 죽어도, 작업 내역은 Redis에 남아있음.
- 서버를 다시 켜면 "아까 못다 한 작업"을 다시 가져와서 수행함
.
3. 코드 비교: 망하는 코드 vs 사는 코드
절대 하지 말아야 할 코드 (BackgroundTasks + Heavy Model)
이 코드는 모델을 메모리에 로드하는 순간 API 서버가 무거워지고, 실행 시 렉이 걸립니다.
# 위험한 방식
from fastapi import FastAPI, BackgroundTasks
import easyocr # 무거운 라이브러리
app = FastAPI()
reader = easyocr.Reader(['en']) # 서버 뜰 때 메모리에 모델 로드 (RAM 낭비)
def process_ocr(image_path: str):
# CPU를 100% 사용하여 서버를 블로킹함
result = reader.readtext(image_path)
save_to_db(result)
@app.post("/scan")
async def scan_receipt(file: UploadFile, background_tasks: BackgroundTasks):
# 파일 저장 로직 생략
background_tasks.add_task(process_ocr, "saved_image.jpg")
return {"status": "processing"}
추천하는 방식 (Celery 사용)
FastAPI는 모델을 전혀 모릅니다. 그저 "일 해라"라고 메시지만 던집니다.
# 안전한 방식 (Main API)
from fastapi import FastAPI
from my_worker import celery_app # 별도 설정한 Celery 앱
app = FastAPI()
@app.post("/scan")
async def scan_receipt(file: UploadFile):
# 1. 파일 저장
file_path = save_file(file)
# 2. 큐에 작업 던지기 (0.01초 소요)
# 모델 로딩은 여기서 안 함! Celery 워커가 함!
task = celery_app.send_task("tasks.process_ocr", args=[file_path])
return {"status": "queued", "task_id": task.id}
4. 요약
| 작업 종류 | 예시 | 추천 기술 | 이유 |
|---|---|---|---|
| Light I/O | 이메일, 슬랙 알림, DB 로그 | BackgroundTasks | 빠르고 간편함. |
| Heavy CPU | PDF 파싱, 이미지 리사이징 | Celery | API 서버 렉 방지. |
| AI Model | OCR, Embedding, LLM 추론 | Celery (GPU Worker) | VRAM/RAM 격리, GPU 자원 관리. |
| Long Polling | Ollama 답변 대기 (30초+) | Celery / Async Client | 스레드 풀 고갈 방지. |
5. 마치며
AI 기능을 API에 붙일 때는 "내 서버가 멈출 수도 있다"를 항상 기억해야됨..
특히 Python의 GIL(Global Interpreter Lock) 때문에 CPU 연산이 많은 OCR이나 PDF 처리를 메인 API 프로세스에서 돌리는 것은 ,, 좋지않습니다 ㅜ
귀찮더라도 Message Queue(Redis) 하나를 사이에 두는 아키텍처가, 훨씬 좋음!