Pure Python을 사용해서 Web App 구축하기 (프레임워크 없이!)
Grace Collins
Solutions Engineer · Leapcell

WSGI를 사용하여 최소한의 Python 웹 애플리케이션 구축
Python 웹 애플리케이션을 작성하고, 특히 온라인 배포를 수행한 사람들은 WSGI 프로토콜에 대해 들어본 적이 있을 것입니다. 이는 Python의 웹 서버와 웹 애플리케이션 간의 데이터 교환 인터페이스를 정의합니다. 이 설명은 다소 추상적일 수 있으므로 아래의 실제 예시를 통해 자세히 설명하겠습니다.
프로덕션 환경에서의 웹 애플리케이션 배포
Django 또는 Flask와 같은 웹 애플리케이션 프레임워크를 사용하여 웹 애플리케이션을 개발했다고 가정해 봅시다. 이러한 프레임워크의 공식 문서에서는 일반적으로 Django의 python manage.py runserver
또는 Flask의 flask --app hello run
과 같은 프레임워크의 내장 서버가 개발 단계에서 디버깅에만 적합하며 프로덕션 환경의 트래픽을 처리할 수 없다고 지적합니다. 프로덕션 환경에 배포할 때 웹 애플리케이션은 웹 서버 뒤에서 실행해야 합니다. 일반적인 웹 서버로는 Gunicorn과 uWSGI가 있습니다. 웹 서버는 웹 애플리케이션의 동시성 성능을 향상시키기 위해 프로세스 모델 및 스레드 모델과 같은 동시성 옵션을 제공합니다.
위의 간단한 시나리오에는 Gunicorn + Django, Gunicorn + Flask, uWSGI + Django 및 uWSGI + Flask의 네 가지 기술 선택 조합이 있습니다. 각 조합에서 웹 애플리케이션 프레임워크가 서로 다른 웹 서비스 적응 코드를 제공해야 하는 경우 복잡성은 $N^2$에 도달하여 명백히 비용 효율적이지 않습니다. WSGI의 존재는 웹 서버와 웹 애플리케이션 간의 인터페이스를 정의하기 위한 것이며, 프레임워크 개발자는 이 인터페이스에 대한 코딩만 수행하면 됩니다. 이렇게 하면 웹 애플리케이션 개발자는 더 자유롭게 선택할 수 있습니다. 예를 들어 동일한 Django 코드를 Gunicorn과 uWSGI 모두에서 실행할 수 있습니다.
웹 애플리케이션 프레임워크 없이
Django와 Flask가 WSGI에 어떻게 적응하는지 논의하기 전에 먼저 문제를 단순화해 보겠습니다. 웹 프레임워크의 역할은 라우팅 및 HTTP 요청 구문 분석과 같은 편리한 기능을 제공하여 웹 애플리케이션을 더 쉽고 빠르게 개발할 수 있도록 돕는 것입니다. 그러나 매우 간단한 애플리케이션의 경우 프레임워크를 사용하지 않도록 선택할 수도 있습니다.
PEP에서 정의한 WSGI 인터페이스는 매우 간단하며 웹 프레임워크를 사용할 필요가 없습니다(그리고 사용해서는 안 됩니다).
HELLO_WORLD = b"Hello world!\n" def simple_app(environ, start_response): """Simplest possible application object""" status = '200 OK' response_headers = [('Content-type', 'text/plain')] start_response(status, response_headers) return [HELLO_WORLD]
이 간단한 웹 애플리케이션은 environ
환경 변수 사전과 start_response
함수를 통해 웹 서버와 상호 작용하며 웹 서버는 올바른 매개변수가 전달되도록 합니다.
위의 코드가 app.py
로 저장되고 Gunicorn이 설치되었다고 가정합니다. 다음 명령을 사용하여 애플리케이션을 시작할 수 있습니다.
gunicorn app:simple_app
기본적으로 Gunicorn은 포트 8000에 바인딩됩니다. curl
을 사용하여 테스트 요청을 보낼 수 있습니다.
$ curl http://localhost:8000 Hello world!
보시다시피 모든 것이 예상대로 작동합니다. 동시에 이 웹 애플리케이션의 코드 로직은 매우 간단합니다. 요청 경로(예: /
, /api
등)와 요청 메서드(예: GET, POST, PUT 등)를 고려하지 않으며 항상 상태 코드 200과 Hello World!
를 응답 본문으로 반환합니다.
$ curl http://localhost:8080/not-found Hello world! $ curl -X POST http://localhost:8080 Hello world!
Hello World 그 이상
앞에서 언급했듯이 일반적인 웹 애플리케이션에는 일반적으로 여러 엔드포인트가 있으며 서로 다른 요청에 따라 다른 응답을 반환해야 합니다.
웹 서버는 요청의 모든 정보를 environ
사전에 저장하고 다른 환경 변수도 포함합니다. 모든 키 중에서 다음 세 가지에 특히 주의해야 합니다.
REQUEST_METHOD
: GET, POST 등과 같은 요청 메서드입니다.PATH_INFO
: 요청 경로입니다.wsgi.input
: 파일 객체입니다. 요청 본문에 데이터가 포함되어 있으면 이 객체를 통해 읽을 수 있습니다. 다른 키인CONTENT_LENGTH
는 요청 본문의 길이를 나타내며 일반적으로 함께 사용됩니다.
/
경로에 JSON 유형 매개변수를 수신하는 새 POST 인터페이스를 구현한다고 가정합니다. 사용자가 {"name": "xxx"}
를 전달하면 웹 애플리케이션은 Hello, xxx!
를 반환하고 GET 인터페이스는 변경되지 않고 계속해서 Hello, World!
를 반환합니다. 코드는 다음과 같습니다.
import json def simple_app(environ, start_response): request_method = environ["REQUEST_METHOD"] path_info = environ["PATH_INFO"] response_headers = [('Content-type', 'text/plain')] if path_info == '/': status = '200 OK' if request_method == 'GET': body = b'Hello world!\n' elif request_method == 'POST': request_body_size = int(environ["CONTENT_LENGTH"]) request_body = environ["wsgi.input"].read(request_body_size) payload = json.loads(request_body) name = payload.get("name", "") body = f"Hello {name}!\n".encode("utf-8") else: status = '405 Method Not Allowed' body = b'Method Not Allowed!\n' else: status = '404 NOT FOUND' body = b'Not Found!\n' start_response(status, response_headers) return [body]
요청 경로 및 요청 메서드 처리에 더하여 몇 가지 간단한 클라이언트 오류 감지도 추가했습니다. 예를 들어 /
이외의 경로에 액세스하면 404가 반환되고 GET 또는 POST 이외의 메서드로 /
에 액세스하면 405가 반환됩니다. 다음은 몇 가지 간단한 테스트입니다.
$ curl http://localhost:8080/ Hello World! $ curl -X POST http://localhost:8080/ -d '{"name": "leapcell"}' Hello leapcell! $ curl -X PUT http://localhost:8080/ Method Not Allowed! $ curl http://localhost:8080/not-found Not Found!
Flask와 더 비슷하게
웹 애플리케이션의 로직이 복잡해짐에 따라 simple_app
함수가 점점 더 길어질 것입니다. 이러한 종류의 "스파게티 코드"는 명백히 좋은 프로그래밍 방식에 부합하지 않습니다. Flask의 API를 참조하여 간단한 캡슐화를 수행할 수 있습니다.
예를 들어 함수를 호출 가능한 클래스로 변환하여 웹 애플리케이션 개발자가 WSGI 애플리케이션을 얻을 수 있도록 합니다. routes
를 사용하여 경로에서 핸들러 함수로의 모든 매핑을 저장합니다. environ
을 request
객체로 캡슐화하는 등의 작업을 수행합니다.
class MyWebFramework: def __init__(self): self.routes = {} def route(self, path): def wrapper(handler): self.routes[path] = handler return handler return wrapper def __call__(self, environ, start_response): request = self.assemble_request(environ) path_info = environ["PATH_INFO"] if path_info in self.routes: handler = self.routes[path_info] return handler(request) else: status = '404 NOT FOUND' response_headers = [('Content-type', 'text/plain')] start_response(status, response_headers) return [b'Not Found!\n'] app = MyWebFramework() @app.route("/my_endpoint") def my_endpoint_handler(request): # business logic here to handle request and assemble response response_headers = [('Content-type', 'text/plain')] status = '200 OK' body = b'Endpoint response!\n' return [body]
이러한 방식으로 MyWebFramework
부분을 웹 애플리케이션 프레임워크로 점진적으로 추상화할 수 있으며 웹 애플리케이션의 실제 비즈니스 로직은 각 핸들러 함수만 작성하면 됩니다. flask.app.Flask
의 소스 코드를 참조하면 유사한 구현 방법을 사용합니다. Flask 애플리케이션은 Flask
코어 클래스에서 파생되며 이 클래스 자체가 WSGI 애플리케이션입니다.
Django의 설계는 약간 다릅니다. 비동기 요청을 지원하기 위해 ASGI(Asynchronous Server Gateway Interface) 프로토콜을 제안하고 구현했습니다. Django 애플리케이션은 내부 함수를 통해 ASGI 애플리케이션 또는 WSGI 애플리케이션으로 변환할 수 있습니다. WSGI 부분에만 집중하면 그 원리가 이전에 소개된 내용과 유사하다는 것을 알 수 있습니다.
추천 자료
Leapcell: 최고의 서버리스 웹 호스팅
마지막으로, Python 서비스를 배포하기에 가장 적합한 플랫폼인 **Leapcell**을 추천합니다.
🚀 좋아하는 언어로 빌드
JavaScript, Python, Go 또는 Rust로 손쉽게 개발하세요.
🌍 무료로 무제한 프로젝트 배포
사용한 만큼만 지불하세요. 요청도 없고, 요금도 없습니다.
⚡ 사용한 만큼 지불, 숨겨진 비용 없음
유휴 요금 없이 원활한 확장성만 제공합니다.
🔹 트위터 팔로우: @LeapcellHQ