본문 바로가기

Posts/Django

Django 실제 서버에 왜 runserver를 하지 않고 웹서버+wsgi를 사용할까?

반응형

"그냥 다들 사용하니까", "검색하면 이렇게 나오니까"

라는 이유 말고 왜라는 질문에서 시작하여 이 내용을 정리하게 되었습니다.


1. 클라이언트와 서버의 구조

내용을 작성하기 앞서 애플리케이션은 요청과 응답이 어떻게 동작하는지에 대해 짚고 넘어가야합니다.

아래 그림을 보며 간략히 설명을 작성하였습니다.

1_JI0ZJiVs7PDDUZlXAf59gg

nginx-uwsgi-django-stack

1-1. 동작 순서

사용자가 앱이나 웹브라우저 환경에서 API 서버에 요청을 하게 됩니다.

해당 요청은 가장 먼저 웹서버에 가게 됩니다.

  1. 클라이언트 API 호출
  2. 가장 먼저 웹서버로 접속합니다 (nginx)
  3. 소켓을 통해 wsgi까지 도달 (wsgi: 장고와 웹서버의 데이터 통신을 교환할 수 있게 도와주는 인터페이스)
  4. wsgi에서 django로 전달되어 프로젝트내 파이썬 코드가 실행

1-2. webserver

클라이언트 요청을 받아 애플리케이션에 전달하고 응답을 받아 다시 클라이언트에게 전달하는 역할을 합니다.

단순히 연결고리 역할, 즉 전달의 기능만 하고 정적 데이터(html, css, image, file)등은 웹서버 자체내에서 바로 응답을 줍니다.

이렇게 정적파일을 따로 관리하기 위한 용도로 사용하며

이를 통해 서버까지 요청과 리소스의 부담을 줄여 조금 더 효율적인 서빙을 할 수 있습니다.

  • SSL
  • 로드밸런싱, 캐싱
  • 동적 요청을 wsgi 전달
  • 정적 파일 제공

1-3. wsgi

Web Server Gateway Interface란 뜻으로 웹서버와 웹프레임워크 사이에 통신을 담당합니다.

웹서버는 html, js외에 서버쪽 python, java등으로 구성된 프레임워크 언어를 해석할 수 없습니다.

이를 해결해주는 것이 wsgi의 역할이고 wsgi는 비동기 방식의 콜백함수로 이루어져

쓰레드를 생성하지 않고도 여러 요청을 동시에 처리할 수 있습니다.

대표적으로 사용하는 것은 uwsgi, gunicorn등이 있습니다.

  • 웹서버 요청과 앱의 응답을 번역 및 http 응답으로 변환
  • 요청이 들어오면 파이썬 코드 호출

1-4. socket

웹서버와 wsgi 사이에 데이터를 주고받기 위한 인터페이스를 말합니다.

웹서버와 wsgi 통신 방법은 크게 통신 방법으로는 http 네트워크 통신과 linux 소켓 방식이 있습니다.

http는 클라이언트의 요청이 있을때 해당 데이터를 전송 후 연결을 종료합니다.

sockect의 경우는 서버 또는 클라이언트중 하나라도 접속을 끊을때까지 연결이 유지가 되어 있기에

실시간으로 데이터 교류가 필요할 때에는 socket방식을 더 많이 사용하게 됩니다.

wsgi 통신은 기본적으로 socket 방식을 사용 하는데

이는 wsgi가 서버 안쪽에서 웹프레임워크의 중계역할을 하기 때문에 상대적으로 가벼운 socket방식을 사용하게 됩니다.


2. django runsever

django 프로젝트를 생성하면 루트 경로에 manage.py 파일이 있습니다. 이 파일은 django 명령어 인터페이스 기능을 합니다.

def main():
        os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv) # 찾아가보자


if __name__ == '__main__':
    main()


def execute_from_command_line(argv=None):
    """Run a ManagementUtility."""
    utility = ManagementUtility(argv)
    utility.execute() # 실행

main() 함수를 보면 execute_from_command_line(sys.argv) 가 실행되는 것을 확인할 수 있습니다.

해당 함수를 따라가서 보면 ManagementUtility(argv) 라는 클래스에 args라는 파라미터를 가진 인스턴스를 가지고

excute() 함수를 실행시키는 것을 확인할 수 있습니다.

    def execute(self):
        """
        Given the command-line arguments, figure out which subcommand is being
        run, create a parser appropriate to that command, and run it.
        """
        try:
            subcommand = self.argv[1]
        except IndexError:
            subcommand = 'help'  # Display help if no arguments were given.

        # Preprocess options to extract --settings and --pythonpath.
        # These options could affect the commands that are available, so they
        # must be processed early.
        parser = CommandParser(usage='%(prog)s subcommand [options] [args]', add_help=False, allow_abbrev=False)
        parser.add_argument('--settings')
        parser.add_argument('--pythonpath')
        parser.add_argument('args', nargs='*')  # catch-all
        try:
            options, args = parser.parse_known_args(self.argv[2:])
            handle_default_options(options)
        except CommandError:
            pass  # Ignore any option errors at this point.

        try:
            settings.INSTALLED_APPS
        except ImproperlyConfigured as exc:
            self.settings_exception = exc
        except ImportError as exc:
            self.settings_exception = exc

        if settings.configured:
            # Start the auto-reloading dev server even if the code is broken.
            # The hardcoded condition is a code smell but we can't rely on a
            # flag on the command class because we haven't located it yet.
            if subcommand == 'runserver' and '--noreload' not in self.argv:
                try:
                    autoreload.check_errors(django.setup)()
                except Exception:
                    # The exception will be raised later in the child process
                    # started by the autoreloader. Pretend it didn't happen by
                    # loading an empty list of applications.
                    apps.all_models = defaultdict(dict)
                    apps.app_configs = {}
                    apps.apps_ready = apps.models_ready = apps.ready = True

                    # Remove options not compatible with the built-in runserver
                    # (e.g. options for the contrib.staticfiles' runserver).
                    # Changes here require manually testing as described in
                    # #27522.
                    _parser = self.fetch_command('runserver').create_parser('django', 'runserver')
                    _options, _args = _parser.parse_known_args(self.argv[2:])
                    for _arg in _args:
                        self.argv.remove(_arg)

            # In all other cases, django.setup() is required to succeed.
            else:
                django.setup()

        self.autocomplete()

        if subcommand == 'help':
            if '--commands' in args:
                sys.stdout.write(self.main_help_text(commands_only=True) + '\n')
            elif not options.args:
                sys.stdout.write(self.main_help_text() + '\n')
            else:
                self.fetch_command(options.args[0]).print_help(self.prog_name, options.args[0])
        # Special-cases: We want 'django-admin --version' and
        # 'django-admin --help' to work, for backwards compatibility.
        elif subcommand == 'version' or self.argv[1:] == ['--version']:
            sys.stdout.write(django.get_version() + '\n')
        elif self.argv[1:] in (['--help'], ['-h']):
            sys.stdout.write(self.main_help_text() + '\n')
        else:
            self.fetch_command(subcommand).run_from_argv(self.argv)

excute() 함수 안에 코드를 보면 주석으로 된 것을 확인할 수 있습니다.

간략히 해석해보면 명령 인자가 주어진 것을 토대로 하위 명령을 사용하는지 파악 후 실행하는 것이고

명령에 따른 여러 파서도 생성과 실행을 한다고 적혀있습니다.

이를 통해 알 수 있는 것은 무언가 runserver를 했을때 동작하는 구성이 작성되어 있다라는 것을 짐작할 수 있습니다.

계속 따라 내려가다보면 self.fetch_command(subcommand).run_from_argv(self.argv)을 확인할 수 있습니다.

    def run_from_argv(self, argv):
        """
        Set up any environment changes requested (e.g., Python path
        and Django settings), then run this command. If the
        command raises a ``CommandError``, intercept it and print it sensibly
        to stderr. If the ``--traceback`` option is present or the raised
        ``Exception`` is not ``CommandError``, raise it.
        """
        self._called_from_command_line = True
        parser = self.create_parser(argv[0], argv[1])

        options = parser.parse_args(argv[2:])
        cmd_options = vars(options)
        # Move positional args out of options to mimic legacy optparse
        args = cmd_options.pop('args', ())
        handle_default_options(options)
        try:
            self.execute(*args, **cmd_options) # 이녀석
        except CommandError as e:
            if options.traceback:
                raise

            # SystemCheckError takes care of its own formatting.
            if isinstance(e, SystemCheckError):
                self.stderr.write(str(e), lambda x: x)
            else:
                self.stderr.write('%s: %s' % (e.__class__.__name__, e))
            sys.exit(e.returncode)
        finally:
            try:
                connections.close_all()
            except ImproperlyConfigured:
                # Ignore if connections aren't setup at this point (e.g. no
                # configured settings).
                pass

다시 해당 함수를 들어가보면 주석으로된 내용을 확인해봅니다.

요청된 환경 설정을 토대로 명령을 실행합니다.

사실 runserver.py는 Command 클래스에서 구동이 되는데

이 Command클래스는 BaseCommand의 상속받은 클래스인 것을 확인할 수 있습니다.

최종적으로 실행하는 부분은 self.execute(*args, **cmd_options) 이며

이 부분은 BaseCommand 클래스의 함수인 것을 확인할 수 있습니다.

class Command(BaseCommand):
    help = "Starts a lightweight Web server for development."

    # Validation is called explicitly each time the server is reloaded.
    requires_system_checks = False
    stealth_options = ('shutdown_message',)

    default_addr = '127.0.0.1'
    default_addr_ipv6 = '::1'
    default_port = '8000'
    protocol = 'http'
    server_cls = WSGIServer # 요기

    def add_arguments(self, parser):
        parser.add_argument(
            'addrport', nargs='?',
            help='Optional port number, or ipaddr:port'
        )
        parser.add_argument(
            '--ipv6', '-6', action='store_true', dest='use_ipv6',
            help='Tells Django to use an IPv6 address.',
        )
        parser.add_argument(
            '--nothreading', action='store_false', dest='use_threading',
            help='Tells Django to NOT use threading.',
        )
        parser.add_argument(
            '--noreload', action='store_false', dest='use_reloader',
            help='Tells Django to NOT use the auto-reloader.',
        )

    def execute(self, *args, **options):
        if options['no_color']:
            # We rely on the environment because it's currently the only
            # way to reach WSGIRequestHandler. This seems an acceptable
            # compromise considering `runserver` runs indefinitely.
            os.environ["DJANGO_COLORS"] = "nocolor"
        super().execute(*args, **options)

    def get_handler(self, *args, **options):
        """Return the default WSGI handler for the runner."""
        return get_internal_wsgi_application()

self.execute()는 결국 Command(BaseCommand) 을 바라보는 것이었고 해당 클래스내에는 실행함수외

wsgi handler, add_args 등 다양한 함수들이 있습니다.

여기서 server_cls 설정에 WSGIServer 를 확인할 수 있는데 이 클래스를 확인해보면

다음과 같은 주석을 확인할 수 있습니다.

"""
HTTP server that implements the Python WSGI protocol (PEP 333, rev 1.21).
Based on wsgiref.simple_server which is part of the standard library since 2.5.
This is a simple server for use in testing or debugging Django apps. It hasn't
been reviewed for security issues. DON'T USE IT FOR PRODUCTION USE!
"""

해석해보자면 python wsgi http서버로 Django 앱을 테스트하거나 디버깅하는 용도로 사용할 수 있는 간단한 서버입니다.

보안 이슈가 있으니 DON'T USE IT FOR PRODUCTION USE! 절대 메인용으로는 사용하지 말아라


3. 결론

요약하자면 django runserver는 개발 및 테스트, 디버깅용으로 사용할 뿐 보안과 성능테스트를 거치지 않았기에

wsgi와 웹서버로 서비스하도록 권장한다고 나와있습니다. 공식문서 링크

wsgi와 웹서버를 같이 사용하는 이유는 위에서도 설명했지만 웹서버를 통해 더 좋은 성능을 낼 수 있으며

몇몇 wsgi는 SSL과 정적 파일을 지원하지 않고 DDos 공격과 같은 많은 주기적 요청을

웹서버가 처리 및 분산시킴으로 서버의 안정성을 보장받을 수 있기 때문입니다.


References



반응형