Tech </> tech.log
· - ·
PythonWeb

[Python][중급] 비동기 시대의 인터페이스 표준, ASGI

ASGI가 프로토콜 수준에서 어떤 문제를 어떻게 해결하는지, 그 설계 원리를 깊이 파고든다.

WSGI의 단순함에는 근본적인 한계가 숨어 있다. WSGI는 동기 함수 호출 모델이다. 함수를 호출하면 응답이 반환될 때까지 그 워커는 아무것도 할 수 없다. 그리고 인터페이스 자체가 "요청 하나 → 응답 하나"라는 HTTP/1.1의 단방향 모델에 맞춰져 있다.

현대 웹은 다르다. WebSocket으로 실시간 양방향 통신을 하고, Server-Sent Events로 데이터를 스트리밍하고, 수십 개의 외부 API를 동시에 호출한다. ASGI(Asynchronous Server Gateway Interface)는 이 요구에 대한 Python 생태계의 답이다. 이 글에서는 ASGI가 프로토콜 수준에서 어떤 문제를 어떻게 해결하는지, 그 설계 원리를 깊이 파고든다.


WSGI의 벽: 인터페이스 자체의 한계

WSGI의 문제를 이해하려면 단순히 "느리다"는 수준을 넘어, 인터페이스 구조 자체가 왜 벽이 되는지를 봐야 한다.

함수 호출/반환 모델의 본질적 제약

WSGI의 시그니처를 다시 보자.

def application(environ, start_response):
    # ... 처리 ...
    start_response('200 OK', headers)
    return [body]

이 인터페이스에는 세 가지 구조적 제약이 내재되어 있다.

첫째, 동기 실행이 강제된다. application은 일반 함수(def)다. 호출되면 return할 때까지 실행 흐름을 점유한다. 중간에 I/O를 기다리더라도 제어권을 이벤트 루프에 돌려줄 방법이 없다.

둘째, 통신이 단방향이다. 입력(environ)은 함수 호출 시 한 번 주어지고, 출력(return)은 함수 종료 시 한 번 반환된다. 함수가 실행되는 동안 클라이언트에게 부분적으로 데이터를 보내거나, 클라이언트로부터 추가 데이터를 받는 구조가 아니다.

셋째, HTTP 프로토콜에 종속되어 있다. environ의 키 자체가 REQUEST_METHOD, PATH_INFO, QUERY_STRING 등 HTTP 개념이다. WebSocket 같은 다른 프로토콜을 표현할 방법이 없다.

# WSGI environ — HTTP에 완전히 종속된 구조
environ = {
    'REQUEST_METHOD': 'GET',       # HTTP 메서드
    'PATH_INFO': '/hello',         # HTTP 경로
    'QUERY_STRING': 'name=world',  # HTTP 쿼리 스트링
    'HTTP_HOST': 'example.com',    # HTTP 헤더
    'wsgi.input': <stream>,        # HTTP 바디 스트림
    # ... WebSocket 연결을 표현할 키가 없다
}

동기 블로킹의 실제 비용

구조적 제약이 실무에서 어떤 비용으로 이어지는지 살펴보자.

import requests

def application(environ, start_response):
    # 외부 API 호출 — 각각 500ms 소요
    weather = requests.get('https://api.weather.com/current')  # 500ms 블로킹
    news = requests.get('https://api.news.com/latest')          # 500ms 블로킹
    # 총 1000ms — 이 동안 워커는 다른 요청을 처리할 수 없다

두 API 호출은 서로 독립적이므로 이론적으로 500ms면 충분하다. 하지만 WSGI의 동기 모델에서는 순차 실행이 강제되어 1000ms가 걸린다. 더 큰 문제는 이 1000ms 동안 워커 프로세스가 완전히 묶인다는 것이다. CPU는 99%의 시간을 아무 일도 하지 않으면서 점유당하고 있다.

WSGI에서는 동시 처리 능력이 워커 수에 정비례한다. 4코어 서버에서 Gunicorn의 기본 워커 수는 9개. 동시 100개 요청이 들어오면 91개는 큐에서 대기한다. 반면 비동기 모델에서는 I/O를 기다리는 순간 다른 태스크로 전환하므로, 단일 프로세스로도 수천 개의 동시 연결을 처리할 수 있다.


ASGI의 핵심 설계: 함수 호출에서 이벤트 메시지로

ASGI는 WSGI의 세 가지 제약을 인터페이스 설계 수준에서 해결한다. 단순히 async를 붙인 것이 아니라, 통신 모델 자체를 바꾼다.

시그니처 비교

# WSGI — 동기 함수, 2개 인자
def application(environ, start_response):
    ...

# ASGI — 비동기 코루틴, 3개 인자
async def application(scope, receive, send):
    ...

인자가 2개에서 3개로 늘어났고, defasync def가 되었다. 이 변화가 의미하는 바를 하나씩 풀어보자.

scope — 연결의 메타데이터

scope는 WSGI의 environ에 대응되지만, 결정적인 차이가 있다. 프로토콜에 종속되지 않는다.

# HTTP 요청의 scope
{
    'type': 'http',               # ← 프로토콜 타입
    'asgi': {'version': '3.0'},
    'http_version': '1.1',
    'method': 'GET',
    'path': '/api/users',
    'query_string': b'page=1',
    'headers': [
        (b'host', b'example.com'),
        (b'accept', b'application/json'),
    ],
    'server': ('127.0.0.1', 8000),
}

# WebSocket 연결의 scope — 같은 인터페이스, 다른 프로토콜
{
    'type': 'websocket',          # ← 타입만 다르다
    'path': '/ws/chat',
    'query_string': b'room=general',
    'headers': [...],
}

# 애플리케이션 생명주기의 scope
{
    'type': 'lifespan',           # ← HTTP도 WebSocket도 아닌 제3의 타입
    'asgi': {'version': '3.0'},
}

scope['type']이라는 단일 필드로 프로토콜을 구분한다. 이 설계 덕분에 하나의 ASGI 애플리케이션이 HTTP, WebSocket, Lifespan을 모두 처리할 수 있다. 미래에 새로운 프로토콜이 추가되더라도, type 값만 하나 더 정의하면 된다.

WSGI의 environ은 HTTP의 개념이 키 이름에 하드코딩되어 있었다(REQUEST_METHOD, HTTP_HOST 등). ASGI의 scope는 프로토콜을 추상화한 것이다.

receive — 비동기 이벤트 수신

receive는 서버로부터 이벤트를 비동기적으로 수신하는 callable이다. WSGI에서 environ['wsgi.input'].read()로 바디를 읽던 것의 비동기 대응물이지만, 근본적으로 다른 점이 있다.

# WSGI — 바디를 한 번에 동기적으로 읽는다
body = environ['wsgi.input'].read()

# ASGI — 이벤트 단위로 비동기적으로 수신한다
event = await receive()
# event = {'type': 'http.request', 'body': b'...', 'more_body': False}

receive()가 반환하는 것은 bytes가 아니라 이벤트 딕셔너리다. 이 이벤트에는 반드시 type 필드가 있으며, 이 타입에 따라 데이터의 의미가 달라진다.

# HTTP 요청 바디 이벤트
{'type': 'http.request', 'body': b'{"name":"Alice"}', 'more_body': False}

# WebSocket 메시지 수신 이벤트
{'type': 'websocket.receive', 'text': 'Hello!'}

# WebSocket 연결 요청 이벤트
{'type': 'websocket.connect'}

# 애플리케이션 시작 이벤트
{'type': 'lifespan.startup'}

같은 receive() 함수가 프로토콜에 따라 완전히 다른 이벤트를 반환한다. 이것이 ASGI의 이벤트 메시지 모델의 핵심이다.

more_body 필드는 특히 중요하다. 대용량 요청 바디가 여러 청크로 나뉘어 전달될 수 있음을 의미한다.

# 대용량 바디 수신 — 청크 단위로 도착할 수 있다
body = b''
while True:
    event = await receive()
    body += event.get('body', b'')
    if not event.get('more_body', False):
        break  # 모든 청크 수신 완료

send — 비동기 이벤트 송신

send는 애플리케이션에서 서버로 이벤트를 비동기적으로 송신하는 callable이다. WSGI의 start_response + return [body]가 합쳐진 것이지만, 역시 이벤트 메시지 모델이라는 점이 다르다.

# HTTP 응답은 두 개의 이벤트로 구성된다

# 1단계: 응답 시작 (상태 코드 + 헤더)
await send({
    'type': 'http.response.start',
    'status': 200,
    'headers': [
        (b'content-type', b'application/json'),
    ],
})

# 2단계: 응답 바디
await send({
    'type': 'http.response.body',
    'body': b'{"message": "Hello"}',
})

**send**를 두 번 호출해야 하는가? WSGI에서는 start_responsereturn이 항상 쌍을 이루었다. ASGI가 이를 분리한 이유는 스트리밍 응답을 자연스럽게 표현하기 위해서다.

# 스트리밍 응답 — 헤더를 먼저, 바디를 여러 번 나눠 전송
await send({
    'type': 'http.response.start',
    'status': 200,
    'headers': [(b'content-type', b'text/event-stream')],
})

for i in range(100):
    await send({
        'type': 'http.response.body',
        'body': f'data: event {i}\n\n'.encode(),
        'more_body': True,       # ← 아직 더 보낼 데이터가 있다
    })
    await asyncio.sleep(1)

await send({
    'type': 'http.response.body',
    'body': b'',
    'more_body': False,          # ← 전송 완료
})

more_body: True로 "이 응답은 아직 끝나지 않았다"는 신호를 보낸다. Server-Sent Events, 대용량 파일 다운로드, 청크 인코딩 등 스트리밍 시나리오를 이벤트 메시지만으로 표현할 수 있다.


세 가지 프로토콜: HTTP, WebSocket, Lifespan

ASGI의 설계가 강력한 이유는, scope['type'] 하나로 근본적으로 다른 통신 패턴을 통일된 인터페이스에 담았다는 것이다.

HTTP 프로토콜

HTTP의 통신 패턴은 "요청 하나 → 응답 하나(또는 스트리밍)"다.

async def http_app(scope, receive, send):
    assert scope['type'] == 'http'

    # 1. 요청 수신 (receive)
    request_event = await receive()
    body = request_event.get('body', b'')

    # 2. 응답 송신 (send × 2)
    await send({
        'type': 'http.response.start',
        'status': 200,
        'headers': [(b'content-type', b'text/plain')],
    })
    await send({
        'type': 'http.response.body',
        'body': b'Hello, ASGI!',
    })

WSGI와 기능적으로 동일하지만, async/await 덕분에 I/O 대기 중에 이벤트 루프가 다른 작업을 처리할 수 있다.

WebSocket 프로토콜

WebSocket의 통신 패턴은 "연결 수립 → 양방향 메시지 반복 → 연결 종료"다. WSGI로는 구조적으로 불가능했던 것이 ASGI에서는 같은 인터페이스로 표현된다.

async def websocket_app(scope, receive, send):
    assert scope['type'] == 'websocket'

    # 1. 연결 수립
    connect_event = await receive()
    # {'type': 'websocket.connect'}
    assert connect_event['type'] == 'websocket.connect'

    await send({'type': 'websocket.accept'})

    # 2. 양방향 메시지 루프
    while True:
        event = await receive()

        if event['type'] == 'websocket.receive':
            text = event.get('text', '')
            await send({
                'type': 'websocket.send',
                'text': f'Echo: {text}',
            })

        elif event['type'] == 'websocket.disconnect':
            break  # 3. 연결 종료

핵심은 같은 **receive/send**가 HTTP와 완전히 다른 이벤트 타입을 주고받는다는 것이다. HTTP에서 receive()http.request를 반환하고, WebSocket에서는 websocket.receive를 반환한다. 인터페이스는 동일하되, 이벤트의 type 필드로 의미가 분기된다.

WebSocket 이벤트의 전체 흐름을 정리하면 다음과 같다.

방향 이벤트 타입 의미
Server → App websocket.connect 클라이언트가 연결을 요청함
App → Server websocket.accept 연결을 수락함
Server → App websocket.receive 클라이언트가 메시지를 보냄
App → Server websocket.send 클라이언트에게 메시지를 보냄
Server → App websocket.disconnect 연결이 끊어짐
App → Server websocket.close 서버 측에서 연결을 닫음

Lifespan 프로토콜

Lifespan은 HTTP도 WebSocket도 아닌 제3의 프로토콜이다. 애플리케이션의 시작과 종료 시점에 초기화/정리 로직을 실행할 수 있게 한다.

async def app_with_lifespan(scope, receive, send):
    if scope['type'] == 'lifespan':
        while True:
            event = await receive()

            if event['type'] == 'lifespan.startup':
                # DB 커넥션 풀 생성, 캐시 워밍업 등
                await initialize_database()
                await send({'type': 'lifespan.startup.complete'})

            elif event['type'] == 'lifespan.shutdown':
                # DB 커넥션 정리, 리소스 해제 등
                await close_database()
                await send({'type': 'lifespan.shutdown.complete'})
                return

    elif scope['type'] == 'http':
        await handle_http(scope, receive, send)

    elif scope['type'] == 'websocket':
        await handle_websocket(scope, receive, send)

WSGI에서는 애플리케이션 초기화를 모듈 임포트 시점이나 별도의 시그널 핸들러에서 처리해야 했다. ASGI는 이를 프로토콜의 일부로 정의하여, 초기화와 정리가 애플리케이션의 생명주기 안에서 관리된다.


이벤트 메시지 모델의 설계 원리

지금까지 본 세 프로토콜에서 일관된 패턴이 보인다. ASGI의 모든 통신은 {'type': '...', ...} 형태의 이벤트 딕셔너리로 이루어진다. 이 설계가 가져오는 이점을 정리해보자.

프로토콜 확장성

새로운 프로토콜을 추가할 때 인터페이스를 변경할 필요가 없다. scope['type']에 새로운 값을 정의하고, 그에 맞는 이벤트 타입을 규정하면 된다.

# 미래에 HTTP/3이나 gRPC 같은 프로토콜이 추가되더라도
async def app(scope, receive, send):
    if scope['type'] == 'http':
        ...
    elif scope['type'] == 'websocket':
        ...
    elif scope['type'] == 'grpc':    # 가상의 미래 프로토콜
        ...
    elif scope['type'] == 'lifespan':
        ...

이벤트 타입의 네이밍 컨벤션

ASGI 이벤트 타입은 {프로토콜}.{동작} 형태의 네이밍 컨벤션을 따른다.

http.request              # HTTP 프로토콜, 요청 수신
http.response.start       # HTTP 프로토콜, 응답 시작
http.response.body        # HTTP 프로토콜, 응답 바디
http.disconnect           # HTTP 프로토콜, 연결 끊김

websocket.connect         # WebSocket 프로토콜, 연결 요청
websocket.accept          # WebSocket 프로토콜, 연결 수락
websocket.receive         # WebSocket 프로토콜, 메시지 수신
websocket.send            # WebSocket 프로토콜, 메시지 송신
websocket.disconnect      # WebSocket 프로토콜, 연결 종료

lifespan.startup          # Lifespan 프로토콜, 시작
lifespan.startup.complete # Lifespan 프로토콜, 시작 완료
lifespan.shutdown         # Lifespan 프로토콜, 종료
lifespan.shutdown.complete# Lifespan 프로토콜, 종료 완료

이 네이밍만 보면 각 이벤트의 프로토콜, 방향, 의미를 즉시 파악할 수 있다. 자기 서술적(self-descriptive)인 설계다.

WSGI 콜백 모델과의 대비

WSGI의 start_response콜백 패턴이다. 서버가 제공한 함수를 애플리케이션이 호출하여 헤더를 전달한다. 응답 바디는 return으로 반환한다. 즉, 헤더 전달 방식(콜백)과 바디 전달 방식(반환값)이 다르다.

# WSGI — 두 가지 다른 메커니즘이 혼재
def app(environ, start_response):
    start_response('200 OK', headers)  # 콜백으로 헤더 전달
    return [body]                       # 반환값으로 바디 전달

ASGI는 모든 통신을 send 이벤트 하나로 통일했다. 헤더든 바디든 모두 await send(event) 형태다.

# ASGI — 통일된 메커니즘
async def app(scope, receive, send):
    await send({...})  # 헤더도 이벤트
    await send({...})  # 바디도 이벤트
    await send({...})  # 추가 바디도 이벤트 (스트리밍)

이 통일성이 스트리밍, WebSocket 양방향 통신 등을 자연스럽게 가능하게 한 것이다.


비동기 실행 모델의 원리

ASGI가 async def를 사용하는 것은 단순한 문법 변경이 아니다. 실행 모델 자체가 바뀐다.

이벤트 루프와 코루틴

import asyncio
import httpx

async def app(scope, receive, send):
    # 두 API를 동시에 호출 — 500ms면 충분
    async with httpx.AsyncClient() as client:
        weather, news = await asyncio.gather(
            client.get('https://api.weather.com/current'),
            client.get('https://api.news.com/latest'),
        )
    # WSGI에서는 1000ms 걸리던 것이 500ms로 줄어든다

asyncio.gather는 두 코루틴을 이벤트 루프에 등록하고, 두 I/O가 모두 완료될 때까지 기다린다. 이 기다리는 동안 이벤트 루프는 다른 요청의 코루틴을 실행할 수 있다.

핵심 원리를 요약하면 다음과 같다. await를 만나면 현재 코루틴은 자발적으로 제어권을 이벤트 루프에 반환한다. 이벤트 루프는 준비된 다른 코루틴을 실행한다. I/O가 완료되면 원래 코루틴이 다시 실행을 재개한다. 이 과정이 **협력적 멀티태스킹(cooperative multitasking)**이다.

CPU 바운드 작업의 함정

비동기 모델에서 가장 흔한 함정이 CPU 바운드 작업이다.

async def app(scope, receive, send):
    # 이 코드는 이벤트 루프를 블로킹한다!
    result = compute_heavy_task()  # CPU를 10초 점유 — await가 없으므로 양보 불가

    # 해결: 별도 스레드로 오프로드
    result = await asyncio.to_thread(compute_heavy_task)

async/awaitI/O 대기를 비동기화하는 메커니즘이지, CPU 연산을 비동기화하는 것이 아니다. 이벤트 루프는 단일 스레드에서 동작하므로, await 없이 오래 실행되는 코드는 다른 모든 코루틴을 멈추게 한다.

동기 라이브러리와의 호환

같은 맥락에서, 동기 I/O 라이브러리도 이벤트 루프를 블로킹한다.

import requests  # 동기 라이브러리

async def app(scope, receive, send):
    # 이벤트 루프를 블로킹한다!
    response = requests.get('https://api.example.com')

    # 해결 1: 비동기 라이브러리 사용
    import httpx
    async with httpx.AsyncClient() as client:
        response = await client.get('https://api.example.com')

    # 해결 2: 부득이한 경우, 스레드로 오프로드
    response = await asyncio.to_thread(
        requests.get, 'https://api.example.com'
    )

ASGI 환경에서 최대의 이점을 얻으려면 I/O 작업에 비동기 라이브러리를 사용해야 한다. requestshttpx, psycopg2asyncpg, redis-pyredis.asyncio 같은 전환이 필요하다.


ASGI 미들웨어: 이벤트 가로채기

WSGI에서 미들웨어는 "WSGI 앱을 감싸는 WSGI 앱"이었다. ASGI에서도 원리는 동일하지만, **receive** **send**를 가로채서 교체할 수 있다는 점에서 더 강력하다.

import time

class TimingMiddleware:
    def __init__(self, app):
        self.app = app

    async def __call__(self, scope, receive, send):
        if scope['type'] != 'http':
            await self.app(scope, receive, send)
            return

        start = time.perf_counter()

        # send를 가로채서 헤더에 처리 시간을 주입
        async def timed_send(event):
            if event['type'] == 'http.response.start':
                elapsed = time.perf_counter() - start
                headers = list(event.get('headers', []))
                headers.append((b'x-process-time', f'{elapsed:.4f}'.encode()))
                event = {**event, 'headers': headers}
            await send(event)

        await self.app(scope, receive, timed_send)

send 함수를 timed_send로 교체하여, http.response.start 이벤트가 서버에 전달되기 전에 커스텀 헤더를 주입한다. receive도 같은 방식으로 교체할 수 있으므로, 요청과 응답 양쪽을 모두 가로챌 수 있다.

# receive를 가로채는 미들웨어 예시 — 요청 바디 로깅
class RequestLoggingMiddleware:
    def __init__(self, app):
        self.app = app

    async def __call__(self, scope, receive, send):
        if scope['type'] != 'http':
            await self.app(scope, receive, send)
            return

        async def logged_receive():
            event = await receive()
            if event['type'] == 'http.request':
                print(f"Request body: {event.get('body', b'')[:100]}")
            return event  # 원본 이벤트를 그대로 전달

        await self.app(scope, logged_receive, send)

이 패턴은 WSGI의 미들웨어보다 유연하다. WSGI에서 start_response를 가로채려면 콜백 함수를 래핑해야 했고, 응답 바디를 가로채려면 반환된 iterable을 래핑해야 했다. ASGI에서는 receivesend가 모두 같은 형태(비동기 callable)이므로, 동일한 래핑 패턴으로 양방향을 처리할 수 있다.


ASGI 서버의 역할

WSGI에 Gunicorn이 있듯, ASGI에는 전용 서버가 필요하다. ASGI 서버의 역할은 다음과 같다.

네트워크 연결을 관리하고, HTTP/WebSocket 프로토콜을 파싱하여, scope 딕셔너리를 구성하고, receivesend 코루틴을 생성한 뒤, app(scope, receive, send)를 호출하는 것이다.

Uvicorn

가장 널리 사용되는 ASGI 서버다. uvloop(libuv 기반 고성능 이벤트 루프)과 httptools(Node.js의 http-parser 바인딩) 위에서 동작한다.

# 기본 실행
$ uvicorn app:app

# 프로덕션 설정
$ uvicorn app:app \
    --host 0.0.0.0 \
    --port 8000 \
    --workers 4 \
    --loop uvloop \
    --http httptools

app:app에서 앞의 app은 모듈명, 뒤의 app은 ASGI callable의 이름이다. --workers는 멀티 프로세스 모드로, 각 프로세스가 독립 이벤트 루프를 운영하여 멀티코어를 활용한다.

Gunicorn + Uvicorn 워커

프로덕션에서는 Gunicorn의 프로세스 관리 위에 Uvicorn 워커를 올리는 조합이 일반적이다.

$ gunicorn app:app \
    --worker-class uvicorn.workers.UvicornWorker \
    --workers 4 \
    --bind 0.0.0.0:8000

Gunicorn이 프로세스 fork, graceful restart, 워커 타임아웃을 관리하고, 각 워커 내부에서 Uvicorn의 비동기 이벤트 루프가 요청을 처리한다.

Hypercorn

HTTP/2와 HTTP/3(QUIC)를 지원하는 ASGI 서버다.

$ hypercorn app:app --bind 0.0.0.0:8000

WSGI와의 공존: 점진적 전환

기존 WSGI 애플리케이션을 한 번에 ASGI로 전환하기 어려운 경우, 어댑터를 통해 공존할 수 있다. 이것이 가능한 이유는 ASGI가 WSGI의 **상위 호환(superset)**으로 설계되었기 때문이다.

# WSGI 앱을 ASGI 인터페이스로 래핑
from asgiref.wsgi import WsgiToAsgi

from my_flask_app import flask_app
asgi_wrapped = WsgiToAsgi(flask_app)

# 이제 Uvicorn으로 실행할 수 있다
# $ uvicorn main:asgi_wrapped

래핑된 앱 내부는 여전히 동기적으로 실행되므로 비동기의 이점은 얻지 못하지만, ASGI 서버 위에서 기존 코드를 그대로 운영하면서 점진적으로 전환할 수 있다.

Django는 이 전략을 공식적으로 채택했다. Django 3.0부터 ASGI를 지원하며, 동기 뷰와 비동기 뷰를 하나의 프로젝트에서 혼용할 수 있다.

# Django — 동기 뷰와 비동기 뷰 혼용
from django.http import JsonResponse

# 기존 동기 뷰 — 그대로 동작
def sync_view(request):
    return JsonResponse({'type': 'sync'})

# 새로운 비동기 뷰
async def async_view(request):
    data = await fetch_external_api()
    return JsonResponse({'type': 'async', 'data': data})

WSGI에서 ASGI로의 패러다임 전환

이 글에서 살펴본 ASGI의 설계를 한마디로 요약하면, 함수 호출/반환"에서 이벤트 메시지 송수신 으로의 전환이다.

WSGI ASGI
시그니처 def app(environ, start_response) async def app(scope, receive, send)
실행 모델 동기 (블로킹) 비동기 (논블로킹)
통신 방식 콜백(start_response) + 반환(return) 이벤트 딕셔너리(send/receive)
프로토콜 HTTP만 HTTP + WebSocket + Lifespan
스트리밍 제한적 (iterable) more_body 플래그로 자연스럽게
확장성 인터페이스 변경 필요 scope['type'] 추가만으로 확장

WSGI의 environ은 HTTP 요청의 스냅샷이었다. ASGI의 scope + receive + send지속적인 대화 채널이다. 이 채널 위에서 HTTP의 단방향 통신도, WebSocket의 양방향 통신도, 애플리케이션의 생명주기 관리도 모두 동일한 패턴으로 이루어진다.


마치며

ASGI는 단순히 "비동기 WSGI"가 아니다. 통신 모델 자체를 이벤트 기반으로 재설계한 것이다. scope로 프로토콜을 추상화하고, receivesend로 양방향 이벤트를 주고받으며, async/await로 I/O 대기 중의 자원 낭비를 제거한다.

물론 이 날것의 이벤트 딕셔너리를 직접 다루는 것은 번거롭다. WSGI에 Werkzeug가 environRequest 객체로 감싸주었듯이, ASGI에도 scope/receive/send를 편리한 객체로 감싸주는 유틸리티가 필요하다. 다음 글에서는 그 역할을 수행하는 Starlette를 살펴보며, ASGI 표준이 어떻게 실용적인 도구로 확장되는지를 확인할 것이다.


참고 자료