PEP 3333–Python Web Server Gateway Interface V1.0.1

Python Web Server Gateway Interface (WSGI) 버전 1.0.1을 정의한 문서.
PEP 333의 개정판으로, Python 3 지원을 개선하고 몇 가지 오랜 de facto 수정사항을 반영
PEP 3333은 PEP 333을 Python 3 시대에 맞게 업데이트한 버전이다.

주요 변경사항과 특징

  1. Python 3 지원:
    • 문자열 처리가 유니코드로 변경됨
    • environ 딕셔너리의 문자열은 str 타입이어야 함
    • 응답 본문은 bytes 타입이어야 함
  2. 새로운 보안 고려사항:
    • 헤더 인젝션 방지
    • 안전한 문자열 처리
  3. 미들웨어 체이닝:
    • 여러 미들웨어를 연결하여 요청/응답 처리 파이프라인 구성 가능
  4. 파일 핸들링:
    • wsgi.input과 wsgi.errors를 통한 표준화된 입출력 처리

WSGI (Web Server Gateway Interface)

웹 서버와 Python 웹 어플리케이션 또는 프레임워크 간의 표준 인터페이스를 정의한다.

목적

다양한 웹 서버와 Python 웹 어플리케이션 간의 호환성을 보장한다.

특징

  1. 호환성과 이식성:
    WSGI는 웹 서버와 Python 웹 애플리케이션/프레임워크 간의 표준 인터페이스를 제공합니다. 이를 통해 다양한 웹 서버와 프레임워크 간의 호환성이 보장되어 애플리케이션의 이식성이 크게 향상됩니다
  2. 유연성:
    개발자는 코드 변경 없이 웹 스택 구성요소를 쉽게 변경할 수 있습니다. 이는 다양한 서버와 프레임워크를 조합하여 사용할 수 있게 해줍니다
  3. 확장성:
    WSGI 서버는 수천 개의 동시 요청을 처리하고 라우팅할 수 있어 대규모 애플리케이션에 적합합니다
  4. 단순성:
    WSGI의 학습 곡선이 완만하여 쉽게 익힐 수 있으며, 복잡한 설정이나 설치 과정이 필요 없습니다
  5. 재사용 가능한 미들웨어:
    인증/인가, 캐싱, 필터링 등의 기능을 제공하는 재사용 가능한 미들웨어 컴포넌트를 활용할 수 있어 개발 시간을 단축할 수 있습니다
  6. Python 3 지원:
    PEP 3333은 Python 3에서의 문자열과 유니코드 처리에 대한 규칙을 추가하여 Python 3 환경에서의 WSGI 사용을 개선했습니다

기본 구조

WSGI 서버 (Server/Gateway)

클라이언트로부터 HTTP 요청을 받아들이고, 이를 WSGI 애플리케이션으로 전달한 후 응답을 클라이언트에 다시 전송한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import os, sys

def run_with_cgi(application):
    environ = dict(os.environ.items())  # 환경 변수 복사
    environ['wsgi.input'] = sys.stdin.buffer  # 요청 본문을 읽기 위한 입력 스트림
    environ['wsgi.errors'] = sys.stderr  # 오류 출력을 위한 스트림
    environ['wsgi.version'] = (1, 0)  # WSGI 버전
    environ['wsgi.multithread'] = False  # 멀티스레드 지원 여부
    environ['wsgi.multiprocess'] = True  # 멀티프로세스 지원 여부
    environ['wsgi.run_once'] = True  # 한 번만 실행되는지 여부
    environ['wsgi.url_scheme'] = 'http'  # URL 스키마

    headers_set = []
    headers_sent = []

    def write(data):
        # 응답 헤더와 본문을 출력하는 함수
        if not headers_set:
            raise AssertionError("write() before start_response()")
        elif not headers_sent:
            status, response_headers = headers_sent[:] = headers_set
            sys.stdout.buffer.write(f"Status: {status}\r\n".encode('iso-8859-1'))
            for header in response_headers:
                sys.stdout.buffer.write(f"{header[0]}: {header[1]}\r\n".encode('iso-8859-1'))
            sys.stdout.buffer.write(b"\r\n")
        sys.stdout.buffer.write(data)
        sys.stdout.buffer.flush()

    def start_response(status, response_headers, exc_info=None):
        # 응답 시작을 처리하는 함수
        if exc_info:
            try:
                if headers_sent:
                    raise exc_info[1].with_traceback(exc_info[2])
            finally:
                exc_info = None
        elif headers_set:
            raise AssertionError("Headers already set!")
        headers_set[:] = [status, response_headers]
        return write

    result = application(environ, start_response)  # 애플리케이션 실행
    try:
        for data in result:
            if data:
                write(data)  # 응답 데이터 출력
        if not headers_sent:
            write(b'')  # 빈 응답일 경우 헤더만 전송
    finally:
        if hasattr(result, 'close'):
            result.close()  # 리소스 정리

# 주석:
# - run_with_cgi: CGI 환경에서 WSGI 애플리케이션을 실행하는 함수
# - environ: WSGI 환경 변수 설정
# - write: 응답을 클라이언트에 전송하는 함수
# - start_response: 응답 헤더를 설정하는 함수
# - result: 애플리케이션의 응답 (이터러블)
WSGI 애플리케이션 (Application/Framework)

서버로부터 전달받은 요청을 처리하고 응답을 생성하여 반환한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def simple_app(environ, start_response):
    """가장 간단한 애플리케이션 객체"""
    status = '200 OK'  # HTTP 상태
    response_headers = [('Content-type', 'text/plain')]  # 응답 헤더
    start_response(status, response_headers)
    return [b"Hello world!\n"]  # 응답 본문

# 주석:
# - environ: 환경 변수와 요청 정보를 담은 딕셔너리
# - start_response: 응답을 시작하는 함수
# - 반환값: 응답 본문을 포함하는 이터러블(iterable)
미들웨어 (Middleware)

서버와 애플리케이션 사이에서 동작하며, 요청을 응답을 가로채어 추가적인 처리를 할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Latinator:
    """평문 응답을 Pig Latin으로 변환하는 미들웨어"""

    def __init__(self, application):
        self.application = application

    def __call__(self, environ, start_response):
        transform_ok = []

        def start_latin(status, response_headers, exc_info=None):
            # 응답 헤더를 확인하고 필요시 변환 설정
            del transform_ok[:]
            for name, value in response_headers:
                if name.lower() == 'content-type' and value == 'text/plain':
                    transform_ok.append(True)
                    break
            # Content-Length 헤더 제거 (변환으로 길이가 바뀔 수 있음)
            response_headers = [(name, value) for name, value in response_headers
                                if name.lower() != 'content-length']
            return start_response(status, response_headers, exc_info)

        response = self.application(environ, start_latin)
        if transform_ok:
            return [self.piglatin(chunk) for chunk in response]
        else:
            return response

    def piglatin(self, text):
        # Pig Latin 변환 로직 (간단한 예시)
        words = text.split()
        return b' '.join([word[1:] + word[0:1] + b'ay' for word in words])

# 주석:
# - Latinator: 응답을 Pig Latin으로 변환하는 미들웨어 클래스
# - __call__: WSGI 호출 규약을 따르는 메서드
# - start_latin: 원본 start_response를 래핑하여 변환 여부 결정
# - piglatin: 텍스트를 Pig Latin으로 변환하는 메서드

작동 방식

  1. 웹 서버가 요청을 받는다.
  2. 서버는 이 요청을 WSGI 애플리케이션에 전달한다.
  3. 애플리케이션은 요청을 처리하고 응답을 생성한다.
  4. 응답은 다시 서버를 통해 클라이언트에게 전송된다.

예시

1
2
3
4
5
def simple_app(environ, start_response):
    status = '200 OK'
    headers = [('Content-type', 'text/plain; charset=utf-8')]
    start_response(status, headers)
    return [b"Hello, World!"]
  • environ: 요청 정보와 서버 환경 변수를 포함하는 딕셔너리
  • start_response: 응답 시작을 알리는 함수
  • 반환값: 응답 본문을 포함하는 이터러블 객체

주요 WSGI 서버/미들웨어

  • Gunicorn: UNIX 환경에서 사용하기 위해 만들어진 Python WSGI HTTP 서버
  • uWSGI: 다양한 언어와 프로토콜을 지원하며, 풀스택 개발이 가능한 WSGI 서버
  • mod_wsgi (Apache 모듈): Apache HTTP Server의 모듈로, Python 웹 애플리케이션을 위한 WSGI 인터페이스를 지원

대표적인 WSGI 프레임워크

  • Flask
  • Django
  • Pyramid
  • Bottle

WSGI를 사용할 때 성능을 최적화하기 위한 몇 가지 방법

  1. 프로덕션용 WSGI 서버 사용:
    개발 서버 대신 Gunicorn이나 uWSGI 같은 프로덕션용 WSGI 서버를 사용하면 처리량과 응답성을 크게 향상시킬 수 있다.
  2. WSGI 프로세스 수 조정:
    서버의 CPU 코어 수에 맞춰 WSGI 프로세스 수를 조정한다. 일반적으로 CPU 코어 수와 동일하게 설정하는 것이 좋다.
  3. 캐싱 구현:
    Flask-Caching 같은 라이브러리를 사용해 자주 요청되는 데이터를 캐싱하여 중복 처리를 줄인다.
  4. 데이터베이스 연결 풀링: SQLAlchemy의 연결 풀링을 활용하여 데이터베이스 연결 오버헤드를 줄인다.
  5. 응답 압축: Flask-Compress를 사용해 응답을 압축하여 전송 데이터량을 줄이고 페이지 로드 시간을 개선한다.
  6. 백그라운드 작업 활용: 이메일 전송이나 대용량 데이터 처리 같은 리소스 집약적 작업은 Celery를 사용해 백그라운드로 처리한다.
  7. 적절한 로깅 설정: 과도한 로깅은 성능에 영향을 줄 수 있으므로, 프로덕션 환경에서는 필요한 로그만 남기도록 설정한다.

참고 및 출처