본문 바로가기
카테고리 없음

[FastAPI] Gunicorn과 Uvicorn, 도대체 왜 섞어 쓸까? (WSGI vs ASGI)

by Lee David 2026. 1. 8.
반응형

Spring Boot로 개발할 때는 고민할 필요가 없었습니다. 내장 Tomcat이 알아서 다 해주니까요.

그런데 Python FastAPI로 넘어오니 배포 명령어부터가 복잡합니다.

# Dockerfile에서 자주 보는 이 명령어...
CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "app.main:app"]

 

gunicorn은 뭐고, uvicorn은 뭘까요? 왜 둘을 섞어 쓰지?

오늘은 Python 웹 서버의 핵심 개념인 WSGI/ASGI와 Gunicorn/Uvicorn의 관계를 Spring 개발자의 시선으로 완벽하게 정리해 봅니다.


1. 프로토콜(약속): WSGI vs ASGI

이 둘은 소프트웨어가 아니라, **웹 서버와 파이썬 애플리케이션이 대화하는 규칙(Interface)**입니다. Spring으로 치면 Servlet InterfaceReactive Streams 같은 개념.

WSGI (Web Server Gateway Interface)

  • 성격: 동기(Synchronous), 블로킹(Blocking)
  • 대표주자: Django, Flask
  • 특징:
    • 오래된 표준입니다. 요청 하나가 오면 처리가 끝날 때까지 스레드를 붙잡고 기다림.
    • 마치 전화기 한 대로 한 명하고만 통화하는 것.
    • async/await 같은 비동기 처리를 못 함.

ASGI (Asynchronous Server Gateway Interface)

  • 성격: 비동기(Asynchronous), 논블로킹(Non-blocking)
  • 대표주자: FastAPI, Sanic
  • 특징:
    • 최신 표준입니다. 요청을 받아두고(await), DB 조회 같은 시간이 걸리는 작업이 있으면 다른 요청을 받으러 감.
    • Spring의 **WebFlux(Netty)**와 같은 개념.
    • 웹소켓(WebSocket)이나 채팅 서버 같은 실시간 처리에 필수적.

요약: FastAPI는 "비동기" 처리를 위해 태어났으므로, 옛날 규약인 WSGI가 아닌 ASGI를 사용합니다.


2. 실행기(Server): Gunicorn vs Uvicorn

규약이 있다면, 그 규약을 실제로 돌리는 **소프트웨어(엔진)**가 있어야겠죠?

Uvicorn: "엄청 빠른 일꾼"

  • 정체: ASGI 웹 서버
  • 특징:
    • uvloop라는 C언어 기반의 비동기 라이브러리를 써서 Node.js나 Go만큼 빠릅니다.
    • FastAPI 코드를 실제로 실행하는 녀석입니다.
  • 단점: "프로세스 관리" 능력이 부족합니다. 혼자서 실행되다가 죽으면 끝이고, CPU 코어가 많아도 1개밖에 못 씁니다.

Gunicorn: "노련한 관리자"

  • 정체: WSGI 웹 서버 (프로세스 매니저)
  • 특징:
    • 원래는 Django/Flask 같은 동기 앱을 돌리기 위해 만들어졌습니다.
    • 하지만 **"프로세스 관리(Process Management)"**가 기가 막힙니다.
    • 워커(일꾼)를 여러 개 띄우고(Fork), 죽으면 살려내고, 개수를 조절합니다.
  • 단점: 얘는 옛날 방식(WSGI)이라 비동기(ASGI)를 모릅니다.

3. 합체: 왜 Gunicorn 안에 Uvicorn을 태울까?

여기서 의문이 생깁니다.

"FastAPI는 ASGI 앱이니까 Uvicorn만 쓰면 되는 거 아냐?"

개발 환경에선 그래도 됩니다. 하지만 **운영 환경(Production)**에선 문제가 있습니다.

Python에는 **GIL(Global Interpreter Lock)**이라는 락이 있어서, 프로세스 하나는 죽었다 깨어나도 CPU 코어 1개만 쓸 수 있기 때문입니다.

서버가 4코어짜리인데 Uvicorn 하나만 띄우면 나머지 3코어는 놀게 되죠.

그래서 우리는 **"Gunicorn의 관리 능력 + Uvicorn의 속도"**를 합치기로 합니다.

Bash
 
gunicorn -k uvicorn.workers.UvicornWorker ...
  • -k uvicorn.workers.UvicornWorker: 이 옵션이 핵심입니다.
    • Gunicorn에게 말합니다. "야, 너 원래 일꾼(Worker)으로 WSGI 쓰지? 이번엔 그거 말고 Uvicorn(ASGI) 일꾼 좀 가져다 써."

동작 구조 (식당 비유)

  1. Gunicorn (점장님): 가게 문을 열고(Port Bind), 요리사 4명(Process)을 고용합니다. 그리고 딴짓 안 하는지 감시합니다.
  2. Uvicorn (요리사): 각 프로세스 안에서 실제로 요리를 합니다(FastAPI 실행). 손이 엄청 빠릅니다(비동기).
  3. 결과: 물리적 서버는 1대지만, 내부적으론 4개의 서버가 동시에 돌아가는 효과를 냅니다.

스케일 아웃에 대해선 각각 서비스마다 다르겠지만 이런식으로 구성이 됨

4. Spring 개발자를 위한 한눈 비교

Java/Spring 생태계와 비교하면 이해가 훨씬 빠릅니다.

구분 Spring (Java) FastAPI (Python)
비동기 인터페이스 Reactive Streams (WebFlux) ASGI
실행 엔진 (I/O) Netty Uvicorn
프로세스 관리 JVM (내부 스레드 풀) Gunicorn (멀티 프로세스)
확장 방식 Multi-Thread (메모리 공유) Multi-Process (메모리 독립)

Spring은 JVM이 알아서 멀티 스레드로 CPU를 골고루 쓰지만, Python은 GIL 때문에 **Gunicorn으로 프로세스를 복제(Fork)**해야만 멀티 코어를 활용할 수 있다는 점이 가장 큰 차이입니다.


5. 결론: 언제 무엇을 쓸까?

  • 로컬 개발 (Local): uvicorn main:app --reload
    • 빠르고 간편하니까 Uvicorn만 씁니다.
  • 운영 배포 (Production): gunicorn -k uvicorn.workers.UvicornWorker ...
    • 서버가 죽지 않고, 모든 CPU 코어를 활용하기 위해 Gunicorn에 태워서 씁니다.

이제 Dockerfile에 적힌 그 긴 명령어가 단순히 복사 붙여넣기 한 코드가 아니라, Python의 한계를 극복하기 위한 최적의 아키텍처라는게 이해가 되기 시작합니다.


참고:

K8s(Kubernetes) 환경에서는 Gunicorn 없이 Uvicorn만 띄우고, 파드(Pod) 복제는 K8s에게 맡기는 경우도 있습니다. 하지만 파드 하나 안에서도 멀티 코어를 쓰고 싶다면 여전히 Gunicorn은 유효합니다.

반응형