본 글은 WSGI에 대한 설명과는 다소 거리가 있습니다.
면접 준비를 위해 프로젝트를 다시 보던 중 ASGI와 WSGI를 사용한 기억이 떠올랐다. 당시 uWsgi, daphne를 사용했는데 이유가 기억나지 않아 찾아보았다. 대부분 블로그에서 Nginx 같은 웹 서버와 Python web application의 HTTP 소통을 위해 사용된다고 나와 있다. (물론 웹 서버 없이 wsgi server만 사용해도 된다는 내용도 있었다)
그렇구나 하고 넘어가려 했는데 urllib3
, requests
같은 모듈로 WSGI없이 HTTP 통신을 아주 잘 사용했던 기억이 떠올랐다.
urllib3
를 사용하면 Python이 HTTP를 잘 해석하는데 왜 굳이 WSGI가 필요할까?결론부터 말해보자
urllib3
는 HTTP client for Python이다. server가 아니라 client다.
urllib3
는 HTTP Request를 송신하고 HTTP Response를 수신한다. WSGI
는 HTTP Request를 수신하고 HTTP Response를 송신한다. 즉 해석하는 HTTP 구조가 WSGI와 다르다. (HTTP 구조에 대해 자세히 알고 싶으면 링크1 링크2)
통신 주체가 다르다.
웹서버와 Python 애플리케이션 간의 통신은 마치 카카오톡 <-> 라인과의 통신과도 같다. 두 개의 프로그램은 서로 독립적이다. 당연히 독립적인 프로세스 혹은 프로그램끼리 통신을 하기 위해서는 별도의 프로세스 혹은 프로그램이 필요하다.
조금만 생각해 보면 너무 당연한데 WS, WAS, HTTP, OS 등 여러 개념이 완벽히 잡히지 않아서 생긴 의문이다. 이제 아주 당연한 위 사실을 어떻게 알아냈는지와 ASGI를 살펴보자
request
를 통한 HTTP 통신과 uvicorn
를 통한 HTTP 통신과의 차이1번은 너무 간단하다. urllib3
를 통해 HTTP 요청을 보낼 때는 Dynamic port를 이용하고 method
를 함께 보내며 특정 행위를 요청한다. 아래 코드를 실행하며 포트 상태를 확인해보자.
import requests
import time
while True:
x = requests.get('https://w3schools.com/python/demopage.htm')
time.sleep(4)
아래 사진처럼 사용자 포트 번호는 여러개이다.
이번에는 uvicorn
으로 간단한 Fastapi
를 실행시키고 외부에서 접속해 보자.
Local Address의 8000번 포트에 접속 되는 것을 볼 수 있다. (unix 소켓의 경우 호스트 네트워크와 port를 바인드 할 때 사용되는 것으로 여기를 보면 알 수 있다.
번외) Local Address와 다중 IP에 대해
uvicorn으로 fastapi를 실행하는 과정에서 네트워크 지식 부족으로 삽질을 조금 했다. 바인딩을 처음에 localhost:8000으로 해서 외부 접속이 불가능하게 한 것... 이를 0.0.0.0:8000 으로 변경한 뒤 외부에서 접속이 잘 되는 것을 확인했다. 그런데 여기서 모든 0.0.0.0은 모든 IP 주소아닌가?
그럼 c4bcc73981ef와 바인딩 하는 것과 0.0.0.0와 바인딩 하는 것은 차이가 없지 않나? 컴퓨터가 여러개의 IP를 가질 수 있나? 하는 의문이 들었다.
결론은 컴퓨터가 여러개의 IP를 가지는 다중 IP는 가능하다!
2번을 살펴 보기전에 우선 WSGI에 대한 개념을 잠깐 환기하고 가자.
정적인 요청은 웹서버가 결과를 클라이언트에게 직접 보내주고 동적인 요청이 들어오면 비즈니스 로직을 수행하기 위해 Web Application에게 요청을 위임하고, Web Application 은 Web Server 에게 로직을 수행한 결과를 다시 돌려준다.
그럼 Web Server 가 Web Application 과 대화할 수 있는 인터페이스가 필요할 것이다. 또한 다양한 종류의 Web Server 와 Web Application 을 Interchangable 하게 사용하기 위해서는 잘 정의된 인터페이스가 필요하다.
간단히 말 해서 WSGI는 python의 callable 객체에 인자를 전해주고 python은 로직을 실행한 뒤 결과를 WSGI에게 돌려준다. 이후 WSGI -> Web -> client 과정을 거친다. 하지만 WSGI는 한 번에 하나의 요청만 동기로 처리한다. 따라서 WebSocket이나 long-poll HTTP를 지원하지 않는다.
또한 우리가 비동기 callable 객체를 만들더라고 WSGI가 동기 방식으로 동작하기 때문에 그 효과는 제한된다.
그래서 ASGI가 탄생했다. 여기서는 비동기를 지원하는 WSGI 정도로 이해하자. ASGI인 uvicorn과 Fastapi를 사용한 것과 Django(내장되어있는 서버 사용)의 예시를 보며 ASGI의 이점을 살펴보자.
Django의 코드는 아래처럼 되어있다. 대충 blabla/som 접속하면 7초간 대기 후 응답을 받는 코드이다.
from django.http import HttpResponse
import time
def index(request):
time.sleep(7)
return HttpResponse("Hello, world. You're at the polls index.")
여러 크롬 브라우저로 해당 url을 접속하면 순차적으로 실행이 된다. 즉 1,2,3,4 클라이언트가 거의 동시에 요청을 보내면 대략 28초가 걸린다. 여기를 참고해서 간단하게 Django앱을 만들고 실험해 볼 수 있다.
Fastapi의 코드. Django와 마찬가지로 blabla/a로 접속하면 7초간 대기 후 응답을 받는 코드이다. 심지어 async 비동기 처리도 아닌 그냥 동기 함수이다.
@app.get("/a")
def test():
time.sleep(7)
return {"test":"test"}
1,2,3,4 유저가 거의 동시에 접속을 하면 어떻게 될까? 놀랍게도 거의 7초에 모든 처리가 완료된다.(물론 상황마다 다를 수 있다.) 이유는 우선 ASGI가 비동기로 한 번에 여러개의 요청을 처리할 수 있고, Fastpi의 경우 동기 함수는 따로 thread pool을 만들어서 처리하기 때문이다. stackoverflow의 실험도 참고하면 좋다.
참고:
https://sgc109.github.io/2020/08/15/python-wsgi/
https://asgi.readthedocs.io/en/latest/introduction.html
https://www.itworld.co.kr/news/245062
https://www.uvicorn.org/
https://fastapi.tiangolo.com/async/#path-operation-functions
더 알아보면 좋을 것
파이썬의 멀티 프로세싱
- uvicorn --reload 옵션을 주면 코드에 수정사항이 있을 경우 알아서 재시작이 된다. 그런데 이 때
multiprocess
를 사용한다. GIL을 회피하는 방법이기도 하니 더 공부해 보자.