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 Interface나 Reactive 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의 속도"**를 합치기로 합니다.
gunicorn -k uvicorn.workers.UvicornWorker ...
- -k uvicorn.workers.UvicornWorker: 이 옵션이 핵심입니다.
- Gunicorn에게 말합니다. "야, 너 원래 일꾼(Worker)으로 WSGI 쓰지? 이번엔 그거 말고 Uvicorn(ASGI) 일꾼 좀 가져다 써."
동작 구조 (식당 비유)
- Gunicorn (점장님): 가게 문을 열고(Port Bind), 요리사 4명(Process)을 고용합니다. 그리고 딴짓 안 하는지 감시합니다.
- Uvicorn (요리사): 각 프로세스 안에서 실제로 요리를 합니다(FastAPI 실행). 손이 엄청 빠릅니다(비동기).
- 결과: 물리적 서버는 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은 유효합니다.