Django는 실제 서버에선 왜 runserver를 하지 않고 웹서버+wsgi를 사용할까?#
“그냥 다들 사용하니까”, “검색하면 이렇게 나오니까”
라는 이유 말고 왜라는 질문에서 시작하여 이 내용을 정리하게 되었습니다.
1. 클라이언트와 서버의 구조#
내용을 작성하기 앞서 애플리케이션은 요청과 응답이 어떻게 동작하는지에 대해 짚고 넘어가야합니다.
아래 그림을 보며 간략히 설명을 작성하였습니다.
1-1. 동작 순서#
사용자가 앱이나 웹브라우저 환경에서 API 서버에 요청을 하게 됩니다.
해당 요청은 가장 먼저 웹서버에 가게 됩니다.
- 클라이언트 API 호출
- 가장 먼저 웹서버로 접속합니다 (nginx)
- 소켓을 통해 wsgi까지 도달 (wsgi: 장고와 웹서버의 데이터 통신을 교환할 수 있게 도와주는 인터페이스)
- 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 명령어 인터페이스 기능을 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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()
함수를 실행시키는 것을 확인할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
|
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)
을 확인할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
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 클래스의 함수인 것을 확인할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
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
를 확인할 수 있는데
이 클래스를 확인해보면 다음과 같은 주석을 확인할 수 있습니다.
1
2
3
4
5
6
7
|
"""
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#