Zettelkasten

ASGI는 WSGI에 async를 얹은 것이 아니라 양방향 메시지 스트림 모델로 재설계된 것이다

·수정 2026.05.04·수정 1

요약

  • ASGI의 본질은 async가 아니라 양방향 메시지 스트림 모델이다. async는 결과일 뿐.
  • WSGI는 (environ, start_response) → iterable 의 1회성 request-response 모델, ASGI는 (scope, receive, send) 의 N개 메시지 스트림 모델이다.
  • 이 차이 때문에 WebSocket/SSE/HTTP/2 streaming/lifespan 같은 개념을 WSGI는 표현 못하고 ASGI는 자연스럽게 다룬다.

본문

인터페이스 비교

WSGI:

def app(environ, start_response):
    start_response("200 OK", [("Content-Type", "text/plain")])
    return [b"Hello"]
  • 함수 1회 호출 → 응답 반환 → 끝
  • 입력: environ dict 한 덩어리
  • 출력: iterable body

ASGI:

async def app(scope, receive, send):
    await send({"type": "http.response.start", "status": 200, "headers": [...]})
    await send({"type": "http.response.body", "body": b"Hello"})
  • 입력: scope (메타데이터) + receive/send (메시지 큐)
  • 메시지를 여러 번 주고받는 스트림

결정적 차이: 메시지 스트림

ASGI는 receive/send를 원하는 만큼 await 할 수 있다.

async def app(scope, receive, send):
    while True:
        msg = await receive()
        if msg["type"] == "websocket.receive":
            await send({"type": "websocket.send", "text": "echo: " + msg["text"]})

→ WebSocket, SSE, HTTP/2 streaming, long-polling이 자연스럽게 표현된다. WSGI에는 이 모델 자체가 없다.

scope의 type 필드: 멀티 프로토콜 지원

scope["type"]  # "http", "websocket", "lifespan"
  • http — HTTP 요청
  • websocket — WebSocket 연결
  • lifespan — 앱 startup/shutdown 이벤트

WSGI는 HTTP만, lifespan 개념도 없다.

비유로 정리

WSGI ASGI
비유 편지 (보내고 답장 받고 끝) 전화 (양방향 대화)
메시지 수 요청 1, 응답 1 N개 (양쪽 다)
프로토콜 HTTP만 HTTP + WebSocket + lifespan
시그니처 (environ, start_response) (scope, receive, send)

호환성의 비대칭

  • WSGI 앱 → ASGI 환경에서 실행: 가능 (asgiref.wsgi.WsgiToAsgi로 wrap, 단 동기 핸들러는 그대로 동기)
  • ASGI 앱 → WSGI 환경에서 실행: 불가능. ASGI의 메시지 스트림을 WSGI의 1회성 모델로 표현 못함

이 비대칭이 "ASGI = WSGI 상위호환" 같은 인상을 주지만, 실제로는 다른 모델이다.

핵심 통찰

ASGI = WSGI + async (X) ASGI = 양방향 메시지 스트림 + async (O)

async는 양방향 스트림을 자연스럽게 표현하기 위한 수단이지, ASGI의 정의 자체가 async인 것은 아니다.

참고