오늘 배운 것

오늘은 비동기 뷰로 변환하기 위해서, 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라는 모듈은 장고뿐만 아니라 파이썬 내에서 사용되는 것으로 보이는데 이 모듈은 어떤 역할을 하는지도 알아보자.

 

+ Recent posts