Post

[둥지] FastAPI + AI 워커를 위한 모던 백엔드 구조 설계 (feat. uv, Producer-Consumer)

FastAPI 기반의 AI 서비스 백엔드를 설계하며 내린 기술적 의사결정을 공유합니다. pip 대신 Rust 기반의 uv를 도입하여 의존성을 관리하고, Redis를 활용한 Producer-Consumer 패턴으로 클라우드 API와 홈 서버(GPU) 간의 비동기 처리를 구현했습니다.

[둥지] FastAPI + AI 워커를 위한 모던 백엔드 구조 설계 (feat. uv, Producer-Consumer)

“코드의 물리적 배치와 논리적 설계”에 집중했습니다. ‘둥지’는 데이터를 읽고 쓰는 CRUD API가 아닌, AI 모델(GPU)을 구동해야 하는 백엔드 서비스를 포함합니다.

이 글은 의존성 관리 도구의 변화(uv), 비동기 처리 구조, 그리고 유지보수와 테스트 용이성을 고려한 디렉토리 설계의 의도를 다룹니다.


1. 패키지 관리: pip에서 uv

개발 환경과 운영 환경의 의존성을 분리하고 싶어요

여느 파이썬 프로젝트처럼 가상 환경(venv/)을 생성하고 requirements.txt로 시작했습니다. 하지만 로컬(Window/Mac), 개발 서버(Linux), 운영 서버(Linux)에서 모두 같은 패키지를 사용하는 것이 아니기 때문에 이를 어떻게 분리해야 할 지 고민했습니다.

uvpyproject.toml 도입

우리는 과감하게 requirements.txt를 버리고, Rust로 작성된 초고속 패키지 매니저 uv를 도입했습니다. 속도 때문만은 아니었습니다. pyproject.toml의 의존성 그룹으로 개발 환경과 운영 환경을 명확히 분리할 수 있기 때문입니다.

① 의존성 그룹 관리 (dependency-groups)

requirements.txt 파일을 여러 개 쪼개는 대신, pyproject.toml이라는 하나의 표준 설정 파일 안에서 그룹을 나누었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
# pyproject.toml
[project]
dependencies = [
    "fastapi==0.128.0",
    "sqlalchemy==2.0.46",  # 운영에 필수적인 친구들
]

[dependency-groups]
dev = [
    "pytest>=8.0.0",       # 개발할 때만 필요한 친구들
    "ruff>=0.3.0",
]

명령어 한 줄이면 상황에 맞는 환경이 구축됩니다.

  • 로컬 개발: uv sync (전부 설치)
  • 운영 배포: uv sync --no-dev (운영 패키지만 설치)

uv.lock

pip를 쓸 때는 가끔 “어제는 됐는데 오늘은 안 되는” 경우가 있습니다. 하위 의존성 패키지의 버전이 몰래 바뀌기 때문입니다.

uvuv.lock 파일을 통해 모든 패키지의 버전을 해시값 단위로 고정합니다. 덕분에 팀원 A의 컴퓨터, 팀원 B의 컴퓨터, 그리고 CI 서버의 환경이 일치하게 됩니다.

결과적으로, uv syncuv.lock을 통한 결정론적(Deterministic) 환경을 구축했습니다.

2. 비동기 아키텍처: Producer-Consumer 패턴

AI 작업이 오래 걸리는데, 비동기 처리를 어떻게 하지?

AI 서비스를 개발할 때 가장 큰 기술적 모순은 속도(Latency)와 비용(Cost)의 충돌입니다. AI 모델이 답변을 생성하는 데 10초가 걸린다고 가정할 때, 이걸 API 서버에서 직접 처리하면 그동안 서버는 아무것도 못 하고 멈춰버립니다(Blocking). 그렇다고 비싼 클라우드 GPU 인스턴스를 24시간 돌리기엔 비용이 부담스럽습니다.

왜 API 서버와 Worker를 분리했나?

이 문제를 해결하기 위해 API 서버와 AI Worker의 역할을 분리했습니다.

  • API 서버 (EC2): 사용자에게 즉각적인 응답을 보장해야 합니다. 가볍고 빨라야 합니다.
  • AI 워커 (Home Server): 무거운 연산을 수행하느라 느립니다.

만약 이 둘을 한 서버에 두면, 무거운 AI 작업 하나가 전체 API 응답을 지연시키는 병목 현상(Blocking)이 발생합니다. 따라서 “API 서버는 클라우드(EC2)에, AI 워커는 집(Home Server)에” 두는 하이브리드 클라우드 구조를 설계했습니다.

Redis를 활용한 Producer-Consumer 패턴

물리적으로 떨어진 두 서버(클라우드와 집)를 연결하기 위해 Redis를 메시지 브로커로 사용하는 Producer-Consumer 패턴을 적용했습니다.

① 물리적/논리적 분리

  • Producer (API Server - EC2):
    • 사용자의 요청을 받습니다. 하지만 직접 처리하지 않습니다.
    • “이거 처리해줘”라는 작업(Task)을 Redis 큐에 등록(Push)하고, 사용자에게는 즉시 “접수되었습니다”라고 응답합니다. (Non-blocking)
  • Broker (Redis - ElastiCache):
    • 클라우드 상에 위치하여 언제든 접근 가능하며, 요청들이 순서대로 쌓이는 대기열(Queue) 역할을 합니다.
  • Consumer (AI Worker - Home Server):
    • 집에 있는 GPU 서버입니다. Redis를 계속 감시하다가, 할 일이 생기면 가져와서(Pop) 처리하고 결과를 DB에 저장합니다.

② 네트워크 문제 해결 (Outbound Connection)

외부(EC2)에서 집(Home Server)으로 어떻게 접속하느냐

보통 공유기 포트포워딩(Port Forwarding)을 떠올리지만, 이는 보안상 위험할 뿐더러 유동 IP 문제로 관리가 매우 까다롭습니다. 우리는 발상을 전환했습니다. “밖에서 안으로 들어오는 게 아니라, 안에서 밖으로 나가면 되지 않을까?”

집 서버(Worker)가 주기적으로 클라우드(Redis)에 접속해서 “일 있어요?”라고 물어보는 Polling 방식을 택했습니다. 이렇게 하면 집 공유기의 포트를 하나도 열지 않아도 되므로 보안성은 극대화되고, 네트워크 설정은 단순해집니다.

3. 테스트 전략: tests/ 디렉토리의 물리적 분리

테스트 코드를 app/ 안에 두는 게 나을까, 밖에 두는 게 나을까?

테스트 코드를 로컬에서만 실행해볼 지, git에 공유하는 게 나을 지 고민이었습니다. 다같이 공유를 하되, 가독성과 배포 최적화를 위해 tests/ 폴더를 만들어 분리하기로 결정했습니다.

역할에 따라 디렉토리 분리

1
2
3
4
5
doongzi-backend/
├── app/          # [Source] 순수 비즈니스 로직
├── tests/        # [Test] 테스트 코드 및 픽스처
├── deploy/       # [Infra] Dockerfile, docker-compose
└── pyproject.toml

deploy/ 디렉토리

Dockerfile이나 docker-compose.yml 같은 인프라 설정 파일들이 프로젝트 루트(Root)를 오염시키는 것을 막기 위해 별도 폴더로 격리했습니다.

tests/ 디렉토리 분리

테스트 코드를 app/ 밖으로 뺐습니다. Docker Image 최적화에 중요합니다.

Dockerfile을 작성할 때 app 폴더만 복사하도록 합니다.

1
COPY app app

만약 테스트 코드가 app/ 안에 있었다면, 운영 서버 이미지에 불필요한 테스트 코드가 포함되었을 것입니다. 구조를 분리함으로써 운영 이미지를 더 가볍고 안전하게(테스트 로직 유출 방지) 만들었습니다.

conftest.py의 활용

테스트 코드를 분리하면서 tests/conftest.py를 적극 활용했습니다. DB 세션 생성이나 API 클라이언트 설정 같은 공통 셋업(Fixture)을 이곳에서 관리하여, 개별 테스트 파일(test_main.py)은 오로지 검증 로직에만 집중하도록 설계했습니다.


마치며

uv를 통해 어디서든 똑같이 동작하는 환경을 만들었고, Producer-Consumer 패턴으로 성능과 비용 효율을 잡았으며, 명확한 디렉토리 구조로 배포 최적화까지 고려했습니다.

설계 단계에서 서비스 개발뿐만 아니라 운영 단계까지 생각하는 게 여간 어려운 일이 아닙니다. 그렇지만, 지금 고생해야 나중에 편하겠지요?

This post is licensed under CC BY 4.0 by the author.