안녕하세요!
저는 이대 컴퓨터공학과에 재학 중이고 캡스톤 디자인과 창업프로젝트-그로쓰에서 AI파트로 참여하고 있습니다.
오늘은 개발 과정에 대해 말씀드리면서 저와 같은 어려움을 겪으셨던 분들에게 조금이나마 도움이 되보고자 합니다.
우선 전체 파이프라인에 대해 언급한 다음, FastAPI의 전체 구조와 도커 빌드에 대해 말씀드리고자 합니다. 말씀드리면서 제가 유용하게 썼던 명령어들에 대해 정리해볼게요!
프로젝트의 목표는 '뉴스 기사를 숏폼으로 생성해보자!'입니다.
전체 파이프라인은 아래와 같습니다!

- 뉴스 기사 크롤링
- gpt-4o-mini를 이용해 TTV와 TTS에 입력할 프롬프트와 기사의 태그 산출
- 영상과 음성 산출
3-1. 프롬프트를 바탕으로 TTV모델이 영상 산출
3-2. 프롬프트를 바탕으로 TTS API가 음성 산출- MoviePyf를 이용해 영상과 음성, 자막을 합산하고 썸네일 산출
- 데이터 업로드
5-1. S3에 영상과 썸네일 푸시
5-2. BE 서버에 메타데이터 post
스타트 때는(3-2) 코랩으로 각 단계별 코드를 짜서 돌렸습니다. 전체 파이프라인이 연결되지 않은 상태였죠!
이때 각 단계별로는 코드가 성공적으로 돌아갔지만, 산출물을 다음 단계에 입력으로 쓰는 게 꽤나 불편했습니다.. csv 파일과 영상, 음성의 디렉토리를 정확히 지정해줘야 됐거등요..
그리고 최종적으로 배포하기 위해서는 도커에 빌드를 해야했기에!
전체 파이프라인 개발은 FastAPI 프레임워크를 사용하기로 결정했습니다.

최종적인 AI 서버의 구조는 위와 같습니다. 오늘은 이중 FastAPI부터 도커 빌드까지 다뤄볼거에요!
우선 FastAPI 구조는 다음과 같습니다.
구조와 기능에 대해 자세히 써놨으니 이걸 토대로 FastAPI 틀 잡으시는 걸 추천드립니다!
📂 dev/
│
└── app/ # 🧠 애플리케이션 패키지
├── __init__.py
│
├── main.py # 🌐 FastAPI 진입점
│
├── core/ # ⚙️ 공통 설정 & 유틸
│ ├── config.py # 환경변수/세팅
│ └── logging.py # 로깅 포맷, 레벨
│
├── api/ # 🚏 라우팅 계층
│ ├── __init__.py
│ └── endpoints/
│ ├── crwal.py # 뉴스 기사 크롤링
│ ├── summarize.py # GPT가 프롬프트&태그 산출
│ ├── tts.py # TTS API가 음성 산출
│ ├── ttv.py # TTV 모델이 영상 산출
│ ├── moviepy.py # 영상+음성+자막 합산, 썸네일 산출
│ └── pipeline.py # 전체 파이프라인 동작
│
├── services/ # 🏷️ 전체 동작 코드
│ ├── logger.py # 모듈 동작 시 로그
│ ├── crawler.py # 셀레니움을 이용해 크롤링
│ ├── gpt.py # 기사 요약 -> 정제 -> 프롬프트 산출
│ ├── tags.py # 태그 산출
│ ├── tts.py # 음성 산출
│ ├── ttv.py # 영상 산출
│ ├── moviepy.py # 영상+음성+자막 합산, 썸네일 산출
│ ├── pipeline.py # 전체 파이프라인 동작
│ ├── storage.py # s3에 푸시
│ └── to_request.py # BE 서버에 푸시
│
└── data/ # google-credentials
└── keys
└── tts.json
꽤나 복잡하죠?!
사실 FastAPI는 파이썬으로 동작하기 때문에 코드를 짜는 것 자체는 어렵지 않습니다. 전체 로직을 짜고 endpoint를 명확히 설정하는 게 훨씬 중요하고 어렵죠!
이제 각 폴더별 기능을 살펴보겠습니다.
설정·환경변수(config.py), 공통 유틸, 로깅 등 앱 전반에서 필요한 기반 기능
아래는 config 코드 예시,
사용할 키값들을 설정하는 역할을 합니다~
from functools import lru_cache
ROOT_DIR = Path(__file__).resolve().parents[2] # app/../../
class Settings(BaseSettings):
# === OpenAI ===
OPENAI_API_KEY: str = Field(..., env="OPENAI_API_KEY")
class Config:
env_file = ROOT_DIR / ".env"
case_sensitive = True
@lru_cache
def get_settings() -> Settings:
return Settings()
config외에 전체 코드에서 공통적으로 사용할 로깅 파일 등도 담습니다.
/crawl로 끝나는 endpoint의 경우, crawler 모듈이 동작하도록 합니다. from fastapi import APIRouter, BackgroundTasks
from app.core.logger import logger
from app.services import crawler
router = APIRouter(prefix="/crawl", tags=["Crawler"])
@router.get("/", summary="네이버 섹션 헤드라인 크롤링")
async def crawl(bg: BackgroundTasks):
def _job():
data = crawler.crawl_headlines()
logger.info(f"[CRAWL] headlines={len(data)}")
bg.add_task(_job)
return {"msg": "크롤링 시작! 로그에서 진행 상황을 확인하세요."}
from selenium import webdriver
from app.core.logger import logger
def _get_driver(headless: bool = True) -> webdriver.Chrome:
...
def crawl_headlines(sections: List[str]|None=None) -> List[Dict]:
...
def enrich_articles(news_list: List[Dict]) -> List[Dict]:
...
if __name__ == "__main__":
...
FastAPI 객체 생성, 미들웨어(CORS 등) 등록, api 라우터 합체 등
app = FastAPI(
title="📰Shortform Generating by New-valance",
description="네이버 뉴스 → GPT요약 → TTS/TTV → 병합 → S3 업로드",
version="1.0.0",
docs_url="/swagger",
redoc_url="/redoc",
openapi_url="/openapi.json")
자~! 이제 FastAPI를 동작시켜보겠습니다!
동작시키기 전 기본적인 uvicorn 명령어 대해 알아보겠습니다..
pip install fastapi "uvicorn[standard]"# 기본 실행
uvicorn app.main:app
# 리로드
uvicorn app.main:app --reload
# .env 로드와 함께 실행
uvicorn app.main:app --env-file .env
# 포트, 호스트 변경
uvicorn app.main:app --host 0.0.0.0 --port 8000
저는 .env로 환경변수를 주입해야 해서
uvicorn app.main:app --env-file .env 명령어를 사용했어요!
그리고 app의 루트폴더(app폴더의 한 단계 상위폴더)에서 실행해야 main.py와 .env파일이 모두 문제 없이 돌아갑니다~!
아래는 명령어를 입력한 뒤 출력된 터미널입니다.

FastAPI로 로컬 서버가 돌아가는 화면입니다.

위 사진보면 아시겠지만 http://127.0.0.1:8000/swagger 에서 돌아가고 있습니다!
위 main.py에 적어둔 app = FastAPI() 부분에 적어둔대로 로컬호스트의 8000번 포트에서 실행되고 있는겁니다!
여기서 크롤링을 해볼게요🐵
입력할 endpoint는 http://127.0.0.1:8000/swagger#/Crawler/crawl_api_crawl__get
위와 같고, 이 endpoint를 직접 입력하거나 swagger 화면에서 execute해도 됩니다.
실행 뒤 아래처럼 response body에 잘 담겨온 걸 확인할 수 있습니다.

로컬 터미널에서는 아래처럼 로그와 출력이 찍힙니다.

지금까지 FastAPI가 잘 동작하는 걸 보았으니 이제 도커로 넘어가도록 하겠습니다.
저도 이번 프로젝트를 하면서 도커를 처음으로 사용해봤습니다. 어렵다는 말을 많이 들어서 지레 겁먹었지만 막상 해보니 별 거 없더라구요!
여러분도 차근차근 따라오시면 됩니다🍀
도커를 사용하려면 기본적으로 Dockerfile을 작성해야 합니다. Docker의 사용흐름은 간단히 정리하면 아래와 같습니다.
1.Dockerfile 작성
2. 이미지 빌드
3. 컨테이너 실행
4. 수정 -> 재빌드 #캐시 활용
5. docker hub에 푸시
6. 배포
7. 정리 & 모니터링
이제 각 단계별로 차근차근 알려드리겠습니다.
Docker를 실행하려면 Dockerfile은 필수, docker-compose.yml과 .dockerignore은 선택입니다.
다만 저는 추후 EC2로 배포를 해야하고 이미지 용량이 크기 때문에 나머지 두 파일을 작성했습니다.
이제 파일 형식에 대해 보여드릴게요!
# ---------- 1단계: 빌드 ----------
FROM --platform=linux/amd64 python:3.11-slim AS builder
# 시스템 패키지 & 가상환경
RUN apt-get update && apt-get install -y gcc curl && \
python -m venv /opt/venv && \
. /opt/venv/bin/activate && pip install --upgrade pip
# 종속성 복사·설치 (캐시 최적화!)
COPY requirements.txt .
RUN . /opt/venv/bin/activate && pip install --no-cache-dir -r requirements.txt
# ---------- 2단계: 런타임 ----------
FROM --platform=linux/amd64 python:3.11-slim
ENV PYTHONUNBUFFERED=1 \
VIRTUAL_ENV=/opt/venv \
PATH="/opt/venv/bin:$PATH"
# 빌더 단계에서 설치한 가상환경 가져오기
COPY --from=builder /opt/venv /opt/venv
# 애플리케이션 소스
COPY app /app
WORKDIR /app
# 기본 실행 명령 (필요하면 --workers 등 추가)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
앞서 말씀드렸듯이 제 도커이미지는 용량이 큽니다. 이때 사용하는 게 멀티 스테이지인데요! 바로 빌드와 런타임 두 단계로 나눠서 이미지 용량을 최소화하는 방법입니다.
위 파일에서도 빌드 단계에서 설치한 걸 런타임에서는 복붙해서 사용하는 구조입니다.
프로젝트 규모가 작거나 CPU를 사용하면 이렇게 멀티스테이지로 나눌 필요는 없을 것 같습니다!
다만 저처럼... GPU를 쓰거나 규모가 크면 나누시는 걸 추천드립니다(ง •_•)ง
services:
api:
build:
context<: .
dockerfile: Dockerfile
restart: unless-stopped
gpus: all
ports:
- "8000:8000"
env_file:
- .env
volumes:
- .:/app
.dockerignore
# 파이썬 캐시
__pycache__/
*.pyc
# 가상환경/IDE
.venv/
env/
이제 세팅을 했으니 실행해보겠습니다. 앞서 말씀드렸듯이
build -> run 순서도 진행해볼게요!
빌드하기 전에 docker desktop을 먼저 키셔야합니다. 그럼 아래처럼 초록불이 들어오면서 engine이 켜지는 걸 확인할 수 있어요!

docker build -t image_name:latest .
image_name에는 이미지의 이름을, 그 뒤에는 ":"를 붙여 태그를 붙여주시면 됩니다. 태그는 선택!!!
아래는 해당 명령어에 대한 터미널 출력 화면입니다(*^-^*)

명령어 입력하고 위 화면처럼 뜨면 성공!!
제 경우는 이미지 크기가 10GB 이상이라 한 번 빌드할 때 10분~30분은 걸렸던 것 같아요. 여러분도 빌드 시에 너무 오래 걸린다고 걱정 마시고 고냥 냅둬주세요!!
docker run --rm -p 8000:8000 myapp:latest
이게 기본 명령어고 저는 변수들을 주입해줘야해서 아래처럼 입력했습니다.
저렇게 알 수 없는 숫자가 뜨면? 잘 돌아가고 있는겁니다.

이제 로컬 호스트에 접속하시면 제대로 돌아가는 걸 확인할 수 있습니다!
docker login
#이미지 태그
docker tag image_name:latest repo_id/myapp:1.0
#docker hub에 push
docker push repo_id/myapp:1.0
이제 푸시를 해보겠습니다.

push 명령어를 입력하면 이렇게 로그가 쭈욱 찍히다가 푸시가 완료됩니다!
푸시된 이미지는 docker hub에서 확인할 수 있습니다.

배포 시에는
1. docker 단일 서버: docker pull myrepo/myapp:1.0
2. compose: docker-compose up -d
3. kubernetes: kubectl apply -f deployment.yaml
위 세 가지 방법이 있습니다. 오늘은 docker run까지만 알아보기로 했으니 배포부터는 다음에 설명하겠습니다!
# 불필요한 이미지/컨테이너 삭제
docker system prune -a
# 로그 확인
docker logs <container>
# 실시간 상태 확인
docker ps
# 빌드된 도커 이미지 확인
docker images
틈틈히 캐시를 정리해주면 build/run/push 시에 속도가 올라가거나 이미지 크기가 작아질 수 있습니다~!
지금까지 FastAPI 설계부터 docker run/push까지 알아봤습니다. 저처럼 docker가 낯선 분들에게 좋은 길잡이가 되길 바라면서 오늘의 글 마무리하겠습니다.
다들 즐거운 개발하시길 바라요~🍀