WSGI: Python용 서버 애플리케이션 인터페이스
게시 됨: 2022-03-111993년에 웹은 약 1,400만 명의 사용자와 100개의 웹사이트를 가진 초기 단계였습니다. 페이지는 정적이지만 이미 최신 뉴스 및 데이터와 같은 동적 콘텐츠를 생성할 필요가 있었습니다. 이에 대한 응답으로 Rob McCool과 기타 기여자들은 NCSA(National Center for Supercomputing Applications) HTTPd 웹 서버(Apache의 전신)에 CGI(Common Gateway Interface)를 구현했습니다. 이것은 별도의 애플리케이션에서 생성된 콘텐츠를 제공할 수 있는 최초의 웹 서버였습니다.
그 이후로 인터넷 사용자 수는 폭발적으로 증가했고 동적 웹사이트는 유비쿼터스화되었습니다. 새로운 언어를 처음 배우거나 코딩을 처음 배울 때 개발자는 곧 코드를 웹에 연결하는 방법에 대해 알고 싶어합니다.
웹상의 파이썬과 WSGI의 부상
CGI가 만들어진 이후로 많은 것이 바뀌었습니다. CGI 접근 방식은 요청 시마다 새로운 프로세스를 생성해야 하므로 메모리와 CPU를 낭비하므로 비실용적이 되었습니다. FastCGI](http://www.fastcgi.com/)(1996) 및 mod_python(2000)과 같은 일부 다른 저수준 접근 방식이 등장하여 Python 웹 프레임워크와 웹 서버 간에 서로 다른 인터페이스를 제공합니다. 다양한 접근 방식이 확산되면서 개발자의 프레임워크 선택이 웹 서버의 선택을 제한하게 되었고 그 반대의 경우도 마찬가지였습니다.
이 문제를 해결하기 위해 2003년 Phillip J. Eby는 PEP-0333, Python 웹 서버 게이트웨이 인터페이스(WSGI)를 제안했습니다. 아이디어는 Python 응용 프로그램과 웹 서버 간에 높은 수준의 범용 인터페이스를 제공하는 것이었습니다.
2003년에 PEP-3333은 WSGI 인터페이스를 업데이트하여 Python 3 지원을 추가했습니다. 오늘날 거의 모든 Python 프레임워크는 웹 서버와 통신하기 위한 유일한 수단은 아니지만 수단으로 WSGI를 사용합니다. 이것이 Django, Flask 및 기타 많은 인기 있는 프레임워크가 수행하는 방식입니다.
이 기사는 독자에게 WSGI가 어떻게 작동하는지 엿볼 수 있도록 하고 독자가 간단한 WSGI 애플리케이션이나 서버를 구축할 수 있도록 하기 위한 것입니다. 그러나 이것이 완전한 것은 아니며 프로덕션 준비 서버 또는 응용 프로그램을 구현하려는 개발자는 WSGI 사양을 보다 철저하게 조사해야 합니다.
파이썬 WSGI 인터페이스
WSGI는 서버와 애플리케이션이 따라야 하는 간단한 규칙을 지정합니다. 이 전체 패턴을 검토하는 것으로 시작하겠습니다.
애플리케이션 인터페이스
Python 3.5에서 애플리케이션 인터페이스는 다음과 같습니다.
def application(environ, start_response): body = b'Hello world!\n' status = '200 OK' headers = [('Content-type', 'text/plain')] start_response(status, headers) return [body] Python 2.7에서 이 인터페이스는 크게 다르지 않습니다. 유일한 변경 사항은 본문이 bytes 1 대신 str 개체로 표시된다는 것입니다.
이 경우 함수를 사용했지만 모든 호출 가능 항목이 수행합니다. 여기에서 응용 프로그램 개체에 대한 규칙은 다음과 같습니다.
-
environ및start_response매개변수를 사용하여 호출 가능해야 합니다. - 본문을 보내기 전에
start_response콜백을 호출해야 합니다. - 문서 본문의 조각으로 iterable을 반환해야 합니다.
이러한 규칙을 충족하고 동일한 효과를 생성하는 객체의 또 다른 예는 다음과 같습니다.
class Application: def __init__(self, environ, start_response): self.environ = environ self.start_response = start_response def __iter__(self): body = b'Hello world!\n' status = '200 OK' headers = [('Content-type', 'text/plain')] self.start_response(status, headers) yield body서버 인터페이스
WSGI 서버는 다음과 같이 이 애플리케이션과 인터페이스할 수 있습니다.
def write(chunk): '''Write data back to client''' ... def send_status(status): '''Send HTTP status code''' ... def send_headers(headers): '''Send HTTP headers''' ... def start_response(status, headers): '''WSGI start_response callable''' send_status(status) send_headers(headers) return write # Make request to application response = application(environ, start_response) try: for chunk in response: write(chunk) finally: if hasattr(response, 'close'): response.close() 눈치채셨겠지만, start_response 콜러블은 애플리케이션이 클라이언트로 데이터를 다시 보내는 데 사용할 수 있는 write 콜러블을 반환했지만 우리의 애플리케이션 코드 예제에서는 사용하지 않았습니다. 이 write 인터페이스는 더 이상 사용되지 않으며 지금은 무시할 수 있습니다. 이 기사의 뒷부분에서 간략하게 설명합니다.
서버 책임의 또 다른 특징은 응답 반복기에 선택적 close 메서드가 있는 경우 이를 호출하는 것입니다. 여기 Graham Dumpleton의 기사에서 지적했듯이, 이는 WSGI에서 종종 간과되는 기능입니다. 존재하는 경우 이 메서드를 호출하면 애플리케이션이 여전히 보유할 수 있는 리소스를 해제할 수 있습니다.
Application Callable의 environ 인수
environ 매개변수는 사전 개체여야 합니다. CGI와 거의 동일한 방식으로 요청 및 서버 정보를 응용 프로그램에 전달하는 데 사용됩니다. 사실, 모든 CGI 환경 변수는 WSGI에서 유효하며 서버는 애플리케이션에 적용되는 모든 것을 전달해야 합니다.
전달할 수 있는 선택적 키가 많이 있지만 몇 가지는 필수입니다. 다음 GET 요청을 예로 들어 보겠습니다.
$ curl 'http://localhost:8000/auth?user=obiwan&token=123'다음은 서버 가 제공해야 하는 키와 취해야 하는 값입니다.
| 열쇠 | 값 | 코멘트 |
|---|---|---|
REQUEST_METHOD | "GET" | |
SCRIPT_NAME | "" | 서버 설정에 따라 다름 |
PATH_INFO | "/auth" | |
QUERY_STRING | "token=123" | |
CONTENT_TYPE | "" | |
CONTENT_LENGTH | "" | |
SERVER_NAME | "127.0.0.1" | 서버 설정에 따라 다름 |
SERVER_PORT | "8000" | |
SERVER_PROTOCOL | "HTTP/1.1" | |
HTTP_(...) | 클라이언트 제공 HTTP 헤더 | |
wsgi.version | (1, 0) | WSGI 버전의 튜플 |
wsgi.url_scheme | "http" | |
wsgi.input | 파일류 객체 | |
wsgi.errors | 파일류 객체 | |
wsgi.multithread | False | 서버가 다중 스레드인 경우 True |
wsgi.multiprocess | False | 서버가 여러 프로세스를 실행하는 경우 True |
wsgi.run_once | False | 서버가 이 스크립트가 한 번만 실행될 것으로 예상하는 경우(예: CGI 환경에서) True |
이 규칙의 예외는 이러한 키 중 하나가 비어 있는 경우(위 표의 CONTENT_TYPE 과 같이) 사전에서 생략할 수 있으며 빈 문자열에 해당하는 것으로 간주됩니다.
wsgi.input 및 wsgi.errors
대부분의 environ 키는 간단하지만 그 중 두 가지는 클라이언트의 요청 본문이 포함된 스트림을 포함해야 하는 wsgi.input 과 애플리케이션이 발생한 오류를 보고하는 wsgi.errors 입니다. 응용 프로그램에서 wsgi.errors 로 전송된 오류는 일반적으로 서버 오류 로그로 전송됩니다.
이 두 키는 파일과 유사한 객체를 포함해야 합니다. 즉, 파이썬에서 파일이나 소켓을 열 때 얻는 객체와 마찬가지로 스트림으로 읽거나 쓸 인터페이스를 제공하는 객체입니다. 처음에는 까다로워 보일 수 있지만 다행히 Python은 이를 처리할 수 있는 좋은 도구를 제공합니다.

먼저, 어떤 종류의 스트림에 대해 이야기하고 있습니까? WSGI 정의에 따라 wsgi.input 및 wsgi.errors 는 Python 3의 bytes 열 개체 및 Python 2의 str 개체를 처리해야 합니다. 두 경우 모두 WSGI를 통해 데이터를 전달하거나 가져오기 위해 메모리 내 버퍼를 사용하려는 경우 인터페이스에서 io.BytesIO 클래스를 사용할 수 있습니다.
예를 들어 WSGI 서버를 작성하는 경우 다음과 같이 애플리케이션에 요청 본문을 제공할 수 있습니다.
- 파이썬 2.7의 경우
import io ... request_data = 'some request body' environ['wsgi.input'] = io.BytesIO(request_data)- 파이썬 3.5의 경우
import io ... request_data = 'some request body'.encode('utf-8') # bytes object environ['wsgi.input'] = io.BytesIO(request_data)애플리케이션 측에서 수신한 스트림 입력을 문자열로 바꾸려면 다음과 같이 작성하고 싶습니다.
- 파이썬 2.7의 경우
readstr = environ['wsgi.input'].read() # returns str object- 파이썬 3.5의 경우
readbytes = environ['wsgi.input'].read() # returns bytes object readstr = readbytes.decode('utf-8') # returns str object wsgi.errors 스트림은 서버에 애플리케이션 오류를 보고하는 데 사용해야 하며 줄은 \n 으로 끝나야 합니다. 웹 서버는 시스템에 따라 다른 줄 끝으로 변환을 처리해야 합니다.
애플리케이션 호출 가능의 start_response 인수
start_response 인수는 두 개의 필수 인수, 즉 status 및 headers 와 하나의 선택적 인수인 exc_info 가 있는 호출 가능이어야 합니다. 본문의 일부가 웹 서버로 다시 보내지기 전에 응용 프로그램에서 호출해야 합니다.
이 기사의 시작 부분에 있는 첫 번째 응용 프로그램 예제에서는 응답 본문을 목록으로 반환했으므로 목록이 반복되는 시기를 제어할 수 없습니다. 이 때문에 목록을 반환하기 전에 start_response 를 호출해야 했습니다.
두 번째 응답에서는 응답 본문의 첫 번째(이 경우 유일한) 부분을 생성하기 직전에 start_response 를 호출했습니다. 어느 쪽이든 WSGI 사양 내에서 유효합니다.
웹 서버 측에서 start_response 를 호출하면 실제로 헤더를 클라이언트로 보내지 않아야 하지만 클라이언트로 다시 보낼 응답 본문에 비어 있지 않은 바이트열이 하나 이상 있을 때까지 이를 지연시킵니다. 이 아키텍처를 사용하면 응용 프로그램 실행의 가장 마지막 가능한 순간까지 오류를 올바르게 보고할 수 있습니다.
start_response 의 status 인수
start_response 콜백에 전달되는 status 인수는 공백 하나로 구분된 HTTP 상태 코드 및 설명으로 구성된 문자열이어야 합니다. 유효한 예는 '200 OK' 또는 '404 Not Found' 입니다.
headers start_response 의 인수
start_response 콜백에 전달된 headers 인수는 tuple 의 Python list 이어야 하며, 각 튜플은 (header_name, header_value) 로 구성됩니다. 각 헤더의 이름과 값은 모두 문자열이어야 합니다(Python 버전에 관계없이). 이것은 WSGI 사양에서 실제로 요구되기 때문에 유형이 중요한 드문 예입니다.
다음은 header 인수가 어떻게 생겼는지에 대한 유효한 예입니다.
response_body = json.dumps(data).encode('utf-8') headers = [('Content-Type', 'application/json'), ('Content-Length', str(len(response_body))]HTTP 헤더는 대소문자를 구분하지 않으며 WSGI 호환 웹 서버를 작성하는 경우 이러한 헤더를 확인할 때 주의해야 합니다. 또한 애플리케이션에서 제공하는 헤더 목록이 완전하지 않아야 합니다. 애플리케이션에서 제공하지 않은 헤더를 채우고 응답을 클라이언트로 다시 보내기 전에 필요한 모든 HTTP 헤더가 존재하는지 확인하는 것은 서버의 책임입니다.
exc_info start_response 의 인수
start_response 콜백은 오류 처리에 사용되는 세 번째 인수 exc_info 를 지원해야 합니다. 이 인수의 올바른 사용법과 구현은 프로덕션 웹 서버 및 애플리케이션에서 가장 중요하지만 이 기사의 범위를 벗어납니다.
이에 대한 추가 정보는 여기 WSGI 사양에서 얻을 수 있습니다.
start_response 반환 값 – write 콜백
이전 버전과의 호환성을 위해 WSGI를 구현하는 웹 서버는 write 콜러블을 반환해야 합니다. 이 콜백을 통해 애플리케이션은 반복자를 통해 서버에 전달하는 대신 본문 응답 데이터를 클라이언트에 직접 다시 쓸 수 있습니다.
존재에도 불구하고 이것은 더 이상 사용되지 않는 인터페이스이며 새 응용 프로그램에서는 사용을 자제해야 합니다.
응답 본문 생성
WSGI를 구현하는 응용 프로그램은 반복 가능한 개체를 반환하여 응답 본문을 생성해야 합니다. 대부분의 응용 프로그램에서 응답 본문은 그리 크지 않으며 서버 메모리에 쉽게 맞습니다. 이 경우 가장 효율적인 전송 방법은 요소가 하나인 iterable을 사용하여 한꺼번에 보내는 것입니다. 전체 본문을 메모리에 로드하는 것이 불가능한 특별한 경우 애플리케이션은 이 반복 가능한 인터페이스를 통해 부분적으로 이를 반환할 수 있습니다.
Python 2와 Python 3의 WSGI 사이에는 약간의 차이점만 있습니다. Python 3에서 응답 본문은 bytes 객체로 표시됩니다. Python 2에서 이에 대한 올바른 유형은 str 입니다.
UTF-8 문자열을 bytes 또는 str 로 변환하는 것은 쉬운 작업입니다.
- 파이썬 3.5:
body = 'unicode stuff'.encode('utf-8')- 파이썬 2.7:
body = u'unicode stuff'.encode('utf-8')Python 2의 유니코드 및 바이트열 처리에 대해 자세히 알아보려면 YouTube에 좋은 자습서가 있습니다.
WSGI를 구현하는 웹 서버는 위에서 설명한 것처럼 이전 버전과의 호환성을 위해 write 콜백도 지원해야 합니다.
웹 서버 없이 애플리케이션 테스트
이 간단한 인터페이스를 이해하면 실제로 서버를 시작할 필요 없이 응용 프로그램을 테스트하는 스크립트를 쉽게 만들 수 있습니다.
다음과 같은 작은 스크립트를 예로 들어 보겠습니다.
from io import BytesIO def get(app, path = '/', query = ''): response_status = [] response_headers = [] def start_response(status, headers): status = status.split(' ', 1) response_status.append((int(status[0]), status[1])) response_headers.append(dict(headers)) environ = { 'HTTP_ACCEPT': '*/*', 'HTTP_HOST': '127.0.0.1:8000', 'HTTP_USER_AGENT': 'TestAgent/1.0', 'PATH_INFO': path, 'QUERY_STRING': query, 'REQUEST_METHOD': 'GET', 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '8000', 'SERVER_PROTOCOL': 'HTTP/1.1', 'SERVER_SOFTWARE': 'TestServer/1.0', 'wsgi.errors': BytesIO(b''), 'wsgi.input': BytesIO(b''), 'wsgi.multiprocess': False, 'wsgi.multithread': False, 'wsgi.run_once': False, 'wsgi.url_scheme': 'http', 'wsgi.version': (1, 0), } response_body = app(environ, start_response) merged_body = ''.join((x.decode('utf-8') for x in response_body)) if hasattr(response_body, 'close'): response_body.close() return {'status': response_status[0], 'headers': response_headers[0], 'body': merged_body} 이러한 방식으로, 예를 들어 일부 테스트 데이터와 모의 모듈을 앱으로 초기화하고 GET 호출을 수행하여 그에 따라 응답하는지 테스트할 수 있습니다. 실제 웹 서버는 아니지만 애플리케이션에 start_response 콜백을 제공하고 사전에 환경 변수를 제공하여 비슷한 방식으로 앱과 인터페이스한다는 것을 알 수 있습니다. 요청이 끝나면 응답 본문 반복자를 사용하고 모든 내용이 포함된 문자열을 반환합니다. 다른 유형의 HTTP 요청에 대해 유사한 메서드(또는 일반적인 메서드)를 만들 수 있습니다.
마무리
이 기사에서는 WSGI가 파일 업로드를 처리하는 방법에 대해 접근하지 않았습니다. 이는 입문 기사에 적합하지 않은 "고급" 기능으로 간주될 수 있기 때문입니다. 그것에 대해 더 알고 싶다면 파일 처리를 참조하는 PEP-3333 섹션을 살펴보십시오.
이 기사가 Python이 웹 서버와 통신하는 방법을 더 잘 이해하고 개발자가 이 인터페이스를 흥미롭고 창의적인 방식으로 사용할 수 있도록 하는 데 도움이 되기를 바랍니다.
감사의 말
이 기사를 작성하는 데 도움을 준 편집자 Nick McCrea에게 감사드립니다. 그의 작업으로 인해 원본 텍스트가 훨씬 명확해졌고 몇 가지 오류가 수정되지 않았습니다.
