✅ 오늘 배운 것
오늘은 비동기 뷰로 변환하기 위해서, gunicorn을 통해 WSGI 기반으로 동작하는 서버를 uvicorn, gunicorn을 같이 사용하여 ASGI 기반으로 동작하도록 변환해 줄 것이다.
공식문서를 참고해서 기존 커맨드를 다음과 같이 변경해 주었다.
# 기존 커맨드
gunicorn -w 2 --timeout 300 -b 0.0.0.0:8000 onestep_be.wsgi:application
# 새 커맨드
python -m gunicorn -w 2 -b 0.0.0.0:8000 onestep_be.asgi:application -k uvicorn_worker.UvicornWorker
-b는 --bind의 약자로, 0.0.0.0:8000 부분을 추가해주지 않으면 오직 localhost에서 오는 요청만 받는 것이 기본값으로 되어있다. 실제로 그래서 예전에 오류가 있었기에, 꼭 이 -b 옵션을 붙여주자.
또한 기존 커맨드는 gunicorn 기반으로 wsgi.py 코드를 실행하는 반면 새 커맨드는 uvicorn 기반으로 asgi.py 코드를 실행한다. 두 코드는 뭐가 다를까?
# wsgi.py
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'onestep_be.settings')
application = get_wsgi_application()
# asgi.py
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'onestep_be.settings')
application = get_asgi_application()
wsgi.py는 django.core.wsgi에서 get_wsgi_application()을 실행하는 반면 asgi.py는 django.core.asgi에서 get_asgi_application()을 실행하는 것이 유일한 차이였다. 함수 안을 보자.
내부 로직도 비슷하게 둘 다 각각 WSGIHandler, ASGIHandler를 호출하고 있었고, 두 핸들러는 모두 BaseHandler를 상속받고 있었다. BaseHandler의 로직은 복잡해서 다 이해하지는 못했지만, 핸들러가 호출되면 기본적으로 __call__ 메소드가 호출되고, 각각의 두 핸들러는 이걸 오버라이딩 한 것으로 보였다. 그래서 일단은 두 핸들러의 __call__ 메소드 부분만 가져와 보았다.
# wsgi.py
class WSGIHandler(base.BaseHandler):
request_class = WSGIRequest
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.load_middleware()
def __call__(self, environ, start_response):
set_script_prefix(get_script_name(environ))
signals.request_started.send(sender=self.__class__, environ=environ)
request = self.request_class(environ)
response = self.get_response(request)
response._handler_class = self.__class__
status = "%d %s" % (response.status_code, response.reason_phrase)
response_headers = [
*response.items(),
*(("Set-Cookie", c.output(header="")) for c in response.cookies.values()),
]
start_response(status, response_headers)
if getattr(response, "file_to_stream", None) is not None and environ.get(
"wsgi.file_wrapper"
):
# If `wsgi.file_wrapper` is used the WSGI server does not call
# .close on the response, but on the file wrapper. Patch it to use
# response.close instead which takes care of closing all files.
response.file_to_stream.close = response.close
response = environ["wsgi.file_wrapper"](
response.file_to_stream, response.block_size
)
return response
# asgi.py
class ASGIHandler(base.BaseHandler):
"""Handler for ASGI requests."""
request_class = ASGIRequest
# Size to chunk response bodies into for multiple response messages.
chunk_size = 2**16
def __init__(self):
super().__init__()
self.load_middleware(is_async=True)
async def __call__(self, scope, receive, send):
"""
Async entrypoint - parses the request and hands off to get_response.
"""
# Serve only HTTP connections.
# FIXME: Allow to override this.
if scope["type"] != "http":
raise ValueError(
"Django can only handle ASGI/HTTP connections, not %s." % scope["type"]
)
async with ThreadSensitiveContext():
await self.handle(scope, receive, send)
async def handle(self, scope, receive, send):
"""
Handles the ASGI request. Called via the __call__ method.
"""
# Receive the HTTP request body as a stream object.
try:
body_file = await self.read_body(receive)
except RequestAborted:
return
# Request is complete and can be served.
set_script_prefix(get_script_prefix(scope))
await signals.request_started.asend(sender=self.__class__, scope=scope)
# Get the request and check for basic issues.
request, error_response = self.create_request(scope, body_file)
if request is None:
body_file.close()
await self.send_response(error_response, send)
await sync_to_async(error_response.close)()
return
async def process_request(request, send):
response = await self.run_get_response(request)
try:
await self.send_response(response, send)
except asyncio.CancelledError:
# Client disconnected during send_response (ignore exception).
pass
return response
# Try to catch a disconnect while getting response.
tasks = [
# Check the status of these tasks and (optionally) terminate them
# in this order. The listen_for_disconnect() task goes first
# because it should not raise unexpected errors that would prevent
# us from cancelling process_request().
asyncio.create_task(self.listen_for_disconnect(receive)),
asyncio.create_task(process_request(request, send)),
]
await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
# Now wait on both tasks (they may have both finished by now).
for task in tasks:
if task.done():
try:
task.result()
except RequestAborted:
# Ignore client disconnects.
pass
except AssertionError:
body_file.close()
raise
else:
# Allow views to handle cancellation.
task.cancel()
try:
await task
except asyncio.CancelledError:
# Task re-raised the CancelledError as expected.
pass
try:
response = tasks[1].result()
except asyncio.CancelledError:
await signals.request_finished.asend(sender=self.__class__)
else:
await sync_to_async(response.close)()
body_file.close()
asgi.py는 asyncio라는 비동기 관련 모듈을 이용해서 handle() 함수에서 메인 로직을 실행하는 것으로 보였다. 그리고 응답 body를 여러 개의 메시지로 쪼갤 수 있다는 것을 감안해서(coroutine과 연관이 있는 듯 하다) chunk_size라는 변수도 선언해 준 것으로 보인다. 그리고 기본값으로 ASGI의 경우는 HTTP 요청만 실행하는 것으로 보였다. 이유가 왜인지는 모르겠다.
✅ 궁금한 점
1. 왜 ASGIHandler는 HTTP 요청만 서빙할 수 있도록 해 두었을까
2. asyncio라는 모듈은 장고뿐만 아니라 파이썬 내에서 사용되는 것으로 보이는데 이 모듈은 어떤 역할을 하는지도 알아보자.
'개발 일기장 > SWM Onestep' 카테고리의 다른 글
20240930 TIL: 사용자 문의 폼 적용하기 [진행중] (0) | 2024.09.30 |
---|---|
20240929 TIL: 사용자 문의 폼 만들기 [진행중] (0) | 2024.09.29 |
20240927 TIL: 알림 기능 개발하기 [끝] & 비동기 뷰로 변환하기 [진행중] (0) | 2024.09.27 |
20240926 TIL: 개발서버 오류 수정하기 & 알림 기능 개발하기 [진행중] (1) | 2024.09.26 |
20240925 TIL: 알림 기능 개발하기 [진행중] (1) | 2024.09.25 |