본문 바로가기
Server

Uvicorn 서버 6회차 : 운영 실행 방식 — Uvicorn 단독 vs Gunicorn+UvicornWorker, 왜 조합을 쓰는가?

by 마틴블레이크 2025. 12. 27.
반응형

Uvicorn 서버 6회차 : 운영 실행 방식 — Uvicorn 단독 vs Gunicorn+UvicornWorker, 왜 조합을 쓰는가?

한눈에 보는 요약

  • Uvicorn은 FastAPI/Starlette 같은 ASGI 앱을 실행하는 서버입니다. 단독으로도 충분히 운영이 가능합니다.
  • 그럼에도 프로덕션에서 Gunicorn(프로세스 매니저) + UvicornWorker(ASGI 워커) 조합을 많이 쓰는 이유는 “요청 처리 성능”보다 운영 안정성(프로세스 관리, 재시작/롤링, 타임아웃, 로그, 워커 제어)을 더 체계적으로 가져가기 위함인 경우가 많습니다.
  • 핵심은 “Uvicorn이 나쁘다”가 아니라, 운영 환경에서 필요한 기능(감시/복구/정교한 워커 정책)을 누가 책임질지의 선택입니다. 컨테이너 오케스트레이션(Kubernetes 등)이 그 역할을 강하게 해주면 Uvicorn 단독이 충분히 깔끔해지기도 합니다.
  • 이번 글의 산출물은 운영용 실행 옵션 비교표(단독/조합)이며, (선택) Linux에서 gunicorn 실행 실습까지 포함합니다.

목차


핵심 포인트

  • 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 설계를 해두면 교체/확장 시 사고가 줄어듭니다.

실행 단계: 비교판을 직접 만들어 보는 학습 루트

  1. 현재 프로젝트의 ASGI 진입점(예: app.main:app)을 확인합니다. 초보 단계에서 가장 자주 막히는 곳이 “모듈 경로 표기”입니다.
  2. Uvicorn 단독으로 먼저 실행해 정상 동작을 확인합니다. 이 단계는 “앱 자체가 제대로 도는지”를 빠르게 검증하는 데 유리합니다.
  3. 동일한 워커 수로 Gunicorn+UvicornWorker 조합을 실행해 봅니다. 이제 비교 포인트는 성능보다 “프로세스 모델(마스터/워커), 로그, 신호 처리”에 있습니다.
  4. 의도적으로 워커를 종료해(예: 프로세스 kill) 복구 동작을 관찰해 보세요. 조합에서는 마스터가 워커를 다시 띄우는 모습을 확인할 수 있습니다.
  5. 운영 옵션(타임아웃, 워커 수, max-requests)을 바꿔가며 로그와 응답을 관찰합니다. ‘옵션이 왜 존재하는지’가 체감되면 운영 감각이 빠르게 올라갑니다.
  6. 마지막으로 본문에 제시된 “운영 옵션 비교표”를 본인 환경에 맞게 수정해 보세요. 팀/인프라가 달라지면 추천 결론도 달라질 수 있기 때문입니다.

추가로 생각해볼 점

  • “조합이 무조건 더 좋다”는 결론은 위험합니다. 오케스트레이션이 강한 환경에서는 Uvicorn 단독이 더 단순하고, 운영 비용도 낮아질 수 있습니다.
  • 반대로 VM 중심, 장기 운영, 운영팀 표준이 Gunicorn에 맞춰져 있다면 조합이 실수와 시행착오를 줄여주는 경우가 많습니다.
  • 중요한 것은 도구 이름이 아니라, 장애 시나리오(프로세스 죽음/배포/트래픽 급증/타임아웃)를 누가 어떤 방식으로 책임지는지입니다.

 

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

반응형