wsgi와 cgi의 개념과 uwsgi를 django와 연결하기

업데이트:

안녕하세요👋
이번에는 cgi, wsgi의 개념과 웹 어플리케이션 서버를 배포할 때 왜 cgi와 wsgi를 사용해야 하는지 또 nginx, apache등의 web server는 무엇이고 왜 사용해야 하는지 알아보겠습니다

어려운 내용이라 이해하고 적용하는데 많은 고난을 겪였습니다 개인적으로 이해하고 적은 내용이다보니 부족한 부분이 있을 수 있습니다.

보시는 분들에게 도움이 되었으면 좋겠습니다

🙏요약

이번 글에서는 wsgi,cgi에 대해 설명하겠습니다

  1. wsgi,cgi란?
  2. django 코드로 알아본 wsgi
  3. uwsgi를 사용하여 django 배포하기

📝wsgi,cgi란?

일반적인 정의를 알아보면

  • WSGI(Web Server Gateway Interface)는 웹 서버 소프트웨어와 파이썬으로 작성된 웹 응용 프로그램 간의 표준 인터페이스이다.
  • CGI(Common Gateway Interface)는 WWW 서버와 서버 상에서 등장하는 다른 프로그램등 HTML에서는 불가능한 인터렉티브한 요소를 홈페이지에 받아 들여 쓸 수 있다.
    쉽게 설명해 보면 cgi는 웹서버와 웹 어플리케이션 사이를 이어주는 인터페이스 역할을 하는 미들웨어 입니다.

    다시말해 웹 서버에서 프로그래밍 언어를 해석할 수 없기 때문에 우리가 작성한 python이나 java와 같은 언어를 이해하기 위해서는 미들웨어가 필요한 것 이것이 cgi입니다.

    wsgi는 cgi의 개념을 이어받아 cgi의 단점들을 보완해서 python에서 사용하기위에 만들어진 cgi의 한 종류라고 보면 되겠습니다

    cgi는 요청이 들어오면 요청마다 스레드를 생성하여 웹 어플리케이션을 동작시키게 되는데 이는 부하가 너무 크고 10000개이상의 동시 요청은 처리할 수 없게 된다고 합니다

    이를 해결하고 부하도 줄이기 위해 wsgi는 비동기 방식의 콜백함수로 이루어져 스레드를 생성하지 않고 여러 동시 요청을 처리할 수 있으므로 부하가 적습니다 따라서 요즘은 wsgi의 종류인 uwsgi나 gunicon등을 많이 사용하고 있다고 합니다.

image
위 그림이 나름 잘 표현한 웹 용어를 표현한 구조라고 생각합니다
(spring은 jvm이 깔려있는 cgi를 사용해야 하므로 tomcat이 기본)

따라서 각 프로그래밍 언어마다 web server에 제공할 수 있는 cgi종류가 다르다고 보면 되겠습니다.

tomcat 앞에 nginx같은 web server을 두는 경우가 종종 있는데 이는 로드밸런싱을 위한 목적이 대부분이고 이 외에는 큰 의미가 없다고 합니다. 개인적으로는 분산 아키텍처를 구성하는 등의 안정성등의 측면에서 필요하다고 느끼고 있습니다.

정적 파일들을 web server에서 관리하기 위해 이렇게 한다는 경우가 있다고 하는데 spring의 경우는 tomcat내에서 가능하다고 합니다.

이제 python에서 사용되는 wsgi에 대해 조금더 알아보고 굳이 runserver로 잘 돌아가는 django에서 왜 cgi를 설정해서 사용해야 하는지 알아보도록 하겠습니다

🔎django 코드를 통해 알아본 wsgi

django 프로젝트를 생성하고 항상 우리가 서버를 실행할때 하는 것은 python manage.py runserver 입니다. 이번에는 이 runserver가 어떻게 동작하는지 보겠습니다.

# project/manage.py
"""Django's command-line utility for administrative tasks."""
import os
import sys

def main():
    """Run administrative tasks."""
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.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의 코드입니다. 그냥 무작정 실행하지 말고 한번 살펴보면 결국 execute_from_command_line(sys.argv)가 실행되는 것을 알 수 있습니다.

vscode의 f12를 통해 따라가 보겠습니댜

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


이러한 코드를 볼 수 있고 execute()함수를 따라가보면

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)
...

    else:
        self.fetch_command(subcommand).run_from_argv(self.argv)        


코드가 길어서 모두 담을 수는 없고 주요 부분만 보면 runserver 입력이 들어왔을때 arg를 설정해주는 것을 볼 수 있습니다. 이후 결국 run_from_argv로 subcommand인 runserver를 실행하게 됩니다.

여기서 들어가보면 BaseCommand의 인터페이스 만 나오게되는데 이제 이 BaseCommand의 인터페이스를 상속한 runserver객체를 찾아보겠습니다. 이는 django/core/management/commands/runserver.py에 있습니다.

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

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

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


보면 아까의 BaseCommand를 상속받아 Command객체를 만들어 주었고 default로 여러 http 설정이 나와있는 것을 볼수있습니다. 자 이제 우리가 찾던 장고가 기본으로 제공하는 wsgi는 어디에 있을까요 server_cls = WSGIServer 이 문장을 주목해 보면 기본적으로 WSGIServer라는 객체를 생성해서 wsgi를 사용한다는 것을 알수있습니다. 마지막으로 WSGIServer 객체를 f12로 찾아가보겠습니다.

#basehttp.py
"""
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!
"""

import logging
import socket
import socketserver
import sys
from wsgiref import simple_server

from django.core.exceptions import ImproperlyConfigured
from django.core.handlers.wsgi import LimitedStream
from django.core.wsgi import get_wsgi_application
from django.utils.module_loading import import_string

class WSGIServer(simple_server.WSGIServer):



자 WSGIServer 객체는 simple_server라는 곳의 객체를 상속받아서 만들어 진다는 것을 알 수 있습니다. 그리고 WSGIServer객체가 정의되어있는 basehttp.py 맨위의 설명 문장을 보면 DON’T USE IT FOR PRODUCTION USE! 라고 나와있는 것을 볼 수 있습니다.

이건 wsgiref라는 것을 토대로 만들어진 simple server일 뿐이고 실제 배포시에는 사용하지 말라 라는 문장입니다. django는 기본적으로 보안적, 성능적으로 좋지 않은 기본 서버인 simple server를 제공해주는 것이고 우리가 실제 배포할 때에는 uwsgi나 gunicorn 같은 오픈 소스 wsgi를 사용해야 하는 것입니다.

여기까지 이해했다면 이제 실제 django를 uwsgi를 통해 배포하는 방법을 알아보도록 하겠습니다.

🔎uwsgi를 사용하여 django배포하기

먼저 pip를 통해 uwsgi를 설치해 줍니다 (가상환경을 사용하고 있다면 가상환경 내에 설치합니다)

pip install uwsgi


다음은 uwsgi를 django와 연결하기 위한 설정파일을 만들어 줍니다. 이부분이 상당히 헷갈려서 개인적으로도 많이 찾아보았습니다. 나중에 docker로 배포할 계획도 있다면 프로젝트 내에 설정파일을 만드는 편이 좋습니다. uwsgi.ini이라는 이름으로 설정 파일을 만들어 보겠습니다.

[uwsgi]
base = /home/ubuntu/taegyun/soma_mainserver
project = mainserver
home = %(base)/venv
# 프로젝트 위치
chdir=%(base)/%(project)
# django wsgi모듈
module=mainserver.wsgi

uid = ubuntu
gid = ubuntu

# http = :8000
socket = /tmp/uwsgi.socket
chmod-socket = 666
chown-socket = ubuntu:ubuntu

master=true
pidfile=/tmp/project-master.pid

vacuum=true
# max-requests=5000

# logger = file:/tmp/uwsgi.log
daemonize = %(base)/%(project)/uwsgi.log 


#은 주석이고 변수 뒤에 ;등으로 주석을 단 블로그들이 있는데 절대 변수 뒤에 띄어쓰기가 있더라도 다른 문자를 작성하지 않아야 합니다..;여기서 고생을 좀 했습니다
ini파일에서 상대경로는 먹히지 않으므로 /부터 시작하는 절대경로로 작성해주면 됩니다.

필수로 필요한 변수는 home,chdir,module

  • home = 프로젝트가 사용할 가상환경(가상환경을 사용하지 않는다면 python이 있는 위치를 넣어주자)
  • chdir = 프로젝트의 위치
  • module = 프로젝트위치에서 wsgi.py파일위치 경로 (.py를 빼고 경로를 .으로 구분한다)


나머지는 부가적인 옵션인데 더 많은 옵션은 uwsgi공식 페이지를 참고해 주세요!

아래는 직접 사용한 옵션입니다.

  • uid,gid - wsgi를 실행할 os user정보,user group 정보이다 자신이 평소에 사용하는 user의 이름을 넣어주자
  • master - pid를 생성할지 여부다 uwsgi가 실행되면 프로세스의 형태로 실행되는데 pid정보를 어딘가 두어야 프로세스를 종료하거나 변경할때 유용하다
  • pidfile - master가 true일 때 pid를 둘 위치이다.
  • vacuume - uwsgi프로세스를 실행할때 생성한 파일들을 종료시 삭제시킨다 -> 깔끔해진다
  • logger - 파일 형태로 로그를 남길때 사용한다
  • daemonize - logger와 마찬가지로 로그를 남기는 옵션인데 logger와 차이점은 콘솔에 뜨는 모든 텍스트를 저장한다 요청응답에 대한내용만 보고싶다면 logger를 사용하자
  • socket ,http - uwsgi를 http로 띄울지 socket으로 띄울지를 결정한다 둘다 사용해도 괜찮다(물론 많이 느려질 것이다)


http보다 socket이 요청 응답이 훨신 빠르다고 알고있습니다 나중에 nginx를 사용할 예정이라면 socket으로 서버를 띄워 nginx와 uwsgi사이의 전송 속도를 올리는 형태가 좋을 것으로 생각합니다.

uwsgi -i uwsgi.ini


자 이제 위 명령어를 uwsgi.ini파일이 있는 위치에서 실행해보면 uwsgi로 서버가 띄워지는 것을 볼 수 있고 nginx를 사용하지 않는다면 http옵션으로 :8000등으로 포트 번호를 지정해주고 실행시켜줘야 하겠습니다

이제 여러분이 열심히 만든 django 웹 어플리케이션을 uwsgi로 배포하여 사용해 보면 되겠습니다.

하지만 정적 파일관리나 많은 요청을 처리하고 ssl인증서를 통해 https를 사용하기 위해서라도 nginx를 uwsgi앞단에 붙혀 uwsgi와 연결해 주어야 합니다 이 내용은 나중에 자세하게 적도록 하겠습니다

긴글 봐주셔서 감사합니다 앞으로도 유용한 내용을 많이 공유하겠습니다 그럼 안녕 👋

댓글남기기