ANYTHING EVERYTHING

[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 ์ง€์›
๋ฐ˜์‘ํ˜•

๋ธ”๋กœ๊ทธ์˜ ์ •๋ณด

Anything Everything

GomSon-E

ํ™œ๋™ํ•˜๊ธฐ