
Uvicorn 서버 6회차 : 운영 실행 방식 — Uvicorn 단독 vs Gunicorn+UvicornWorker, 왜 조합을 쓰는가?
한눈에 보는 요약
- Uvicorn은 FastAPI/Starlette 같은 ASGI 앱을 실행하는 서버입니다. 단독으로도 충분히 운영이 가능합니다.
- 그럼에도 프로덕션에서 Gunicorn(프로세스 매니저) + UvicornWorker(ASGI 워커) 조합을 많이 쓰는 이유는 “요청 처리 성능”보다 운영 안정성(프로세스 관리, 재시작/롤링, 타임아웃, 로그, 워커 제어)을 더 체계적으로 가져가기 위함인 경우가 많습니다.
- 핵심은 “Uvicorn이 나쁘다”가 아니라, 운영 환경에서 필요한 기능(감시/복구/정교한 워커 정책)을 누가 책임질지의 선택입니다. 컨테이너 오케스트레이션(Kubernetes 등)이 그 역할을 강하게 해주면 Uvicorn 단독이 충분히 깔끔해지기도 합니다.
- 이번 글의 산출물은 운영용 실행 옵션 비교표(단독/조합)이며, (선택) Linux에서 gunicorn 실행 실습까지 포함합니다.
목차
- 1. 학습 목표: 프로덕션 패턴을 ‘운영 관점’으로 이해하기
- 2. 전제 지식: ASGI, Uvicorn, Gunicorn, Worker가 각각 뭘 하나?
- 3. “Uvicorn만으로도 되는데” 왜 조합을 쓰는가
- 4. 운영 옵션 비교표: 단독 vs 조합(핵심 산출물)
- 5. 실전 명령어: Uvicorn 단독 / Gunicorn+UvicornWorker
- 6. (선택) Linux에서 서비스로 돌리기: systemd 관점
- 7. 운영 체크리스트: 장애/성능/관찰성(로그) 관점
- 실행 단계: 비교판을 직접 만들어 보는 학습 루트
- 블로그 최적화 정보
핵심 포인트
- Uvicorn은 ASGI 서버로서 “요청을 받아 앱을 실행”하는 역할에 집중합니다. 단독 운영도 가능하며, 간단한 배포에서는 매우 실용적입니다.
- Gunicorn은 “마스터 프로세스가 워커들을 관리”하는 전통적인 프로세스 매니저 성격이 강합니다. 죽은 워커 복구, 워커 수/정책 관리, 신호 처리, 운영 옵션이 탄탄합니다.
- UvicornWorker는 Gunicorn의 워커로 Uvicorn을 붙여 ASGI 앱을 처리하게 만드는 브리지입니다. 즉 “프로세스 관리는 Gunicorn, 네트워크/ASGI 처리는 Uvicorn”으로 역할을 분리합니다.
- 조합이 널리 쓰이는 이유는 “성능”이라기보다 운영 중 안정적 재시작(롤링), 워커 관리, 타임아웃, 관찰성 옵션을 일관되게 가져가기 쉽기 때문인 경우가 많습니다.
- 반대로 컨테이너 환경에서 오케스트레이터가 재시작/스케일링을 책임지고, 단일 프로세스 철학을 선호한다면 Uvicorn 단독이 더 단순하고 좋은 선택이 될 수 있습니다.
상세 설명
1) 학습 목표: 프로덕션 패턴을 ‘운영 관점’으로 이해하기
개발 서버에서 돌아가던 API가 프로덕션에 올라가면, 단순히 “잘 뜬다”가 끝이 아닙니다. 다음이 매일 일어납니다.
- 배포(새 버전으로 교체) 중에도 서비스는 끊기지 않아야 합니다.
- 일시적인 오류/메모리 누수/예외로 프로세스가 죽어도 자동으로 복구되어야 합니다.
- 부하가 늘면 워커를 늘리고, 로그와 지표로 상태를 관찰해야 합니다.
- 타임아웃/연결 종료/그레이스풀 셧다운 같은 “끝맺음”이 깔끔해야 합니다.
이 글은 “Uvicorn을 어떤 옵션으로 켜나요?”에서 끝나지 않고, 운영에서 중요한 요구사항을 누가 책임지는지 관점으로 단독/조합을 비교합니다.
2) 전제 지식: ASGI, Uvicorn, Gunicorn, Worker가 각각 뭘 하나?
용어를 한 번에 외우기보다 역할로만 잡으면 빠릅니다.
- ASGI: 파이썬 웹 앱이 “비동기(Async) 요청”까지 포함해 처리할 수 있도록 정의된 인터페이스(규약)입니다.
- Uvicorn: ASGI 서버. 소켓을 열고(포트 바인딩), HTTP 요청을 받아 ASGI 앱(FastAPI 등)에 전달합니다.
- Gunicorn: 원래는 WSGI 서버로 유명하지만, 핵심 강점은 “마스터-워커 기반 프로세스 관리”입니다. 다양한 워커 타입을 끼워 넣을 수 있습니다.
- UvicornWorker: Gunicorn의 워커로서 Uvicorn을 사용하도록 만든 워커 구현입니다. 즉 Gunicorn이 워커를 띄우고 관리하고, 워커 내부에서 Uvicorn이 ASGI 요청 처리를 담당합니다.
3) “Uvicorn만으로도 되는데” 왜 조합을 쓰는가
결론부터 말하면, 둘 다 가능합니다. 차이는 “운영 책임의 위치”입니다.
- Uvicorn 단독: 실행 구성이 단순합니다. 컨테이너 환경에서 “프로세스 1개 = 컨테이너 1개” 철학과도 잘 맞습니다. 다만 운영 정책(워커 재활용, 워커 교체, 신호 처리 정책 등)을 더 세밀하게 가져가려면 외부(systemd/K8s/프로세스 매니저)가 그 역할을 맡아야 합니다.
- Gunicorn+UvicornWorker: “마스터가 워커를 통제한다”는 운영 모델이 강합니다. 워커 수, 재시작, 타임아웃, 워커 교체(예: max-requests) 같은 정책을 표준적으로 적용하기 쉽습니다. 운영팀/서버팀이 경험적으로 축적해 둔 Gunicorn 운영 레시피를 그대로 가져오기에도 유리합니다.
즉 “Uvicorn만으로도 되는데 왜 조합을 쓰나”에 대한 답은 보통 아래 중 하나입니다.
- 프로세스/워커 관리 정책을 더 견고하게 표준화하고 싶어서
- 오래된 운영 환경(전통적인 VM, systemd 기반)에서 Gunicorn이 검증된 런북이어서
- 워커 교체/타임아웃/로그/신호 처리 등을 한 도구(Gunicorn)에서 통합해 다루고 싶어서
4) 운영용 실행 옵션 비교표(단독/조합) — 이번 회차 산출물
| 비교 항목 | Uvicorn 단독 | Gunicorn + UvicornWorker | 운영 관점 메모 |
|---|---|---|---|
| 프로세스 모델 | 단일 프로세스 또는 다중 워커(옵션 기반) | 마스터(관리) + 워커(처리) | 조합은 “관리 전담(마스터)”가 명확합니다. |
| 워커 관리 | 기본은 단순(외부 도구 의존 가능) | 워커 수/재시작/교체 정책이 풍부 | 장기 운영에서 워커 정책이 중요해집니다. |
| 죽은 프로세스 복구 | systemd/K8s 등 외부가 복구하는 패턴이 흔함 | 마스터가 워커를 즉시 재기동 | 복구 책임을 “내부에 둘지, 외부에 둘지” 차이입니다. |
| 그레이스풀 재시작 | 외부 배포 전략에 따라 구현 | 신호 기반 롤링/재시작 운영 패턴이 성숙 | 무중단 배포에서 체감 차이가 납니다. |
| 타임아웃/워커 강제 종료 | 서버/리버스프록시 설정과 함께 맞춰야 함 | timeout 등 워커 통제 옵션이 표준화 | 요청이 “걸리는” 상황에서 운영 안정성에 영향 |
| 메모리 누수 완화 | 외부 롤링/재배포로 대응하는 경우가 많음 | max-requests 등 워커 교체로 완화 가능 | 장시간 운영에서 유용한 전술입니다. |
| 컨테이너 적합성 | 매우 좋음(1프로세스 철학에 자연스러움) | 가능하지만 “한 컨테이너에 마스터+워커” 구조가 됨 | K8s에서는 단독+복제(Replica)로 가는 팀도 많습니다. |
| 추천 상황 | 단순 운영, K8s/오케스트레이션 기반, 빠른 구성 | VM/systemd 기반, 표준 런북, 워커 정책/통제가 중요 | 정답은 “환경과 팀의 운영 방식”에 따라 달라집니다. |
5) 실전 명령어: Uvicorn 단독 / Gunicorn+UvicornWorker
아래 예시는 FastAPI 앱이 app/main.py에 있고, ASGI 객체가 app(예: app = FastAPI())라고 가정합니다. 실행 대상 표기는 app.main:app 형태입니다.
코드 예시: Uvicorn 단독(운영형)
# ✅ 기본: 단일 프로세스(가장 단순)
uvicorn app.main:app --host 0.0.0.0 --port 8000 --log-level info
# ✅ 멀티 워커(부하 대응): CPU 코어 수에 맞춰 조정
# (주의) 워커 수가 늘면 메모리도 그만큼 늘어납니다.
uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4 --log-level info
# ✅ 리버스 프록시(Nginx 등) 뒤에서 헤더 신뢰가 필요할 때(환경에 맞게)
uvicorn app.main:app --host 0.0.0.0 --port 8000 --proxy-headers --forwarded-allow-ips="*"
운영에서는 개발 옵션인 --reload는 보통 사용하지 않습니다. 대신 무중단 배포는 systemd/K8s/Nginx/배포 도구로 “프로세스 교체”를 설계하는 방식이 일반적입니다.
코드 예시: Gunicorn + UvicornWorker(운영형)
# ✅ 가장 흔한 형태: -k 로 UvicornWorker 지정
gunicorn app.main:app \
-k uvicorn.workers.UvicornWorker \
-w 4 \
-b 0.0.0.0:8000 \
--access-logfile - \
--error-logfile - \
--timeout 60
# ✅ 장시간 운영에서 자주 보는 옵션(상황에 따라)
# --max-requests: 일정 요청 수 처리 후 워커 교체(누수 완화)
# --max-requests-jitter: 동시에 교체되는 것을 방지(랜덤 분산)
gunicorn app.main:app
-k uvicorn.workers.UvicornWorker
-w 4
-b 0.0.0.0:8000
--timeout 60
--graceful-timeout 30
--max-requests 2000
--max-requests-jitter 200
--access-logfile -
--error-logfile -
조합의 장점은 “워커를 운영 정책으로 관리한다”는 점입니다. 예를 들어 메모리 누수가 의심되는 상황에서 워커를 주기적으로 교체하는 전략은 운영에서 꽤 실용적입니다(물론 근본 해결은 아니지만, 장애 완화에는 도움이 됩니다).
코드 예시: Gunicorn 설정 파일로 관리(gunicorn.conf.py)
# gunicorn.conf.py
bind = "0.0.0.0:8000"
workers = 4
worker_class = "uvicorn.workers.UvicornWorker"
timeout = 60
graceful_timeout = 30
accesslog = "-"
errorlog = "-"
loglevel = "info"
max_requests = 2000
max_requests_jitter = 200
설정 파일로 운영 옵션을 고정하면 “누가 실행하더라도 같은 옵션”이 보장됩니다. 특히 팀 단위 운영에서는 커맨드라인보다 설정 파일이 실수를 줄이는 편입니다.
6) (선택) Linux에서 서비스로 돌리기: systemd 관점
VM이나 bare-metal 환경에서는 systemd로 서비스화를 많이 합니다. 여기서 중요한 것은 “어떤 실행 방식을 택하든, 재시작 정책과 로그/권한/환경변수”를 운영 수준으로 고정하는 것입니다.
코드 예시: systemd 유닛(예시)
# /etc/systemd/system/myapi.service
[Unit]
Description=MyAPI (FastAPI)
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/srv/myapi
Environment="ENV=production"
# ✅ 1) Uvicorn 단독 예시(둘 중 하나만 사용)
# ExecStart=/srv/myapi/.venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4
# ✅ 2) Gunicorn+UvicornWorker 예시(둘 중 하나만 사용)
ExecStart=/srv/myapi/.venv/bin/gunicorn app.main:app -k uvicorn.workers.UvicornWorker -w 4 -b 0.0.0.0:8000 --access-logfile - --error-logfile - --timeout 60
Restart=always
RestartSec=3
KillSignal=SIGTERM
TimeoutStopSec=30
[Install]
WantedBy=multi-user.target
이 예시는 “운영 실행 방식의 골격”을 보여주기 위한 것으로, 실제로는 포트, 사용자, WorkingDirectory, 환경변수 로딩 방식(.env), 로그 수집 방식(journald/파일/사이드카)을 환경에 맞게 다듬어야 합니다.
7) 운영 체크리스트: 장애/성능/관찰성(로그) 관점
- 워커 수 산정: “무조건 많이”가 정답이 아닙니다. 워커는 메모리를 소비하고, 비동기 앱은 워커 수보다 이벤트 루프/IO가 병목인 경우도 많습니다. CPU, 메모리, 트래픽 패턴을 보고 조절하는 것이 안전합니다.
- 타임아웃 정합성: Gunicorn의 timeout, 리버스 프록시(Nginx)의 timeout, 클라이언트 타임아웃이 서로 충돌하면 “중간에서 끊긴 요청”이 늘어납니다. 운영에서 가장 흔한 함정 중 하나입니다.
- 그레이스풀 셧다운: 배포/재시작 시 처리 중인 요청을 어떻게 마무리할지(유예 시간) 정책이 필요합니다. graceful-timeout 같은 옵션은 이 영역과 맞닿아 있습니다.
- 로그: access log, error log를 분리하고, 요청 단위 추적(trace id) 또는 correlation id를 붙이면 장애 분석 속도가 크게 올라갑니다.
- 헬스체크: 컨테이너/로드밸런서 환경에서는 /health 같은 엔드포인트와 readiness/liveness 설계를 해두면 교체/확장 시 사고가 줄어듭니다.
실행 단계: 비교판을 직접 만들어 보는 학습 루트
- 현재 프로젝트의 ASGI 진입점(예: app.main:app)을 확인합니다. 초보 단계에서 가장 자주 막히는 곳이 “모듈 경로 표기”입니다.
- Uvicorn 단독으로 먼저 실행해 정상 동작을 확인합니다. 이 단계는 “앱 자체가 제대로 도는지”를 빠르게 검증하는 데 유리합니다.
- 동일한 워커 수로 Gunicorn+UvicornWorker 조합을 실행해 봅니다. 이제 비교 포인트는 성능보다 “프로세스 모델(마스터/워커), 로그, 신호 처리”에 있습니다.
- 의도적으로 워커를 종료해(예: 프로세스 kill) 복구 동작을 관찰해 보세요. 조합에서는 마스터가 워커를 다시 띄우는 모습을 확인할 수 있습니다.
- 운영 옵션(타임아웃, 워커 수, max-requests)을 바꿔가며 로그와 응답을 관찰합니다. ‘옵션이 왜 존재하는지’가 체감되면 운영 감각이 빠르게 올라갑니다.
- 마지막으로 본문에 제시된 “운영 옵션 비교표”를 본인 환경에 맞게 수정해 보세요. 팀/인프라가 달라지면 추천 결론도 달라질 수 있기 때문입니다.
추가로 생각해볼 점
- “조합이 무조건 더 좋다”는 결론은 위험합니다. 오케스트레이션이 강한 환경에서는 Uvicorn 단독이 더 단순하고, 운영 비용도 낮아질 수 있습니다.
- 반대로 VM 중심, 장기 운영, 운영팀 표준이 Gunicorn에 맞춰져 있다면 조합이 실수와 시행착오를 줄여주는 경우가 많습니다.
- 중요한 것은 도구 이름이 아니라, 장애 시나리오(프로세스 죽음/배포/트래픽 급증/타임아웃)를 누가 어떤 방식으로 책임지는지입니다.
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
'Server' 카테고리의 다른 글
| Uvicorn 서버 8회차 : 배포 준비 2 - 리버스 프록시(Nginx)와 포트 설계 (0) | 2025.12.27 |
|---|---|
| Uvicorn 서버 7회차 : 배포 준비 1 - 환경변수, 설정 분리, 비밀키 관리 감각(.env / APP_ENV / PORT / LOG_LEVEL) (0) | 2025.12.27 |
| Uvicorn 서버 5회차 : 성능의 첫 관문 – 워커(worker)와 동시성(concurrency) (0) | 2025.12.26 |
| Uvicorn 서버 4회차 : 로그를 다루는 법, 문제의 80%는 로그에서 끝난다 (0) | 2025.12.23 |
| Uvicorn 서버 3회차 : 개발 모드의 핵심, reload와 코드 변경 반영 (1) | 2025.12.22 |