[FastAPI] FastAPI ๊ฐ๋ ๋ฟ์๊ธฐ: ASGI
by GomSon-E๋ฐ์ํ
ASGI
ASGI(Asynchronous Server Gateway Interface)๋ ๋น๋๊ธฐ Python ์น ์๋ฒ, ํ๋ ์์ํฌ, ์ ํ๋ฆฌ์ผ์ด์ ๊ฐ์ ํ์ค ์ธํฐํ์ด์ค์ด๋ค. WSGI์ ํ์ ๋ฒ์ ์ผ๋ก, ๋น๋๊ธฐ ์ฒ๋ฆฌ์ WebSocket ๊ฐ์ ์ฅ์๊ฐ ์ฐ๊ฒฐ์ ์ง์ํ๋ค.
1. ์ฃผ์ ๊ธฐ๋ฅ ๋ฐ ํน์ง
- ๋น๋๊ธฐ ์ฒ๋ฆฌ: async/await๋ฅผ ํ์ฉํ ๊ณ ์ฑ๋ฅ ์น ์ ํ๋ฆฌ์ผ์ด์
- ํ๋กํ ์ฝ ์ง์: HTTP, WebSocket, HTTP/2 ๋ฑ ๋ค์ํ ํ๋กํ ์ฝ
- ์ฅ์๊ฐ ์ฐ๊ฒฐ: ์ค์๊ฐ ํต์ , ์คํธ๋ฆฌ๋ฐ ์ง์
- ํ์คํ: ์๋ฒ์ ํ๋ ์์ํฌ ๊ฐ ํธํ์ฑ ๋ณด์ฅ
- ๋์์ฑ: ๋ง์ ์์ ๋์ ์ฐ๊ฒฐ ํจ์จ์ ์ฒ๋ฆฌ
- WSGI ํธํ: WSGI ์ฑ์ ASGI๋ก ๋ํ ๊ฐ๋ฅ
2. ์ฌ์ฉ ๋ชฉ์
- FastAPI, Starlette ๊ฐ์ ๋น๋๊ธฐ ํ๋ ์์ํฌ ์ฌ์ฉ
- WebSocket ์ค์๊ฐ ํต์ ์ง์
- ๋์ ๋์์ฑ์ด ์๊ตฌ๋๋ ์ ํ๋ฆฌ์ผ์ด์
- SSE(Server-Sent Events), ์คํธ๋ฆฌ๋ฐ ์ง์
- ๋ง์ดํฌ๋ก์๋น์ค, API ๊ฒ์ดํธ์จ์ด ๊ตฌ์ถ
3. ํ์ด์ฌ ์์ ์ฝ๋
โ ๊ธฐ๋ณธ ASGI ์ ํ๋ฆฌ์ผ์ด์
# ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ASGI ์ ํ๋ฆฌ์ผ์ด์
async def app(scope, receive, send):
"""
scope: ์ฐ๊ฒฐ ์ ๋ณด (ํ์
, ๊ฒฝ๋ก, ํค๋ ๋ฑ)
receive: ํด๋ผ์ด์ธํธ๋ก๋ถํฐ ๋ฉ์์ง ์์
send: ํด๋ผ์ด์ธํธ์๊ฒ ๋ฉ์์ง ์ ์ก
"""
assert scope['type'] == 'http'
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!',
})
# Uvicorn์ผ๋ก ์คํ: uvicorn main:app
โก ASGI ๊ตฌ์กฐ ์ดํดํ๊ธฐ
async def detailed_app(scope, receive, send):
"""
ASGI์ 3๊ฐ์ง ํต์ฌ ํ๋ผ๋ฏธํฐ ์ค๋ช
"""
# 1. scope: ์ฐ๊ฒฐ์ ๋ํ ์ ๋ณด
print(f"Type: {scope['type']}") # 'http' or 'websocket' or 'lifespan'
print(f"Method: {scope.get('method')}") # GET, POST, etc
print(f"Path: {scope.get('path')}") # /users/123
print(f"Query String: {scope.get('query_string')}") # b'name=john'
print(f"Headers: {scope.get('headers')}") # [(b'host', b'localhost')]
# 2. receive: ํด๋ผ์ด์ธํธ ๋ฉ์์ง ์์
message = await receive()
print(f"Received: {message}")
# HTTP: {'type': 'http.request', 'body': b'...'}
# WebSocket: {'type': 'websocket.receive', 'text': '...'}
# 3. send: ํด๋ผ์ด์ธํธ์๊ฒ ๋ฉ์์ง ์ ์ก
await send({
'type': 'http.response.start',
'status': 200,
'headers': [[b'content-type', b'application/json']],
})
await send({
'type': 'http.response.body',
'body': b'{"message": "Hello"}',
})
โข HTTP ์์ฒญ ์ฒ๋ฆฌ
import json
async def http_app(scope, receive, send):
"""HTTP ์์ฒญ ์ฒ๋ฆฌ ์์"""
if scope['type'] != 'http':
return
# ์์ฒญ ๊ฒฝ๋ก์ ๋ฐ๋ฅธ ๋ผ์ฐํ
path = scope['path']
method = scope['method']
if path == '/' and method == 'GET':
await handle_home(scope, receive, send)
elif path == '/api/users' and method == 'GET':
await handle_users(scope, receive, send)
elif path == '/api/users' and method == 'POST':
await handle_create_user(scope, receive, send)
else:
await handle_404(scope, receive, send)
async def handle_home(scope, receive, send):
await send({
'type': 'http.response.start',
'status': 200,
'headers': [[b'content-type', b'text/html']],
})
await send({
'type': 'http.response.body',
'body': b'<h1>Welcome to ASGI App</h1>',
})
async def handle_users(scope, receive, send):
users = [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"}
]
await send({
'type': 'http.response.start',
'status': 200,
'headers': [[b'content-type', b'application/json']],
})
await send({
'type': 'http.response.body',
'body': json.dumps(users).encode('utf-8'),
})
async def handle_create_user(scope, receive, send):
# ์์ฒญ ๋ฐ๋ ์ฝ๊ธฐ
body = b''
while True:
message = await receive()
body += message.get('body', b'')
if not message.get('more_body'):
break
# JSON ํ์ฑ
try:
data = json.loads(body.decode('utf-8'))
user = {"id": 3, "name": data.get('name')}
await send({
'type': 'http.response.start',
'status': 201,
'headers': [[b'content-type', b'application/json']],
})
await send({
'type': 'http.response.body',
'body': json.dumps(user).encode('utf-8'),
})
except Exception as e:
await handle_400(scope, receive, send, str(e))
async def handle_404(scope, receive, send):
await send({
'type': 'http.response.start',
'status': 404,
'headers': [[b'content-type', b'text/plain']],
})
await send({
'type': 'http.response.body',
'body': b'Not Found',
})
async def handle_400(scope, receive, send, error):
await send({
'type': 'http.response.start',
'status': 400,
'headers': [[b'content-type', b'application/json']],
})
await send({
'type': 'http.response.body',
'body': json.dumps({"error": error}).encode('utf-8'),
})
โฃ WebSocket ์ฒ๋ฆฌ
async def websocket_app(scope, receive, send):
"""WebSocket ์ฐ๊ฒฐ ์ฒ๋ฆฌ"""
if scope['type'] == 'websocket':
# WebSocket ์ฐ๊ฒฐ ์๋ฝ
await send({
'type': 'websocket.accept'
})
# ๋ฉ์์ง ์ก์์ ๋ฃจํ
while True:
message = await receive()
if message['type'] == 'websocket.receive':
# ํด๋ผ์ด์ธํธ๋ก๋ถํฐ ๋ฉ์์ง ์์
text = message.get('text', '')
# ์์ฝ ์๋ต
await send({
'type': 'websocket.send',
'text': f'Echo: {text}'
})
elif message['type'] == 'websocket.disconnect':
# ์ฐ๊ฒฐ ์ข
๋ฃ
print("WebSocket disconnected")
break
elif scope['type'] == 'http':
# HTTP ์์ฒญ์ ๋ค๋ฅด๊ฒ ์ฒ๋ฆฌ
await send({
'type': 'http.response.start',
'status': 200,
'headers': [[b'content-type', b'text/plain']],
})
await send({
'type': 'http.response.body',
'body': b'Use WebSocket connection',
})
โค ์ฑํ ์๋ฒ ์์
import json
from typing import Set
# ์ฐ๊ฒฐ๋ ํด๋ผ์ด์ธํธ ๊ด๋ฆฌ
connections: Set = set()
async def chat_server(scope, receive, send):
"""๊ฐ๋จํ ์ฑํ
์๋ฒ"""
if scope['type'] == 'websocket':
await send({'type': 'websocket.accept'})
# ํ์ฌ ์ฐ๊ฒฐ ์ถ๊ฐ
connections.add(send)
try:
while True:
message = await receive()
if message['type'] == 'websocket.receive':
text = message.get('text', '')
data = json.loads(text)
# ๋ชจ๋ ํด๋ผ์ด์ธํธ์๊ฒ ๋ธ๋ก๋์บ์คํธ
broadcast_message = json.dumps({
'user': data.get('user', 'Anonymous'),
'message': data.get('message', '')
})
for connection in connections:
try:
await connection({
'type': 'websocket.send',
'text': broadcast_message
})
except:
pass
elif message['type'] == 'websocket.disconnect':
break
finally:
# ์ฐ๊ฒฐ ์ ๊ฑฐ
connections.discard(send)
elif scope['type'] == 'http':
# ์ฑํ
ํด๋ผ์ด์ธํธ HTML ์ ๊ณต
html = """
<!DOCTYPE html>
<html>
<head><title>Chat</title></head>
<body>
<div id="messages"></div>
<input id="username" placeholder="Username" />
<input id="message" placeholder="Message" />
<button onclick="send()">Send</button>
<script>
const ws = new WebSocket('ws://localhost:8000/ws');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
const msg = document.createElement('div');
msg.textContent = `${data.user}: ${data.message}`;
document.getElementById('messages').appendChild(msg);
};
function send() {
const user = document.getElementById('username').value;
const message = document.getElementById('message').value;
ws.send(JSON.stringify({user, message}));
document.getElementById('message').value = '';
}
</script>
</body>
</html>
"""
await send({
'type': 'http.response.start',
'status': 200,
'headers': [[b'content-type', b'text/html']],
})
await send({
'type': 'http.response.body',
'body': html.encode('utf-8'),
})
โฅ ASGI ๋ฏธ๋ค์จ์ด
class LoggingMiddleware:
"""์์ฒญ ๋ก๊น
๋ฏธ๋ค์จ์ด"""
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope['type'] == 'http':
print(f"[REQUEST] {scope['method']} {scope['path']}")
await self.app(scope, receive, send)
if scope['type'] == 'http':
print(f"[RESPONSE] Completed")
class CORSMiddleware:
"""CORS ์ฒ๋ฆฌ ๋ฏธ๋ค์จ์ด"""
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope['type'] == 'http':
async def send_with_cors(message):
if message['type'] == 'http.response.start':
headers = list(message.get('headers', []))
headers.append([b'access-control-allow-origin', b'*'])
message['headers'] = headers
await send(message)
await self.app(scope, receive, send_with_cors)
else:
await self.app(scope, receive, send)
# ๋ฏธ๋ค์จ์ด ์ ์ฉ
async def my_app(scope, receive, send):
await send({
'type': 'http.response.start',
'status': 200,
'headers': [[b'content-type', b'text/plain']],
})
await send({
'type': 'http.response.body',
'body': b'Hello with middleware',
})
# ์ฌ๋ฌ ๋ฏธ๋ค์จ์ด ์ฒด์ด๋
app = LoggingMiddleware(CORSMiddleware(my_app))
โฆ Lifespan ์ด๋ฒคํธ ์ฒ๋ฆฌ
import asyncio
async def lifespan_app(scope, receive, send):
"""์ ํ๋ฆฌ์ผ์ด์
์๋ช
์ฃผ๊ธฐ ๊ด๋ฆฌ"""
if scope['type'] == 'lifespan':
while True:
message = await receive()
if message['type'] == 'lifespan.startup':
# ์์ ์ ์คํ
print("๐ ์ ํ๋ฆฌ์ผ์ด์
์์")
# DB ์ฐ๊ฒฐ, ์บ์ ์ด๊ธฐํ ๋ฑ
await send({'type': 'lifespan.startup.complete'})
elif message['type'] == 'lifespan.shutdown':
# ์ข
๋ฃ ์ ์คํ
print("๐ ์ ํ๋ฆฌ์ผ์ด์
์ข
๋ฃ")
# DB ์ฐ๊ฒฐ ํด์ , ๋ฆฌ์์ค ์ ๋ฆฌ ๋ฑ
await send({'type': 'lifespan.shutdown.complete'})
return
elif scope['type'] == 'http':
await send({
'type': 'http.response.start',
'status': 200,
'headers': [[b'content-type', b'text/plain']],
})
await send({
'type': 'http.response.body',
'body': b'App is running',
})
โง Server-Sent Events (SSE)
- Server-Sent Events(์๋ฒ ์ ์ก ์ด๋ฒคํธ)๋ ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก์ ๋จ๋ฐฉํฅ ์ค์๊ฐ ๋ฐ์ดํฐ ์ ์ก ๊ธฐ์ ์ด๋ค. ์ค์๊ฐ ์๋ฆผ, ์ฃผ์ ์ ๋ณด, ๋ด์ค ํผ๋ ๋ฑ์ ํ์ฉ๋๋ค.
import asyncio
async def sse_app(scope, receive, send):
"""Server-Sent Events ์คํธ๋ฆฌ๋ฐ"""
if scope['type'] == 'http' and scope['path'] == '/events':
await send({
'type': 'http.response.start',
'status': 200,
'headers': [
[b'content-type', b'text/event-stream'],
[b'cache-control', b'no-cache'],
[b'connection', b'keep-alive'],
],
})
# ์ฃผ๊ธฐ์ ์ผ๋ก ์ด๋ฒคํธ ์ ์ก
for i in range(10):
data = f"data: Message {i}\n\n"
await send({
'type': 'http.response.body',
'body': data.encode('utf-8'),
'more_body': True,
})
await asyncio.sleep(1)
# ์คํธ๋ฆผ ์ข
๋ฃ
await send({
'type': 'http.response.body',
'body': b'',
})
else:
# SSE ํด๋ผ์ด์ธํธ HTML
html = """
<!DOCTYPE html>
<html>
<head><title>SSE</title></head>
<body>
<h1>Server-Sent Events</h1>
<div id="events"></div>
<script>
const eventSource = new EventSource('/events');
eventSource.onmessage = (event) => {
const div = document.createElement('div');
div.textContent = event.data;
document.getElementById('events').appendChild(div);
};
</script>
</body>
</html>
"""
await send({
'type': 'http.response.start',
'status': 200,
'headers': [[b'content-type', b'text/html']],
})
await send({
'type': 'http.response.body',
'body': html.encode('utf-8'),
})
โจ ASGI vs WSGI ๋น๊ต
# WSGI (๋๊ธฐ)
def wsgi_app(environ, start_response):
"""์ ํต์ ์ธ WSGI ์ ํ๋ฆฌ์ผ์ด์
"""
status = '200 OK'
headers = [('Content-Type', 'text/plain')]
start_response(status, headers)
return [b'Hello, WSGI!']
# ASGI (๋น๋๊ธฐ)
async def asgi_app(scope, receive, send):
"""๋น๋๊ธฐ ASGI ์ ํ๋ฆฌ์ผ์ด์
"""
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!',
})
# ASGI์ ์ฅ์ :
# 1. ๋น๋๊ธฐ ์ฒ๋ฆฌ ๊ฐ๋ฅ
# 2. WebSocket, HTTP/2 ์ง์
# 3. ์ฅ์๊ฐ ์ฐ๊ฒฐ ์ง์
# 4. ๋ ๋์ ๋์์ฑ
โฉ FastAPI๊ฐ ASGI๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ์
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello"}
# FastAPI๋ ๋ด๋ถ์ ์ผ๋ก ASGI ํธ์ถ ๊ฐ๋ฅ ๊ฐ์ฒด๋ก ์๋
# app(scope, receive, send) ํํ๋ก ํธ์ถ๋จ
# Uvicorn์ด ์ด ASGI ์ธํฐํ์ด์ค๋ฅผ ํตํด FastAPI์ ํต์
4. ASGI ์๋ฒ ์์
- Uvicorn: ๊ฐ์ฅ ๋น ๋ฅธ ASGI ์๋ฒ
- Daphne: Django Channels ๊ฐ๋ฐํ ์ ์
- Hypercorn: HTTP/2 ์ง์
๋ฐ์ํ
'๐ฅ๏ธ Web Programming > FastAPI' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| [FastAPI] FastAPI ๊ฐ๋ ๋ฟ์๊ธฐ: OpenAPI (Swagger) (0) | 2025.12.27 |
|---|---|
| [FastAPI] FastAPI ๊ฐ๋ ๋ฟ์๊ธฐ: REST API (0) | 2025.12.26 |
| [FastAPI] FastAPI ๊ฐ๋ ๋ฟ์๊ธฐ: SQLAlchemy (0) | 2025.12.24 |
| [FastAPI] FastAPI ๊ฐ๋ ๋ฟ์๊ธฐ: Uvicorn (0) | 2025.12.24 |
| [FastAPI] FastAPI ๊ฐ๋ ๋ฟ์๊ธฐ: Starlette (0) | 2025.12.24 |
๋ธ๋ก๊ทธ์ ์ ๋ณด
Anything Everything
GomSon-E