💻 프로그래밍/Django

Django App은 어떻게 실행되는 걸까?

피트웨어 제이 (FitwareJay) 2022. 8. 11. 09:38

안녕하세요! 개발자 Jay 입니다! 오늘은 Django App이 어떻게 실행되는지에 대해 알아보려고 합니다!

 

처음 개발을 시작할때는 runserver 명령어를 치면 "알아서 되는구나"라고 생각했고 라이브 배포시 "uwsgi, gunicorn 같은 wsgi들이 django app과 연결되니까 실행이 되겠지" 정도로 약간 추상적으로 알았던 것 같아요.

 

그래서 오늘은 한번 전반적으로 디버깅을 해보면서 runserver 명령어가 실행되면 어떻게 django app 실행까지 되는지 확인해보겠습니다.

(본 포스팅은 Django 3.2.4 기준으로 작성되었습니다!)

 

 

🏃‍♂️ runserver 


먼저 우리는 django app을 실행시킬때 ./manage.py runserver 명령어를 사용합니다. 명령어 자체를 그냥 본다면

manage.py 파일을 실행하고 인자로 runserver를 넘긴다

 

정도로 이해할 수 있겠네요.

 

manage.py 로직

manage.py 파일 내부에는 이렇게 생긴 로직들이 있습니다. improt 체크를 하고 execute_from_command_line 함수를 실행하네요. sys.argv 변수에는 명령어가 담겨 있겠죠?

 

execute_from_command_line 함수

execute_from_command_line() 내부 로직을 보면 argv는 runserver 뿐만 아니라 실행되는 명령어 자체가 모두 들어가 있네요. 여기도 뭐 특별한 건 없고 ManagementUtility 인스턴스를 생성하고 execute()를 호출합니다.

 

execute 메소드, fetch_command 메소드

execute() 내부 로직을 실행하고 마지막에 fetch_command()run_from_argv()를 실행합니다.

 

fetch_command()를 통해서 리턴받는 클래스는 BaseCommand

fetch_comamnd()에서는 현재 실행한 django command에 대한 클래스를 찾아서 리턴해주는 것 같네요.

 

run_from_argv 메소드

결론적으로 command에 맞는 run_from_argv()를 실행하고 내부적으로 또 excute()를 실행 합니다.

 

Command 클래스와 execute() 메소드

Command(django.contrib.staticfiles.management.commands.runserver.Command)의 execute() 내부에서는  options 파라미터에 따라 뭔가 해주고 있네요. 로직을 쭉 따라가면 handle() 메서드를 실행합니다.

 

handle()

handle()에서는 debug모드에 따라 ALLOWED_HOSTS를 체크하는 로직도 있고 ipv6 관련 체크하는 로직도 보입니다. 마지막으로 run() 메서드를 실행!

 

Command.run()
run_with_reloader()

run()을 실행하면 reloader 옵션에 따라 분기 처리하여 실행합니다. 기본적으로 django는 아무런 옵션이 없을 때 reload를 강제하고 있습니다. 강제라는 게 좀 어색하긴 한데 production 레벨에서 앱이 죽으면 안 되니 자동으로 앱을 리로드 하도록 권장하는 옵션인 것 같습니다.

(django runeserver 명령어에 option으로 --noreload를 사용하면 바로 inner_run()을 실행합니다.)

 

여하튼, use_reloaderTrue이기 때문에 run_with_reloader()를 실행하고 파라미터로 inner_run()을 넘겨주네요. run_with_reloader() 내부 로직을 보면 DJANGO_AUTORELOAD_ENV값에 따라서 분기 처리되는 걸 볼 수 있습니다

 

DJANGO_AUTORELOAD_ENV가 true인 경우에는 start_django()를 실행하네요. 드디어 django 앱을 뭔가 실행할 것 같은 함수를 만났습니다.

 

DJANGO_AUTORELOAD_ENV를 true로 세팅

DJANGO_AUTORELOAD_ENV값은 말 그대로 자동으로 reload 할 거냐는 환경변수입니다. 만약 이 값이 False이면 restart_with_reloader()를 실행하며 앱이 재실행됩니다. django는 특별한 옵션이 없는 경우 두 개의 프로세스를 실행한다고 합니다. (reload와 django app 이렇게 두 개!)

 

 

🚀 django app 실행 


start_django() 내부로직

start_django() 내부 로직을 보면 thread를 하나 생성하네요. inner_run()을 실행하는 thread인 것 같습니다!

그리고 reloader.run으로 django_main_trhread를 실행해줍니다.

 

inner_run()

inner_run() 내부에서는 마이그레이션 체크, quit_command 등등 실행을 하고 handle를 가져온 뒤 run 시킵니다. try-except로직을 보면 get_handler()로 hander를 가져오고 있습니다.

 

이 부분이 이번 포스팅의 핵심이라고 볼 수 있습니다!

 

(좌)Command 클래스의 get_handler() (우) 상속받은 클래스의 get_handler()

get_handler()를 호출하면 super()로 부모 클래스의 get_handler()를 먼저 호출합니다. 

부모 클래스(RunserverCommand)의 get_hander()에서  get_internal_wsgi_application()를 한 번 더 호출합니다.

 

settings의 WSGI_APPLICATION을 가져와 wsgi application을 return

결국 핵심은 wsgi application을 가져와서 실행한다는 것인데 우리가 사용하는 wsgi application은 django에서 제공하는 wsgi application을 사용합니다.

 

settings에 설정되어있는 WSGI_APPLICATION

django가 제공하는 wsgi application이 그럼 어디에 있느냐?! 

 

wsgi.py

바로 여기 wsgi.py입니다! django project를 만들면 기본적으로 생성되는 이 파일이 바로 django application을 실행하는 django wsgi application입니다.

https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/

 

How to deploy with WSGI | Django documentation | Django

Django The web framework for perfectionists with deadlines. Overview Download Documentation News Community Code Issues About ♥ Donate

docs.djangoproject.com

 

debug모드에서는 StaticFilesHndler를 리턴

debug모드에서는 조금 다른 게 바로 handler를 리턴하는 게 아니라  WSGIHandler를 상속받은 StaticFilesHanlder를 리턴합니다. debug모드에서 static file directory 세팅 관련된 미들웨어라고 보시면 될 것 같아요!

 

이제 django app run!

앞에서 이것저것 세팅을 하고 django application을 실행하기 위한 handler까지 가져온 후에는 run()으로 django application을 실행하게 됩니다.

 

django app run

run() 내부 로직은 httpd라는 WSGIServer 인스턴스를 생성한 뒤 wsgi_handler를 set_app()을 통해 주입합니다. 

 

socket bind 와 activate 실행

이때 WSGIServer 인스턴스는 socket을 생성하고 binding합니다. WSGIServer의 부모 클래스를 타고 들어가다 보면 TCPServer 클래스인 걸 확인할 수 있습니다.

 

django app이 계속 실행되게 하는 serve_forever()

serve_forever()에서는 django app이 계속 실행되게 while문을 돌고 있습니다. while문을 돌면서 request가 들어오는지 체크를 하고 ready라는 변수에 값이 있는 경우 request를 처리하는 것처럼 보이네요!!

 

serve_forever()는 "Handle one request at a time until shutdown" 라고 하네요. shutdown 하기 전까지는 time마다 하나의 request를 처리한다 뭐 그런 의미 같습니다. time은 poll_interval 파라미터로 정의하는 것 같네요. (default 0.5초)

 

 

🤝 클라이언트 request 요청 처리 


뭔지 잘 모르겠지만 event를 읽어서 처리하는 것 같음

django app은 이제 실행되고 request를 어떤 식으로 받고 처리하는지 궁금해졌습니다. 위 이미지에서 보면 ServerSelector()를 통해 EVENT_READ라는 걸 등록하고 while문 내부에서 계속 select()로 무언갈 가져옵니다.(아마도 서버로 오는 request겠죠?)

 

일단은 register 내부 로직을 순서대로 따라가 보겠습니다.

 

_PollLikeSelector.register()

뭔가 이벤트를 등록하는 로직 같습니다. 내부에 부모 클래스의 register()를 한 번 더 호출합니다. register()에서는 WSGIServer read event를 등록하는 걸로 보입니다. 그리고 그 key를 리턴해주는 것 같고요! 이벤트의 경우 비트 마스킹(bit masking)으로 처리하네요!

 

이벤트가 등록된 후에는 select()를 호출하여 ready값을 확인합니다.

 

select()에서 이벤트 체크, 위 화면에서는 현재 포스트맨으로 request를 보내서 이벤트가 잡힌 상태이다.

select() 내부에서는 현재 이벤트가 있는지 체크하고 등록된 이벤트가 있는 경우 key값을 체크합니다.

 

여기서 fd는 file descriptoer의 약자입니다.

file descriptor까지 가게 되면 너무 길어지는 관련 내용 블로그 글을 첨부하겠습니다.

https://twofootdog.tistory.com/51

 

파일 디스크립터(File Descriptor) 란 무엇인가?

1. 개념 파일 디스크립터(File Descriptor)란 리눅스 혹은 유닉스 계열의 시스템에서 프로세스(process)가 파일(file)을 다룰 때 사용하는 개념으로, 프로세스에서 특정 파일에 접근할 때 사용하는 추상

twofootdog.tistory.com

 

ready(이벤트)가 존재하는 경우

select()에서 리턴 받은 ready가 none이 아닌 경우에 _handl_request_noblock()을 호출합니다.

 

_handl_request_noblock()

 

이어서 _handl_request_noblock() 내부에서는 다시 get_request()를 호출합니다! 

 

socket 커넥션을 허용

쭈욱 훑어보면 get_request()를 통해서 request, client_address를 받고 proscees_request()로 넘겨줍니다. 맥락상 request에 대한 처리를 이 메서드를 통해 처리하는 것 같네요. 

 

클라이언트에서 request요청시 쓰레드가 생기는걸 볼 수 있음

get_request()에서는  request요청 시 TCP socket 연결을 처리해주는 것 같습니다. 이때 소켓 컨넥션에 대한 스레드가 계속 생깁니다! 다만 http 요청시 헤더의 connection: keep-alive를 제거하면 매번 새로운 스레드로 생기고 반대의 경우 쓰레드가 유지되는 걸 볼 수 있습니다.

 

정확한 건 좀 더 공부해봐야겠지만 connection: keep-alive 옵션에 따라 소켓 커넥션이 유지되고 소켓 커넥션의 유지는 스레드의 유지라고 볼 수 있을 것 같습니다. 이 스레드는 WSGIRequstHandler와 연관 있는 스레드인 것 같습니다. (소켓 커넥션 스레드 관련 내용은 추후에 좀 더 스터디 후 정리해보겠습니다)

 

proscees_request()

proscees_request()에서는 전달받은 request를 trhead로 처리를 하네요! 오호!

 

process_request_thread()

thread로 실행되는 메서드는 process_request_thread() 입니다. 이 메서드는 finish_request()를 호출합니다.

되게 모듈화를 많이 해놨네요 ㅋㅋ 이쯤 되니 조금 지치기는 하네요 ㅋㅋㅋ

 

finish_request()
BaseRequestHandeler

finish_request()에서는 RequestHandlerClass를 초기화하는데 이때 __init__()에서 handle()을 호출하네요. (handle이라는 메서드를 진짜 많이 본 것 같네요 ㅋㅋ)

 

WSGIRequestHandler.handle()
WSGIRequestHandler.handle_one_reuqest()

handle_one_request()에서는 WSGIHandler를 상속받은(Debug모드라서) StaticFilesHandler를 파라미터로 넘기면 run()을 실행시킵니다.

 

WSGIRequestHandler.run()

run()에서는 application 파라미터로 넘긴 StaticFielsHandler를 실행합니다.

 

StaticFielsHandler

결론적으로 StaticFielsHandler가 호출되면 self.application을 호출하게 되는데 self.application은 2번 과정 마지막쯤에 get_handler() 내부 로직에서 StaticFielsHandler의 생성자로 넘겨주었던 WSGIHandler입니다!

 

결국 우리는 클라이언트에서 http요청을 한 뒤 이 WSGIHandler를 호출하기까지의 과정을 따라간 것입니다 ㅋㅋㅋ

 

앞의 모든 과정을 거쳐서 실질적으로 request를 처리하는 WSGIhandler

WSGIHandler.__call__() 에서는 request의 response를 처리하고 status, header 등을 세팅해줍니다. WSGIhandler.__call__() 내부에서 response를 처리해주는 get_response()가 궁금하기 때문에!! 이 부분도 한번 자세히 알아보겠습니다!

 

 

♻️ WSGIHander에서 response를 리턴하는 과정 


WSGIHandler.__call__() 에서 가장 중요하다고 생각되는 request_class, get_response 이 두 개의 메서드에 대해 순차적으로 알아보겠습니다.

 

WSGIRequest

request_classWSGIClass 이며, 생성자 호출 시 request의 META정보, path등에 초기값들을 넣어줍니다. (생각보다 별게 없네요;) 그리고 이 request를 get_response()의 파라미터로 넘기게 됩니다.

 

get_response()

 

get_response()에서는 가장 중요한 부분이 _middleware_chain() 호출입니다. 메서드 이름에서 알 수 있다 시피 체이닝 된 미들웨어를 호출하는 메서드입니다. django settings에 미들웨어 설정하는 부분이 있는데 이때 추가된 미들웨어가 여기서 순차적으로 실행이 됩니다. 

 

request는 각각의 미들웨어를 순차적으로 돌면서 가공이 되거나 미들웨어 내부 로직을 실행하는 데 사용될 것입니다.

 

get_response 값을 보면 middlware인걸 확인 할 수 있습니다

_middleware_chain()을 호출하게 되면 convert_exception_to_reponse()가 실행이 됩니다. 그리고 다음 스텝으로 넘어가면 get_reponse() 즉, 미들웨어가 계속 바뀌면서 실행이 됩니다. 왜 그럴까요?

 

runserver를 실행할 때  WSGIhandler 생성자에서  load_middleware()를 실행합니다. load_middleware()는 메서드명처럼 미들웨어를 넣어주는(?) 역할을 합니다. 내부 로직을 살펴보겠습니다.

 

handler로 미들웨어를 한번 감싸고 선언하고 다시 미들웨어 인스턴스 선언시 파라미터로 주입

내부 로직을 보면 handlerconvert_exception_to_response(get_response)로 선언해주고 있습니다. for loop에서 미들웨어를 가져오고 미들웨어 인스턴스를 선언할 때 파라미터로 handler를 넣어주고 있습니다. 그리고 이 과정을 반복!

 

convert_exception_to_response() 특별한 게 아니라 미들웨어 익셉션 관련 처리를 해주기 위해 래핑 하는 함수입니다. 이 과정이 어떻게 미들웨어 체이닝이 되는지 이해가 안 될 수도 있습니다! 조금 헷갈리니 미들웨어의 구성과 예제로 확인해보겠습니다!

 

미들웨어 기본 구조

MiddlewareMixin을 보면 __init__()에서 파라미터로 get_response를 받고 있고 __call__()에서 request파라미터를 넘겨주면 get_response를 호출하고 있습니다. 모든 미들웨어는 이런 구조를 가지고 있고 그렇기 때문에 체이닝이 가능합니다!

 

위 과정으로 response를 가져온 뒤 finish_response()를 실행합니다.

 

finish_response()
socket으로 data 전송

finish_response() 내부 로직에 write()에서 socket을 통해 데이터를 보내는 역할을 합니다. 이 과정까지 마치게 되면 이제 클라이언트는 서버로부터 응답을 받게 되는 것입니다!

 

💡 middleware chaning 예시

class SessionMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
 
    def __call__(self, request):
        print(f'SessionMiddleware- {request}' )
        return self.get_response(request)
 
 
class JwtTokenMiddleware:
 
    def __init__(self, get_response):
        self.get_response = get_response
 
    def __call__(self, request):
        print(f'JwtTokenMiddleware- {request}')
        return self.get_response(request)
 
 
class BaseMiddleware:
 
    def __init__(self, get_response):
        self.get_response = get_response
 
    def __call__(self, request):
        print(f'BaseMiddleware - {request}')
        return self.get_response(request)
 
 
class Request:
 
    def __call__(self, request):
        print('Request')
        # return self.get_response()
 
 
handler = BaseMiddleware(Request())
handler = JwtTokenMiddleware(handler)
handler = SessionMiddleware(handler)
handler(Request)
 
cs

위 로직은 미들웨어를 간단하게 구현해본 예제입니다. for loop를 사용하지 않았을 뿐 load_middleware() 내부 로직을 유사하게 구현했습니다. 위 코드를 실행하면 다음과 같은 결과를 볼 수 있습니다.

 

순서대로 출력된 미들웨어 __call__

 

👋 마무리


django runserver 명령어를 통해 django가 어떻게 app을 띄우고, 그 app들에 미들웨어가 어떻게 주입되고 request를 받아서 response를 주는지 등에 대해 한번 쭉 훑어봤습니다.

 

일일이 디버깅하면서 하나씩 보다 보니 좀 과할(?) 정도의 양이 된 것 같네요! 일단 한번 더 정리해보면

 

  1.  runserver 명령어를 실행하면 django app을 띄운다.
    이때 reloader와 함께 실행하기 때문에 별도의 옵션 혹은 환경변수 설정이 없으면 앱이 두 번 실행될 수 있다.
  2.  django app이 띄워질 때 handler를 선택하게 되어있는데 django에서 제공하는 WSGIHandler를 사용한다.
    나중에 uwsgi, uvicorn 같은 wsgi를 사용할 때도 WSGIHandler와 연결되는 것임.
  3.  WSGIHandler 인스턴스가 생성될 때 TCP Scoket 관련 binding과 activation이 실행된다. 이후 미들웨어 체이닝을 실행한다.
  4.  request를 받을 준비가 된 상태에서 클라이언트로부터 http request가 온다면 해당 클라이언트와 소켓 커넥션을 맺은 후 response전달을 하기 위한 로직이 실행된다.
  5.  response는 3번에서 선행된 미들웨어 체인을 돌면서 미들웨어 로직을 처리하고 url path를 파싱(reverse)하여 관련 view를 찾아간 뒤 비즈니스 로직을 처리한다.
  6. 5번 과정에서 처리된 response는 소켓으로 전송된다.
  7. connection: keep-alive가 헤더에 있는 경우 소켓 커넥션(스레드)은 유지, 아닌 경우 connection이 close 된다.

 

어우~ 끝이없는 배움

이 과정에서 완벽히 이해 못 한 개념들을 정리해봤습니다.

  • socket 커넥션 시 생기는 스레드 관리는 어디서 누가 하는지?
  • connection: keep-alive 옵션이 있을 때, 커넥션이 유지되는 default 시간
  • TCP socket 통신에 대한 기본적인 이해
  • 파일 디스크립터란 무엇인가?

 

다음에는 위 내용들에 대해 좀 더 스터디해보고 정리해보려고 합니다. 특히 TCP socket이 web에서 굉장히 중요한 개념인데도 그동안 너무 두리뭉실하게 알 고 있었던 것 같습니다 ㅎㅎ

 

굉장히 길었던 과정이고 이걸 정리하면서도 힘들었지만 그래도 나름 django가 어떻게 돌아가는지 알 수 있었습니다! 여러분들도 한번 읽어보시고 디버깅을 하시면서 그냥 쭉 훑어만 봐도 도움이 될 것 같아요!!

 

 내가 사용하는 프레임워크가 애플리케이션을 어떻게 만들고 로직을 처리하는지 정도는 대략적으로 알면 많이 도움이 될 거라고 생각합니다! 그럼 오늘도 즐거운 코딩 하시길 바라며!! 아디오스!

 

혹시나 제가 잘못 적은 부분이 있다면 과감하게 말씀해주세요!