본문으로 건너뛰기
김신건의 로그

[Django] async views와 ASGI

· 수정 · 📖 약 2분 · 717자/단어 #django #async #asgi #channels
django async view, async def view, ASGI, sync_to_async, async_to_sync, django channels

정의

Django 3.1+ 부터 async view를 지원한다. async def로 view를 정의하면 ASGI 서버(Uvicorn, Daphne, Hypercorn) 아래에서 코루틴으로 실행. I/O 바운드(외부 API, DB - 4.1+, 캐시) 작업에서 동시 처리량 향상. CPU 작업이나 동기 라이브러리엔 효과 X 또는 음수.

ORM은 5.0+에서 본격 비동기 (afilter, aget 등). 그 전엔 sync_to_async로 래핑.

async view 기본

import asyncio
from django.http import JsonResponse

async def async_view(request):
    await asyncio.sleep(1)
    return JsonResponse({"ok": True})

# 외부 HTTP
import httpx

async def fetch_data(request):
    async with httpx.AsyncClient() as client:
        r = await client.get("https://api.example.com/data")
    return JsonResponse(r.json())

URL conf에 평소처럼 등록. Django가 async view를 감지하면 적절히 실행.

동기 view와 async view 혼재

같은 프로젝트에 둘 다 가능. Django가 자동으로 양방향 변환:

  • 동기 view + 비동기 미들웨어 → 동기를 thread pool에서 실행
  • 비동기 view + 동기 미들웨어 → 미들웨어를 sync_to_async

자동 변환은 작동하지만 성능 손해. 가능한 한 일관 유지.

ORM의 async (4.1+, 5.0+)

# 5.0+
async def view(request):
    post = await Post.objects.aget(id=1)
    posts = [p async for p in Post.objects.filter(is_published=True)]
    count = await Post.objects.acount()
    return JsonResponse({"count": count})

비동기 메서드는 a 접두 (aget, afilter, acount, acreate, asave, adelete, aupdate, afirst, alast, aexists, aiterator).

5.0 이전에는 sync_to_async로 래핑.

sync_to_async / async_to_sync

from asgiref.sync import sync_to_async, async_to_sync

async def view(request):
    # 동기 함수를 비동기 컨텍스트에서
    posts = await sync_to_async(list)(Post.objects.all())

    # ORM 직접 호출 (5.0 이전)
    user = await sync_to_async(User.objects.get)(id=1)

def sync_view(request):
    # 비동기 함수를 동기에서
    result = async_to_sync(fetch)("url")
    return JsonResponse(result)

thread_sensitive=True(기본)는 같은 스레드 보장 (DB 트랜잭션 안전). False는 더 빠르지만 thread-local에 문제 생길 수 있음.

ASGI 서버

WSGI(Gunicorn 등)는 동기. 비동기 view를 활용하려면 ASGI 서버:

pip install uvicorn
uvicorn myproject.asgi:application --workers 4

또는 Daphne, Hypercorn, Granian.

# myproject/asgi.py
import os
from django.core.asgi import get_asgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
application = get_asgi_application()

gunicorn + uvicorn worker도 인기:

gunicorn myproject.asgi:application -k uvicorn.workers.UvicornWorker --workers 4

자주 보는 패턴

동시 외부 API 호출

import asyncio
import httpx

async def dashboard(request):
    async with httpx.AsyncClient() as client:
        users_resp, orders_resp, metrics_resp = await asyncio.gather(
            client.get("https://api1.example/users"),
            client.get("https://api2.example/orders"),
            client.get("https://api3.example/metrics"),
        )
    return JsonResponse({
        "users": users_resp.json(),
        "orders": orders_resp.json(),
        "metrics": metrics_resp.json(),
    })

세 외부 호출이 1초씩이면 동기는 3초, 비동기는 ~1초.

Streaming response

from django.http import StreamingHttpResponse
import asyncio

async def stream_view(request):
    async def gen():
        for i in range(10):
            yield f"data: {i}\n\n"
            await asyncio.sleep(1)

    return StreamingHttpResponse(gen(), content_type="text/event-stream")

SSE, 실시간 로그 스트림 등.

LLM/OpenAI 통합

async def chat(request):
    body = json.loads(request.body)
    response = StreamingHttpResponse(content_type="text/event-stream")

    async def stream():
        async with openai_client.responses.stream(...) as s:
            async for event in s:
                yield f"data: {event.text}\n\n"

    return StreamingHttpResponse(stream(), content_type="text/event-stream")

긴 LLM 응답을 토큰 단위로 흘려보냄.

백그라운드 작업 트리거

async def trigger(request):
    asyncio.create_task(background_job())    # fire-and-forget
    return JsonResponse({"started": True})

async def background_job():
    await asyncio.sleep(10)
    ...

단 ASGI 서버 종료 시 task가 잘릴 수 있어 프로덕션엔 Celery 등 권장.

함정

1. 동기 ORM in async view

async def view(request):
    posts = Post.objects.all()           # OK (lazy)
    list(posts)                           # WARN: sync 호출 in async context

    # 5.0+
    posts = [p async for p in Post.objects.all()]    # OK

SynchronousOnlyOperation 예외 발생할 수 있음. 명시적 sync_to_async 또는 a- 메서드.

2. 동기 라이브러리 사용

async def view(request):
    requests.get("...")    # 블로킹! 이벤트 루프 정지

asyncio 친화 라이브러리: httpx, aiohttp, aiomysql, asyncpg, redis.asyncio.

대체 어렵다면 sync_to_async(requests.get)("...") (스레드 풀로 위임).

3. 미들웨어 모두 async?

미들웨어가 동기/비동기 혼재면 변환 비용. 가능한 한 통일.

class AsyncMiddleware:
    sync_capable = True
    async_capable = True

    def __init__(self, get_response):
        self.get_response = get_response

    async def __call__(self, request):
        response = await self.get_response(request)
        return response

4. 트랜잭션과 sync_to_async

thread_sensitive=False로 다른 스레드에서 실행 시 트랜잭션 컨텍스트 누락. 기본 True 유지.

5. 캐시 백엔드

Django cache 백엔드 일부는 sync만. 비동기 코드 안에서 cache.get은 SynchronousOnlyOperation 가능.

acache.get (5.0+) 또는 sync_to_async.

성능 기대치

  • 외부 I/O 동시 호출: 큰 향상 (5-10x)
  • DB 단순 조회: 거의 동등 (5.0+에서 async ORM도 thread pool 위임)
  • CPU 작업: 향상 없음 (또는 단점)
  • 단일 동기 라이브러리 잠시 호출: 미미

async가 만능 아님. 측정 후 결정.

Django Channels

표준 async view는 WebSocket 지원 X. Channels로 확장.

pip install channels[daphne]
# settings.py
INSTALLED_APPS = ["daphne", ..., "channels"]
ASGI_APPLICATION = "myproject.asgi.application"

# asgi.py
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from myapp.routing import websocket_urlpatterns

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns)),
})

Consumer:

from channels.generic.websocket import AsyncWebsocketConsumer

class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        await self.accept()

    async def receive(self, text_data):
        await self.send(text_data=f"Echo: {text_data}")

자세한 건 django-channels 페이지.

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기