Python으로 API 서버를 처음 만들다 보면 uvicorn app:app --host 0.0.0.0 --port 8000 명령어를 자연스럽게 쓰게 된다. 그런데 왜 Uvicorn인지, Gunicorn과는 무엇이 다른지, 프로덕션에서는 어떻게 조합해서 쓰는지 정확히 설명할 수 있는가.
이 글에서는 그 개념부터 실무 아키텍처까지 순서대로 정리한다.
📌 WSGI vs ASGI — 먼저 이 차이부터
Uvicorn과 Gunicorn을 비교하기 전에 WSGI와 ASGI를 먼저 이해해야 한다. 이 둘은 Python 웹 애플리케이션과 웹 서버 사이의 통신 규약(인터페이스 표준) 이다.
구분 WSGI ASGI
| 정식 명칭 | Web Server Gateway Interface | Asynchronous Server Gateway Interface |
| 표준 | PEP 3333 (전통적인 표준) | WSGI의 후속 표준 |
| 처리 방식 | 동기(synchronous) — 요청 하나를 처리하는 동안 다음 요청은 대기 | 비동기(async/await) — I/O 대기 중에 다른 요청 처리 가능 |
| 대표 프레임워크 | Django, Flask | FastAPI, Django Channels |
| 대표 서버 | Gunicorn | Uvicorn |
| 적합한 작업 | CPU 연산 중심 | I/O 바운드 (DB 조회, 외부 API 호출) |
왜 FastAPI는 ASGI인가? FastAPI는 DB 조회, 외부 API 호출처럼 I/O 대기가 잦은 백엔드 서비스에서 쓰인다. ASGI 기반이면 하나의 요청이 DB 응답을 기다리는 동안 다른 요청을 처리할 수 있어서 동시 처리 효율이 높다. WSGI 기반이었다면 DB 응답을 기다리는 동안 다른 요청은 줄을 서야 한다.
📌 Uvicorn — ASGI 서버
Uvicorn은 ASGI 표준을 구현한 웹 서버다. uvloop와 httptools 기반으로 매우 빠른 비동기 처리를 제공한다. FastAPI 공식 권장 서버다.
# 기본 실행
uvicorn app:app --host 0.0.0.0 --port 8000
# 개발 환경: 코드 변경 시 자동 재시작
uvicorn app:app --host 0.0.0.0 --port 8000 --reload
# 워커 수 지정 (프로덕션에서는 Gunicorn에 위임하는 편이 나음)
uvicorn app:app --host 0.0.0.0 --port 8000 --workers 4
Uvicorn 단독 사용의 한계 Uvicorn만으로도 --workers 옵션으로 멀티 프로세스를 띄울 수 있지만, 프로세스 관리(장애 복구, graceful reload, 워커 모니터링)는 Uvicorn의 영역이 아니다. 프로덕션에서는 Gunicorn이 이 역할을 담당하고 Uvicorn은 각 워커의 실행 엔진으로 사용하는 것이 일반적이다.
📌 Gunicorn — 프로세스 매니저
Gunicorn(Green Unicorn)은 원래 WSGI 서버지만, 워커 클래스(Worker Class)를 교체할 수 있는 구조 덕분에 ASGI 앱도 실행할 수 있다. FastAPI + Uvicorn 조합에서는 Gunicorn이 프로세스 관리자로, Uvicorn이 각 워커의 실행 엔진으로 역할을 나눈다.
[ Gunicorn + Uvicorn Worker 구조 ]
Gunicorn (마스터 프로세스)
├─ Uvicorn Worker 1 ← 실제 ASGI 요청 처리
├─ Uvicorn Worker 2
├─ Uvicorn Worker 3
└─ Uvicorn Worker 4
Gunicorn 역할: 워커 생성/종료, 장애 복구, graceful reload, 시그널 처리
Uvicorn 역할: 각 워커에서 비동기 요청 처리
# Gunicorn + UvicornWorker 조합 (프로덕션 권장)
gunicorn app:app \
--worker-class uvicorn.workers.UvicornWorker \
--workers 4 \
--bind 0.0.0.0:8000 \
--timeout 120 \
--keep-alive 5
# 워커 수 공식: (CPU 코어 수 × 2) + 1 이 일반적인 시작점
# I/O 바운드 작업이 많으면 코어 수보다 더 늘리기도 함
🔍 Uvicorn vs Gunicorn — 언제 무엇을 쓸까
항목 Uvicorn 단독 Gunicorn + UvicornWorker
| 용도 | 개발 환경 | 프로덕션 |
| 멀티 프로세스 | 가능하지만 관리 기능 없음 | Gunicorn이 완전히 관리 |
| 워커 장애 복구 | ❌ 없음 | ✅ 자동 재시작 |
| Graceful reload | ❌ 없음 | ✅ 지원 |
| --reload (핫 리로드) | ✅ 지원 | 개발용 아님 |
| 설정 복잡도 | 단순 | 중간 |
📌 다른 서버 옵션들
Uvicorn / Gunicorn 외에도 Python API 서버로 사용할 수 있는 옵션들이 있다.
서버 종류 특징
| Hypercorn | ASGI | HTTP/2, HTTP/3(QUIC) 지원. Uvicorn의 대안 |
| Waitress | WSGI | 순수 Python 구현. Windows 환경 친화적. Django/Flask 프로젝트에서 사용 |
| uWSGI | WSGI | 고성능이지만 설정 복잡. Nginx 조합으로 오래 쓰였으나 최근 Gunicorn + Uvicorn에 자리를 내줌 |
| Daphne | ASGI | Django Channels 전용. Django 프로젝트에서 WebSocket 필요 시 사용 |
📌 Python API 서버 아키텍처 패턴
FastAPI 기반 서비스를 실제로 구성할 때 자주 쓰이는 패턴들을 정리한다. 어떤 패턴이든 핵심은 관심사 분리(Separation of Concerns) 다.
패턴 1 — 계층형 아키텍처 (Layered Architecture)
가장 일반적으로 쓰이는 패턴이다. Router → Service → Repository 순서로 역할을 나눈다. 각 계층은 바로 아래 계층만 호출하고, 위 계층의 존재를 모른다.
HTTP 요청
↓
Router ← 요청 수신, 입력 검증, 응답 직렬화
↓
Service ← 비즈니스 로직, 규칙 처리, 트랜잭션
↓
Repository ← DB 접근, 쿼리 실행, 데이터 반환
↓
Database
# routers/user.py — 요청/응답만 담당
@router.get('/users/{user_id}', response_model=UserResponse)
async def get_user(user_id: int, service: UserService = Depends(get_user_service)):
return await service.get_user(user_id)
# services/user_service.py — 비즈니스 로직만 담당
class UserService:
async def get_user(self, user_id: int) -> User:
user = await self.repo.find_by_id(user_id)
if not user:
raise HTTPException(status_code=404)
return user
# repositories/user_repository.py — DB 접근만 담당
class UserRepository:
async def find_by_id(self, user_id: int) -> User | None:
return await self.db.query(User).filter_by(id=user_id).first()
패턴 2 — Nginx + Gunicorn + Uvicorn (프로덕션 표준)
대부분의 Python 프로덕션 환경에서 사용하는 구조다. Nginx가 리버스 프록시로 앞단에서 SSL 처리, 정적 파일 서빙, 로드밸런싱을 담당하고, 뒤에서 Gunicorn + Uvicorn 워커가 API 요청을 처리한다.
클라이언트 (브라우저)
↓ HTTPS
Nginx ← SSL 종료, 정적 파일, 로드밸런싱, 요청 제한
↓ HTTP (내부)
Gunicorn ← 프로세스 관리, 워커 감시
├─ Uvicorn Worker 1
├─ Uvicorn Worker 2
└─ Uvicorn Worker N
↓
FastAPI Application
왜 Nginx를 앞에 두는가? Gunicorn/Uvicorn은 느린 클라이언트(Slow Client Attack) 방어, SSL 처리, 정적 파일 서빙에 특화되어 있지 않다. Nginx가 이 역할을 맡아 처리하고, 빠른 요청만 뒷단으로 전달하면 애플리케이션 서버는 비즈니스 로직에만 집중할 수 있다.
패턴 3 — Docker + 컨테이너 배포
현대적인 배포 환경에서는 컨테이너로 패키징해서 배포한다. Dockerfile 하나로 개발/스테이징/프로덕션 환경을 동일하게 맞출 수 있다.
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# 프로덕션: Gunicorn + UvicornWorker
CMD ["gunicorn", "app:app",
"--worker-class", "uvicorn.workers.UvicornWorker",
"--workers", "4",
"--bind", "0.0.0.0:8000"]
패턴 4 — 마이크로서비스 / API Gateway
규모가 커지면 단일 FastAPI 앱을 도메인별로 분리하고, API Gateway가 라우팅을 담당하는 구조로 발전한다.
클라이언트
↓
API Gateway ← 인증, 라우팅, 레이트 리밋
├─ /auth/* → Auth Service (FastAPI)
├─ /users/* → User Service (FastAPI)
├─ /orders/* → Order Service (FastAPI)
└─ /analytics → Analytics Service (FastAPI)
📌 FastAPI 계층형 아키텍처 — 디렉토리 구조
실무에서 자주 쓰이는 FastAPI 프로젝트 구조다. Router / Service / Repository 세 계층으로 나누고, 쿼리와 유틸리티를 별도 디렉토리로 분리한다.
app/
├── main.py # FastAPI 인스턴스, 미들웨어, 라우터 등록
│
├── routers/ # API 엔드포인트 정의
│ ├── auth.py # POST /auth/login, POST /auth/refresh
│ ├── user.py # GET /users, GET /users/{id}
│ └── ...
│
├── services/ # 비즈니스 로직
│ ├── base_service.py # 공통 로직 (날짜 검증, 로깅 등)
│ ├── auth_service.py
│ └── ...
│
├── repositories/ # 데이터 접근
│ ├── base_repository.py # DB 연결, 공통 쿼리 실행
│ ├── user_repository.py
│ └── ...
│
├── queries/ # SQL 파일 분리 관리
│ ├── user/
│ │ └── get_user_by_id.sql
│ └── ...
│
├── schemas/ # Pydantic 모델 (요청/응답 스키마)
│ ├── user.py # UserRequest, UserResponse
│ └── ...
│
├── utils/ # 유틸리티
│ ├── db_connection.py # DB 엔진, 연결 풀 관리
│ ├── logger_utils.py
│ └── cache_decorator.py
│
└── infrastructure/ # 인프라 설정
└── dependency_injection.py # FastAPI Depends 설정
SQL을 별도 파일로 분리하는 이유 복잡한 쿼리를 Python 코드 안에 문자열로 쓰면 가독성이 떨어지고, SQL 전용 에디터 지원(자동완성, 문법 강조)도 받을 수 없다. queries/ 디렉토리에 .sql 파일로 분리하면 SQL은 SQL답게 관리하고, Python 코드는 비즈니스 로직에만 집중할 수 있다.
📌 의존성 주입 (Dependency Injection)
FastAPI의 Depends()는 의존성 주입을 간결하게 처리한다. Router가 Service를 직접 생성하지 않고 주입받는 구조로, 테스트 시 mock으로 교체하기 쉽고 결합도가 낮아진다.
# infrastructure/dependency_injection.py
from fastapi import Depends
def get_user_repository() -> UserRepository:
return UserRepository(db=get_db_connection())
def get_user_service(
repo: UserRepository = Depends(get_user_repository)
) -> UserService:
return UserService(repo=repo)
# routers/user.py
@router.get('/users/{user_id}')
async def get_user(
user_id: int,
service: UserService = Depends(get_user_service) # 주입
):
return await service.get_user(user_id)
# 테스트 시 mock으로 교체
app.dependency_overrides[get_user_service] = lambda: MockUserService()
📋 정리
항목 내용
| WSGI vs ASGI | WSGI는 동기 처리 (Flask, Django). ASGI는 비동기 처리 (FastAPI). I/O가 많은 API 서버라면 ASGI가 유리하다 |
| Uvicorn | ASGI 서버. 빠르고 가볍다. 개발 환경에서 단독 사용하고, 프로덕션에서는 Gunicorn의 워커 엔진으로 사용 |
| Gunicorn | 프로세스 매니저. 워커 생성/종료, 장애 복구, graceful reload 담당. UvicornWorker 클래스와 조합해 FastAPI를 프로덕션에서 실행 |
| 프로덕션 표준 구조 | Nginx (리버스 프록시) → Gunicorn (프로세스 관리) → Uvicorn Worker (ASGI 처리) → FastAPI |
| 계층형 아키텍처 | Router (입출력) → Service (비즈니스 로직) → Repository (DB 접근). 각 계층은 바로 아래 계층만 안다 |
| 의존성 주입 | Depends()로 Router와 Service의 결합도를 낮춘다. 테스트 시 mock으로 교체하기 쉽다 |
'Backend > Python' 카테고리의 다른 글
| FastAPI + Oracle DB 연결 오류 — SQLAlchemy 1.4와 oracledb 3.x 버전 충돌 해결기 (0) | 2026.04.07 |
|---|