FastAPI와 Django 같은 Python 웹 프레임워크는 일반적으로 웹 서버를 통해 HTTP 요청을 처리합니다. 그러나 ASGI의 동작 원리를 이해하면 별도의 웹 서버를 거치지 않고도 서버리스 환경에서 기존 애플리케이션을 실행할 수 있습니다. 이 글에서는 ASGI의 기본 구조부터 AWS Lambda와 Mangum의 연동 방식, 그리고 Google Cloud Functions에서 활용할 수 있도록 개발한 Vellox의 구현 원리까지 살펴봅니다.
ASGI 표준은 Python 웹 애플리케이션을 어떻게 연결할까요?
ASGI는 비동기 Python 웹 애플리케이션을 위한 표준 인터페이스입니다. 웹 서버와 애플리케이션 사이의 통신 규칙을 정의하기 때문에 프레임워크와 실행 환경을 서로 독립적으로 구성할 수 있습니다.

ASGI와 WSGI의 차이점
ASGI는 Asynchronous Server Gateway Interface의 약자입니다. Python 웹 생태계에서 서버와 애플리케이션이 데이터를 주고받는 방식을 표준화한 인터페이스로, FastAPI, Starlette, Django 등의 프레임워크에서 활용됩니다.
기존에 널리 사용되던 WSGI는 동기식 요청 처리 모델을 중심으로 설계되었습니다. 반면 ASGI는 비동기 처리를 지원하며, HTTP뿐만 아니라 WebSocket과 같은 연결 방식도 처리할 수 있도록 설계되었습니다.
ASGI의 핵심은 애플리케이션을 호출하는 방식에 있습니다. ASGI 애플리케이션은 다음 세 가지 인자를 전달받습니다.
- scope: 현재 연결이나 요청에 관한 메타데이터를 담고 있는 객체입니다.
- receive: 애플리케이션이 요청 본문이나 연결 관련 메시지를 비동기적으로 수신할 수 있도록 하는 함수입니다.
- send: 애플리케이션이 응답이나 연결 관련 메시지를 외부로 전달하는 데 사용하는 함수입니다.
이 세 가지 요소를 올바르게 구성하면 특정 웹 서버에 종속되지 않고 ASGI 애플리케이션을 실행할 수 있습니다.
scope에는 어떤 정보가 포함될까요?
scope는 요청을 처리하는 데 필요한 정보를 전달합니다. 일반적으로 Python 딕셔너리 형태이며, HTTP 요청에서는 메서드, 경로, 헤더, 프로토콜 버전 등의 데이터를 포함합니다.
예를 들어 클라이언트가 /items 경로에 GET 요청을 보냈다면 다음과 비슷한 구조를 구성할 수 있습니다.
scope = {
"type": "http",
"http_version": "1.1",
"method": "GET",
"scheme": "https",
"path": "/items",
"headers": [
[b"content-type", b"application/json"],
],
"query_string": b"",
"asgi": {
"version": "3.0",
"spec_version": "2.0",
},
}
위 코드는 구조를 이해하기 위한 간단한 예시입니다. 실제 요청을 처리할 때는 사용 중인 ASGI 사양에 맞춰 필요한 필드를 추가해야 합니다. 특히 HTTP 요청에서 사용하는 scope와 WebSocket 연결에서 사용하는 scope는 type과 필요한 정보가 서로 다릅니다.
receive와 send는 요청과 응답을 어떻게 처리할까요?
receive와 send는 단순한 데이터 변수가 아니라 비동기 메시지 교환을 위한 호출 인터페이스입니다. 애플리케이션은 이 함수들을 이용해 요청을 읽고 응답을 작성합니다.
다음은 ASGI 인터페이스를 이용해 간단한 HTTP 응답을 반환하는 예시입니다.
async def application(scope, receive, send):
await send({
"type": "http.response.start",
"status": 200,
"headers": [
[b"content-type", b"text/plain; charset=utf-8"],
],
})
await send({
"type": "http.response.body",
"body": b"Hello, world!",
})
첫 번째 메시지는 HTTP 상태 코드와 응답 헤더를 지정합니다. 두 번째 메시지는 실제 응답 본문을 전달합니다.
여기서 중요한 점은 ASGI 애플리케이션이 반드시 네트워크 소켓을 직접 사용해야 하는 것은 아니라는 사실입니다. ASGI 규약에 맞는 입력과 메시지 전달 함수를 제공한다면, 애플리케이션을 호출하는 환경에 따라 다른 방식으로 실행할 수 있습니다.
FastAPI도 동일한 ASGI 규약을 따릅니다
FastAPI는 Starlette를 기반으로 만들어진 프레임워크이며, ASGI 애플리케이션으로 동작합니다. 내부 구현에서 __call__ 메서드를 통해 ASGI 요청을 처리하는 구조를 확인할 수 있습니다.
async def __call__(self, scope, receive, send):
...
위 코드는 실제 구현의 핵심 형태를 간략화한 것입니다. 세 인자를 전달받는 호출 구조가 ASGI의 규약과 일치한다는 점에 주목할 필요가 있습니다.
따라서 FastAPI 역시 ASGI 인터페이스에 맞는 scope, receive, send를 전달받으면 요청을 처리할 수 있습니다. 일반적인 웹 서버가 이 과정을 담당하지만, 다른 실행 환경에서 동일한 인터페이스를 제공하는 것도 가능합니다.
AWS Lambda에서 ASGI 애플리케이션을 실행하는 방법
AWS Lambda는 서버를 직접 관리하지 않고 함수를 실행할 수 있는 서버리스 컴퓨팅 서비스입니다. 기존 ASGI 애플리케이션과 Lambda 사이에서 요청 형식을 변환해 주면 두 환경을 연결할 수 있습니다.
Lambda의 이벤트 기반 실행 구조
일반적인 Lambda 핸들러는 event와 context라는 두 인자를 전달받습니다.
import json
def lambda_handler(event, context):
return {
"statusCode": 200,
"body": json.dumps("Hello from Lambda!"),
}
event에는 함수 실행을 유발한 요청의 데이터가 포함되며, context에는 실행 환경과 관련된 정보가 담깁니다.
API Gateway를 통해 HTTP 요청이 전달되는 경우, 이벤트 객체에는 HTTP 메서드, URL 경로, 헤더, 쿼리 문자열, 요청 본문 등의 정보가 포함될 수 있습니다. 다만 구체적인 구조는 API Gateway의 통합 유형과 이벤트 버전에 따라 달라질 수 있습니다.
기본 Lambda 핸들러는 이러한 이벤트를 직접 해석해 응답을 반환합니다. 반면 FastAPI 같은 프레임워크는 ASGI 규약에 따라 요청을 전달받기를 기대합니다.
이 차이를 해결하려면 Lambda 이벤트를 ASGI 요청 형식으로 변환하고, ASGI 애플리케이션이 생성한 응답을 다시 Lambda가 요구하는 형식으로 변환해야 합니다.
Lambda 이벤트를 ASGI scope로 변환하기
AWS Lambda와 ASGI 애플리케이션을 연결하는 대표적인 도구가 Mangum입니다. Mangum은 Lambda 이벤트를 분석하고 ASGI 애플리케이션이 이해할 수 있는 요청 정보를 구성하는 어댑터 역할을 합니다.
예를 들어 API Gateway에서 다음과 같은 요청 정보가 전달되었다고 가정해 보겠습니다.
{
"httpMethod": "GET",
"path": "/items",
"headers": {
"content-type": "application/json"
},
"queryStringParameters": {
"page": "1"
},
"body": null,
"isBase64Encoded": false
}
이 데이터는 그대로 ASGI 애플리케이션에 전달할 수 없습니다. Lambda 이벤트의 헤더는 일반적인 딕셔너리 형태지만, ASGI에서는 헤더를 바이트 문자열의 쌍으로 표현합니다. HTTP 메서드와 경로, 쿼리 문자열 등도 ASGI에서 요구하는 형식으로 변환해야 합니다.
Mangum은 이러한 변환을 담당합니다. 개념적으로는 다음과 같은 scope를 구성하게 됩니다.
scope = {
"type": "http",
"http_version": "1.1",
"method": event["httpMethod"],
"path": event["path"],
"headers": [
(key.lower().encode(), value.encode())
for key, value in event.get("headers", {}).items()
],
"query_string": b"page=1",
"root_path": "",
}
위 코드는 변환 과정을 설명하기 위한 예시이며, 실제 운영 코드에서는 다중 헤더, 인코딩된 본문, URL 인코딩, 프록시 경로, 클라이언트 정보 등을 추가로 처리해야 합니다.
특히 API Gateway의 이벤트 형식이 다르면 메서드와 경로를 가져오는 방법도 달라집니다. 따라서 실제 구현에서는 선택한 통합 유형에 맞는 이벤트 구조를 사용해야 합니다.
요청 변환 이후에는 어떤 과정이 진행될까요?
ASGI 어댑터는 scope만 만드는 것으로 작업을 끝내지 않습니다. 요청 본문을 전달하는 receive와 응답 메시지를 수집하는 send도 구현해야 합니다.
전체 처리 흐름은 다음과 같습니다.
- API Gateway가 HTTP 요청을 Lambda 이벤트로 변환합니다.
- Lambda 핸들러가 이벤트를 ASGI 어댑터에 전달합니다.
- 어댑터가 이벤트를 분석해 scope를 구성합니다.
- receive를 통해 요청 본문을 제공하고 ASGI 애플리케이션을 실행합니다.
- 애플리케이션이 send로 전달한 상태 코드, 헤더, 본문을 수집합니다.
- 어댑터가 결과를 Lambda 통합에서 요구하는 응답 형식으로 변환합니다.
이 과정을 구현하면 기존 FastAPI 애플리케이션을 완전히 새로 작성하지 않고도 Lambda에서 실행할 수 있습니다.
다만 모든 종류의 요청이 무조건 지원되는 것은 아닙니다. 응답 스트리밍, WebSocket, 바이너리 데이터, 장시간 연결 등은 API Gateway의 기능과 통합 유형, Lambda의 실행 모델에 따라 별도의 제약을 받을 수 있습니다.
Google Cloud Functions용 ASGI 어댑터 Vellox의 구현 원리
AWS Lambda에서 Mangum이 수행하는 역할을 이해하면 다른 서버리스 플랫폼에서도 비슷한 구조를 구현할 수 있습니다. Vellox는 이러한 아이디어를 바탕으로 Google Cloud Functions에서 ASGI 애플리케이션을 실행할 수 있도록 개발한 프로젝트입니다.
Google Cloud Functions와 Lambda의 입력 방식 비교
AWS Lambda는 일반적인 함수 실행 과정에서 event와 context를 전달합니다. 반면 Google Cloud Functions의 HTTP 요청 처리 환경에서는 Flask의 요청 객체를 중심으로 입력을 다룰 수 있습니다.
Flask의 Request 객체에는 HTTP 메서드, 경로, 헤더, 쿼리 문자열, 요청 본문과 같은 정보가 포함됩니다. 이 데이터는 FastAPI가 직접 사용하는 ASGI scope와 형식이 다릅니다.
따라서 Vellox를 구현할 때 가장 중요한 작업은 Flask 요청 객체를 ASGI 규약에 맞게 변환하는 것이었습니다.
Flask Request 객체에서 ASGI scope 생성하기
Flask 요청 객체에는 ASGI 요청을 구성하는 데 필요한 정보가 대부분 들어 있습니다. 예를 들어 요청 메서드는 request.method, 경로는 request.path, 프로토콜은 request.scheme을 통해 확인할 수 있습니다.
이를 바탕으로 다음과 같은 형태의 scope를 구성할 수 있습니다.
@property
def scope(self):
return {
"type": "http",
"http_version": "1.1",
"method": self.request.method,
"headers": [
[key.lower().encode(), value.encode()]
for key, value in dict(self.request.headers).items()
],
"path": self.request.path,
"raw_path": None,
"root_path": "",
"scheme": self.request.scheme or "https",
"query_string": self.request.query_string,
"client": (None, 0),
"asgi": {
"version": "3.0",
"spec_version": "2.0",
},
}
이 코드는 핵심 변환 과정을 보여 주는 단순화된 예시입니다. 실제 구현에서는 서버 주소와 포트, 기본 경로 제거, 헤더 처리, 요청 본문 전달 등 추가적인 처리가 필요할 수 있습니다.
또한 Flask의 요청 객체를 직접 보관해 필요한 시점에 접근할 수 있도록 별도의 scope 항목을 제공하는 것도 가능합니다. 이러한 방식은 기존 Flask 요청 데이터와 ASGI 인터페이스를 연결할 때 유용합니다.
서버리스 환경에서 발생하는 차이점 처리하기
서로 다른 실행 환경을 연결할 때는 단순히 필드 이름만 바꾸는 것으로 충분하지 않을 수 있습니다. 요청 본문을 읽는 방식, 경로의 접두사를 처리하는 방식, 헤더의 표현 형식 등이 플랫폼마다 다르기 때문입니다.
Vellox 역시 Flask 요청 객체를 변환하는 핸들러를 구성하고, Google Cloud Functions에서 정상적으로 작동하도록 필