ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Django] runserver 로 실제 서버를 돌리면 안되는 이유 (런서버는 어떻게 동작하나??)
    Programing/Python 2020. 3. 21. 15:00

    django를 개발하다보면, 혹은 다른 프레임워크로 웹백엔드를 개발하다 보면 앵간하면 개발 서버를 켜는 명령어가 있다.
    django에서는 python manage.py runserver 라는 명령어를 치면 아주쉽게 간편하게 로컬에 서버가 켜진다.
    아쉽게도 개발하다가 많은 사람들을 만나다보면 그냥 되는게 되는거고 말면 마는 분들이 꽤나 있는것같다.

    '메인 서버에 올릴때 runserver로 그냥 켜놔도 되는거아니에요?'
    '그냥 runserver nohup으로 백그라운드로 돌게하면 되는거아니에요?'
    '그냥 검색해보니까 nginx+uwsgi 이렇게 메인에 배포하라는데 이유는 잘모르겠네요 ㅎㅎ'
    라는 질문을 하는 분들이 꽤나 많다.

    잘은 모르지만 was + wsgi 로 다들 조합해서 하는거 같으니까..
    단지 검색하다보니까 어떤 조합으로 해서하면 좋다더라 하는 이런 글을 보고 그냥 쓰시는 분들도 많은것 같다.
    (물론 잘 알고 쓰시는분들도 정말 많다 - 세상은 넓고 고수는 많다.)

    해당 포스트는 이제 개발을 시작하시는 분들 초보자 or 신입분들을 위한 글이니
    그 분들의 초점으로 글을 쓰겠다.
    (그말인 즉슨 고수님들은 굳이 안보셔도 된다는 소리..)


    일단 runserver 라는 명령어가 어떻게 켜지는지 부터 볼 필요가 있다.
    [프로젝트를 아에 켜셔서 따라 가시는걸 추천합니다. 코드가 길어서 보기 힘들 수 도 있습니다.]

    django 프로젝트를 생성하면 manage.py 라는 파일이 가장 바깥쪽 경로에 생성이 된다.
    이 해당 manage.py 는 django 명령어를 인식하게 하기위한 중간역할 즉, django의 명령어 인터페이스라고 보면 된다.

    #!/usr/bin/env python
    """Django's command-line utility for administrative tasks."""
    import os
    import sys
    
    
    def main():
        os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ProjectName.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()

    manage.py을 열어보면 위의 코드가 기본적으로 작성되어있다.

    1. 프로젝트의 세팅을 뭘로 볼껀지 환경변수에 등록을 한다.
    2. django가 안깔려있면 예외처리
    3. excute_from_command_line 이라는 함수가 받아온 sys.args로 실행된다.
    [오호~ 이곳이 뭔가 처리하는 곳이다.]

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

    해당 함수를 따라가면 ManagementUtility라는 클래스를 args로 가져오고
    그것을 excute라는 클래스내 함수를 실행시킨다는 것을 알 수 있다.
    계속 들어가보자!!

    우리는 ManagementUtiliuty라는 아이를 볼필요가 있다.
    (클래스가 꽤 길기 때문에 특징적인것만 넣겠다. 그외 안쪽 함수들은 직접 보시길...)

    class ManagementUtility:
        """
        Encapsulate the logic of the django-admin and manage.py utilities.
        """
        def __init__(self, argv=None):
            self.argv = argv or sys.argv[:]
            self.prog_name = os.path.basename(self.argv[0])
            if self.prog_name == '__main__.py':
                self.prog_name = 'python -m django'
            self.settings_exception = None
            
            
        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)

    클래스의 생성자는 해당 args를 세팅하는 모양이다.
    그리고나서 excute를 실행했으니 excute 함수를 보자!

    1. 가져온 args를 subcommand라는 변수에 넣는다. (없을 시 help를 넣는다.)
    2. 그리고 CommandParser라는 클래스를 세팅하면서 두번째 인자값을 찾아본다.
    python manage.py runserver --settings=settings.product 같은 옵션을 넣을때 사용하는 친구로 보인다.
    (해당 포스팅과는 관련이 적으니 넘어가겠다.)
    3. 그리고나서 세팅에 설정한 앱들을 체크한다.
    4. 이제서야 커맨드의 값이 runserver 일때 무엇을 할 것인지 나온다.

    간단하게 살펴보면 에러를 체크해서 예외처리를 한 이 후에 가장 마지막으로 해당 커맨드가 help나 version, --help, -h 가 아니면 fetch_command라는 함수를 실행시킨 값을 토대로 run_from_args라는 아이를 실행시킨다.
    이런식으로 쭈욱 따라가다 보면 정적파일들을 체크하고 경로를 설정하고, 각 종 django에 관련있는 아이들을 체크하고, DB도 체크하고 커낵션도 맺고, 로그를 어떻게보여줄지 설정하고, 경로를 설정하고 온갖 난리를 치다가 마지막에 결국 runserver에 맞는 모듈을 찾아서 해당 모듈을 실행시킨다.

    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 inner_run(self, *args, **options):
            # If an exception was silenced in ManagementUtility.execute in order
            # to be raised in the child process, raise it now.
            autoreload.raise_last_exception()
    
            threading = options['use_threading']
            # 'shutdown_message' is a stealth option.
            shutdown_message = options.get('shutdown_message', '')
            quit_command = 'CTRL-BREAK' if sys.platform == 'win32' else 'CONTROL-C'
    
            self.stdout.write("Performing system checks...\n\n")
            self.check(display_num_errors=True)
            # Need to check migrations here, so can't use the
            # requires_migrations_check attribute.
            self.check_migrations()
            now = datetime.now().strftime('%B %d, %Y - %X')
            self.stdout.write(now)
            self.stdout.write((
                "Django version %(version)s, using settings %(settings)r\n"
                "Starting development server at %(protocol)s://%(addr)s:%(port)s/\n"
                "Quit the server with %(quit_command)s.\n"
            ) % {
                "version": self.get_version(),
                "settings": settings.SETTINGS_MODULE,
                "protocol": self.protocol,
                "addr": '[%s]' % self.addr if self._raw_ipv6 else self.addr,
                "port": self.port,
                "quit_command": quit_command,
            })
    
            try:
                handler = self.get_handler(*args, **options)
                run(self.addr, int(self.port), handler,
                    ipv6=self.use_ipv6, threading=threading, server_cls=self.server_cls)
            except OSError as e:
                # Use helpful error messages instead of ugly tracebacks.
                ERRORS = {
                    errno.EACCES: "You don't have permission to access that port.",
                    errno.EADDRINUSE: "That port is already in use.",
                    errno.EADDRNOTAVAIL: "That IP address can't be assigned to.",
                }
                try:
                    error_text = ERRORS[e.errno]
                except KeyError:
                    error_text = e
                self.stderr.write("Error: %s" % error_text)
                # Need to use an OS exit because sys.exit doesn't work in a thread
                os._exit(1)
            except KeyboardInterrupt:
                if shutdown_message:
                    self.stdout.write(shutdown_message)
                sys.exit(0)

    그래서 결국 열리는 파일은 django/core/management/commands/runserver.py 라는 아이다.
    물론 해당 클래스도 꽤나 길기 때문에 사용되는 주요함수를 보겠다.
    일단 클래스 자체가 기본으로 로컬아이피와 8000포트를 잡고 있다. 따라서 우리는 python manage.py runserver 라고만 쳤을때
    127.0.0.1:8000 이 열린다는 것을 알 수 있다. 그 이후 실행되는 inner_run이라는 함수가 초반에 무언가 쫘악 셋팅을 한다.

    1. 서버를 끄는 커맨드셋팅
    2. 마이그레이션 체크
    3. 현재시간을 가져와서 결국 로그창에 지금까지 했던 셋팅들을 띄운다. (↓ 바로 우리가 매일 보는 이것)

    Performing system checks...
    
    System check identified no issues (0 silenced).
    
    You have 17 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
    Run 'python manage.py migrate' to apply them.
    March 21, 2020 - 14:26:31
    Django version 3.0.3, using settings 'ProjectName.settings'
    Starting development server at http://127.0.0.1:8000/
    Quit the server with CTRL-BREAK.

    4. 그리고나서 run이라는 함수를 실행시킨다.

    이 run이라는 함수는 django.core.servers.basehttp 에 있는 말그대로 python이 제공하는 http server를 여는것이다.
    (해당 코드는 직접 살펴 보시길 - 간략하게 프로토콜은 HTTP/1.1, 요청수의 최대값, 각종 예외처리 등을 하고 서버를 연다.)
    잘 살펴보면 https를 사용하면 예외처리를 한다는것을 알 수 있다.
    이거 하나만으로도 보안처리를 최대한 생략한다는것을 알 수 있다.

    생략한 과정이 꽤나 있었지만 그럼에도 불구하고 이 많은 함수를 거쳐서 우리가 간단하게 쳤던 그 명령어 runserver 하나로 드디어 서버가 열린것이다.

    django.core.servers.basehttp의 가장 위 주석을 보면

    """
    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!
    """

    요런식으로 써있다.
    이 주석의 3번째문단이 해당 포스팅의 내용을 다 담고있다고 볼수 있다.
    다들 영어 잘하시겠지만 간단하게 해당 심플서버는 테스팅이나 디버깅할때 사용한다. 보안적으로 안좋으니, 메인서버에는 사용하지 마라!!! 이말이다.

    어쩌다보니 runserver가 켜지는 과정을 포스팅해버리고 말았는데, 포스팅이 길어져서 다음 포스팅에 연결해서 써야겠다.
    우리는 마지막 문장을 꼭 기억하자 DON'T USE IT FOR PRODUCTION USE!


    ps.1. 내가 개발을 하는 초보분들에게 항상 추천하는 방법은 해당 라이브러리를 까보라는 것이다.
    ps.2. 왜냐면 아무리 본인 잘짜도 검증된 라이브러리의 코드만큼 공부가 많이 되는것이 없기때문이다.
    ps.3. 그러다가 나중엔 맘에안드는 코드부분의 컨트리뷰터 까지 된다면 금상첨화
    ps.4. 이렇게 공부를 하면 의미없는 1일1커밋보다 훨씬 공부가 될 것이다.
    ( 1일 1커밋을 할 바에 잠을 자는게 더 낫다. - https://twowix.me/84 )
    ps.5. django는 각 파일마다 주석만 봐도 이파일이 뭐하는앤지 아주 잘 설명해뒀다... 미친 프레임워크다...

    댓글

Designed by Tistory.