Django의 서버가 실행되는 과정과 그 서버에 요청이 어떻게 처리되는지를 알아보자.
Django 서버가 실행되는 과정
manage.py 커맨드가 실행되는 과정
runserver를 포함한 command가 실행되자마자 가장 먼저 manange.py 파일 내의 execute_from_command_line
함수가 호출된다. command 명령어가 실행될 때는 크게 3가지 과정을 거치게된다.
- 설정 값을 불러오고
- django.setup() 이 호출되고
- command 가 실행된다.
위 과정이 실행되는 실질적인 로직은 django > core > management > __init__.py
의 ManagementUtility.execute
메소드에 정의되어있다. 첫번재로 설정 값을 불러오는 단계를 보자. 이 메소드가 정의된 __init__.py
파일에는 from django.conf import settings
을 통해 settings
변수를 import 하지만 이 시점에는 settings.py
에 정의된 설정 값들이 실제로 로딩되지 않는다. 그 이유는 settings
는 LazySettings
인스턴스가 할당되어 있는데, 이 LazySettings
클래스는 setting이 실제로 사용되는 시점에 설정 값들을 로딩하게끔 lazy loading이 적용되어 있다. 또한 Singleton 패턴이 적용되어 django 어플리케이션 내에서 한번 인스턴스화 되고나면 django 코드 내에서 global하게 사용할 수 있다. 실제로 settings.INSTALLED_APPS
를 호출하게 되면서 모든 설정 값을 불러오게 된다. 두번째로 django.setup()
이 호출되는 과정이다. 이 과정에서는 설정 파일의 INSTALLED_APPS
내에 정의된 django app들의 configuration과 model을 저장하는 registry를 생성한다. 세번째로 command를 실행한다. 입력을 string 형태로 받지만 실행될 때는 django > core > management > commands
에 정의된 Command
인스턴스로 변환되어 실행된다. Command
클래스는 기본적으로 add_arguments
와 handle
메소드를 구현하고 handle
에서 실질적인 command 로직을 작성한다.
python manage.py runserver 를 실행하면 벌어지는 일
python manage.py runserver
를 호출하면 서버가 띄워지는데 이 과정을 좀 더 자세히 뜯어보자. 먼저 로컬에서 runserver를 실행하면 기본적으로 2개의 프로세스가 뜨게된다. runserver 커맨드는 기본적으로 auto-reloader가 적용되어 있어 코드 변경이 있을 때 자동으로 서버를 재시작하게 되는데, 이 때 메인 프로세스와 서브 프로세스에서 동일한 django 코드가 실행된다. auto-reloader 를 끄고 싶다면 --noreload
옵션을 추가해주면 된다. 실제로 RunCommand 클래스 내에 정의된 서버를 실행하는 코드는 아래와 같다.
def run(self, **options):
"""Run the server, using the autoreloader if needed."""
use_reloader = options["use_reloader"]
if use_reloader:
autoreload.run_with_reloader(self.inner_run, **options)
else:
self.inner_run(None, **options)
def run_with_reloader(main_func, *args, **kwargs):
signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
try:
if os.environ.get(DJANGO_AUTORELOAD_ENV) == "true":
reloader = get_reloader()
logger.info(
"Watching for file changes with %s", reloader.__class__.__name__
)
start_django(reloader, main_func, *args, **kwargs)
else:
exit_code = restart_with_reloader()
sys.exit(exit_code)
except KeyboardInterrupt:
pass
def restart_with_reloader():
new_environ = {**os.environ, DJANGO_AUTORELOAD_ENV: "true"}
args = get_child_arguments()
while True:
p = subprocess.run(args, env=new_environ, close_fds=False)
if p.returncode != 3:
return p.returncode
run
함수에서 use_reloader
옵션을 추가한 경우에 처음 커멘드와 동일한 커멘트로 실행한 subprocess를 하나 더 생성함을 알 수 있다. 처음 runserver 가 실행되자마자 메인 프로세스가 생성되고 실제 어플리케이션이 호출되는 서버는 서브 프로세스로 실행된다. 메인 프로세스에서는 파일 시스템의 변경을 감시하며 변경이 감지되면 기존 서브 프로세스를 종료하고 새로운 서브 프로세스를 실행한다. 위의 run 함수를 보면 내부적으로 self.inner_run
메소드를 실행하여 서버를 실행한다. inner_run
메소드에는 굉장히 중요한 개념들이 정의되어있다. 간략화한 코드를 한번 보자.
def inner_run(self, *args, **options):
# ...
self.check_migrations()
# ...
handler = self.get_handler(*args, **options)
run(
self.addr,
int(self.port),
handler,
ipv6=self.use_ipv6,
threading=threading,
on_bind=self.on_bind,
server_cls=self.server_cls,
)
inner_run
메소드를 보면 먼저 migration 체크를 하는데 migration 파일과 실제 db의 migration을 비교하여 warning 메세지를 띄워준다. 근데 여기서 중요한건 그 아래 코드인데, run
의 인자 중 addr
는 WSGI Server의 주소이고, handler
는 WSGI application를 의미한다. get_handler
메소드를 타고 들어가다보면 우리가 잘아는 get_wsgi_application
함수의 결과값을 반환하고 이 함수는 WSGIHandler
인스턴스를 반환한다. WSGIHandler
인스턴스를 호출하면 여러 미들웨어와 뷰를 거친 response가 반환된다. 즉, 위 코드에서 run
함수 인자 값으로 들어가는 handler
가 호출될 때마다 여러 미들웨어를 거친 response가 반환되는 것이다. WSGIHandler
클래스의 구조는 아래 코드를 참고하자.
# django > core > handlers > base.py
class BaseHandler:
_view_middleware = None
_template_response_middleware = None
_exception_middleware = None
_middleware_chain = None
def load_middleware(self, is_async=False):
# ...
handler = convert_exception_to_response(self._get_response)
for middleware_path in reversed(settings.MIDDLEWARE):
mw_instance = get_middleware(middleware_path)
handler = convert_exception_to_response(mw_instance)
self._middleware_chain = handler
def _get_response(request):
# url path 와 일치하는 view function 가져오기
callback, callback_args, callback_kwargs = self.resolve_request(request)
def get_response(self, request):
"""Return an HttpResponse object for the given HttpRequest."""
# Setup default url resolver for this thread
set_urlconf(settings.ROOT_URLCONF)
# call _middleware_chain
response = self._middleware_chain(request)
# ....
return response
# django > core > handlers > 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):
# ...
request = self.request_class(environ)
response = self.get_response(request)
# ...
return response
CGI, WSGI, WAS 란?
여기서 WSGI에 대해 잠깐 짚고가자. WSGI가 나오기전 CGI(Common Gateway Inferface)가 존재했다. 이는 웹서버와 웹 어플리케이션(쉽게 그냥 디스크에 존재하는 실행파일이라고 생각하자) 간의 상호작용을 정의하는 프로토콜로, 웹서버의 특정 디렉토리에 다양한 언어로 정의된 스크립트 파일을 저장하고 http 요청마다 특정 스크립트를 실행했다. 특정 스크립트가 실행될 때 별도의 프로세스로 실행되기 때문에 성능 이슈가 있었다.
그래서 파이썬 진영에서 등장한 것이 WSGI(Web Server Gateway Interface)이다. 마찬가지로 웹서버와 웹 어플리케이션 사이의 인터페이스를 정의했다. CGI와 다르게 어플리케이션을 실행할 때마다 프로세스를 생성할 필요가 없다. WSGI는 크게 2개의 구성요소가 존재한다. 하나는 WSGI Server이고, 다른 하나는 WSGI Application 이다. WSGI Server는 HTTP 요청을 받고 특정 WSGI application를 실행한 뒤 응답을 주는 서버다. WSGI Server의 대표적인 구현체로 Gunicorn, uWSGI, Daphne 등이 있다. 그리고 WSGI Application은 python으로 작성된 웹 어플리케이션으로 WSGI Server에 의해 호출되며, django에서 정의된 어플리케이션은 모두 WSGI Application 의 일종이다.
그리고 WAS(Web Application Server)를 보자. WSGI는 웹 서버와 웹 어플리케이션 사이의 통신을 표준화하는 인터페이스이고, WAS(Web Application Server)는 웹 어플리케이션의 실행하는 서버로 WAS를 WSGI Server와 WSGI Application의 구현체를 묶은 개념으로 이해해도 될 것 같다. 이 WAS를 구현하기 위한 대표적인 python framework가 Flask나 Django이다.
다시 django 코드로 돌아가서 django > core > servers > basehttp.py
를 보면 크게 WSGIServer
와 ServerHandler
클래스가 구현되어 있는걸 볼 수 있다. 근데 실제로 runserver에서 사용하는 hander는 django > core > handers > wsgi.py
에서 새롭게 구현한 WSGIHandler
클래스이다.
Django에서 요청이 처리되는 과정
WSGI Server는 새로운 요청이 올 때마다 기본적으로 새로운 스레드를 생성하여 처리하는 멀티스레드 방식으로 요청을 처리한다. 이때 요청 하나당 무조건 하나의 스레드를 생성하는 것이 아니라, 이미 존재하는 스레드를 재사용하기도 한다. 10개의 요청을 보냈을 때 실제로 8개의 스레드가 생성되었다.
각 스레드에 요청이 도달했을 때 가장 먼저 호출되는 부분이 아래의 WSGIRequestHandler
클래스이다. 각 요청마다 인스턴스가 생성되고 handle
메소드를 호출한다. handle
메소드에서 가장 핵심적인 로직이 아래 wsgiref 패키지에서 정의된 BaseHandler
클래스의 run
메소드를 호출하는 부분이다. 여기서 잠깐, 아래의 wsgiref > handlers.py
에 구현된 BaseHandler
클래스와 위의 django > core > handlers > base.py
에 구현된 BaseHandler
클래스는 이름이 같고 모두 WSGI 요청을 처리하기 위해 사용되지만 몇가지 차이점이 존재한다. wsgiref 패키지 내에서 정의된 BaseHandler
클래스는 WSGI의 가장 기본적인 요청, 응답처리 흐름을 구현했고 django 내의 BaseHandler
클래스는 django 미들웨어와 뷰를 찾아 호출하고 요청 중 발생하는 예외를 잡는 등의 django의 application 호출 새롭게 구현한 handler 클래스이다. wsgiref 패키지의 BaseHandler.run
메소드의 application 인자로 django의 BaseHandler
클래스를 상속하여 구현된 WSGIHander
의 인스턴스가 들어간다.
# django > core > servers > basehttp.py
class WSGIRequestHandler(simple_server.WSGIRequestHandler):
def handle(self):
# ...
self.handle_one_request()
# ...
def handle_one_request(self):
# ...
handler.run(self.server.get_app())
# wsgiref > handlers.py
class BaseHandler:
"""Manage the invocation of a WSGI application"""
def run(self, application):
"""Invoke the application"""
try:
self.setup_environ()
self.result = application(self.environ, self.start_response)
self.finish_response()
except (ConnectionAbortedError, BrokenPipeError, ConnectionResetError):
# We expect the client to close the connection abruptly from time
# to time.
return
except:
try:
self.handle_error()
except:
# If we get an error handling an error, just give up already!
self.close()
raise # ...and let the actual server figure it out.
다시 돌아와서 run 메소드에서 application을 호출하는 부분이 있다. 여기서는 WSGIHander
클래스의 __call__
메소드를 호출하고 __call__
메소드는 _middleware_chain
을 호출하는 것이 핵심 로직이다. 즉 _middleware_chain
은 django의 request를 처리하기 위한 핵심 로직이 구현되어있다. Django 내의 BaseHandler.load_middleware
메소드에서 _middleware_chain
가 만들어지는데, 이는 middleware 인스턴스를 순서대로 호출하는 함수를 정의하며 핵심 로직은 다음과 같다.
- settings에 정의된 미들웨어들을 순차적으로 호출하고 최종적으로 뷰 함수에 도달한다.
- 뷰 함수에 도달하여 응답을 생성한다.
- 뷰 함수에서 생성된 응답은 미들웨어 체인을 역순으로 통과하면서 미들웨어의 후처리를 거친 후 클라이언트에게 반환된다.
이 과정을 코드로 좀더 풀어서 설명하면 위의 django > core > handlers > base.py
에 위치한 BaseHandler.load_middleware
메소드를 보면 가장 먼저 url을 resolve 하여 view를 호출하고 미들웨어 인스턴스들을 역순차적으로 호출하여 function chain을 만든다. 즉, 서버가 뜨는 시점에 load_middleware
가 호출되는데 이러한 function chain 을 미리 만들어놓는다. 그리고 모든 django의 미들웨어는 MiddlewareMixin
클래스를 상속받아 구현되는데 이해를 위해 아래의 MiddlewareMixin
클래스의 코드를 살펴보자. __call__
메소드에서 process_request
를 먼저 호출한 다음에, get_response
를 호출하게 되는데, 미들웨어 인스턴스의 process_request
를 모두 호출하고 나면 get_response
에서 뷰 함수의 로직을 처리한다. 그리고 미들웨어 인스턴스의 process_response
를 역순으로 다시 호출하여 최종적으로 클라이언트에게 응답이 반환된다. 예를들어 미들웨어 A, B, C와 뷰 V가 있다고 가정할 때, A.process_request -> A.get_response -> B.process_request -> B.get_response -> C.process_request -> C.get_response -> V -> C.process_response -> B.process_response -> A.process_response의 순서로 호출된다.
class MiddlewareMixin:
def __init__(self, get_response):
self.get_response = get_response
super().__init__()
def __call__(self, request):
if hasattr(self, "process_request"):
response = self.process_request(request)
response = response or self.get_response(request)
if hasattr(self, "process_response"):
response = self.process_response(request, response)
return response
여기서 잠깐 BaseHandler.resolve_request
메소드를 통해 url이 resolve 되는 과정에 대해서도 잠깐 보고 넘어가자. 먼저 요청된 url 경로에 해당하는 뷰 함수를 호출하기 위해 크게 다음 과정이 진행된다.
- django 서버가 띄워질 때,
settings.py
의ROOT_URLCONF
에 정의된 디렉토리 파일을 로드하여 urlconf를 정의한다. - 요청된 url 경로를 urlconf의 패턴들과 순서대로 비교해본다.
- 첫번째로 매칭된 url 패턴을 찾으면 해당 패턴에 매핑된 뷰 함수를 반환한다.
각 django app에 urlpatterns
라는 변수에 여러 path 인스턴스로 구성된 리스트를 정의한다. path 인스턴스는 route(str)와 view 인자를 입력으로 받고 URLResolver
인스턴스를 반환한다. 즉 urlpatterns
변수는 여러 URLResolver
인스턴스를 가지고 있는 리스트이다. URLResolver
class는 url과 view를 매핑하는 기능이 구현되어있고 해당 클래스의 resolve
메소드는 url 문자열을 입력으로 받아 ResolverMatch
인스턴스를 반환하는데 이는 매칭된 resolved url과 관련된 뷰 함수 객체를 비롯한 모든 속성 값들을 담고 있는 객체이다. 위에서 BaseHandler
에서 호출하는 resolve_request
는 ResolverMatch
객체를 반환한다.