장고 고급 #1 Async views와 ASGI

7 분 소요

장고 기초 7편 + 중급 7편을 마쳤다면, 이제 고급으로 들어갑니다. 고급 7편은 대규모/장기 운영 장고 프로젝트에서 마주치는 주제들 — 비동기, 커맨드, 쿼리 최적화, 캐싱, 시그널 깊이, WebSocket, 배포 보안 — 을 다룹니다.

기초 #4 FBV중급 #1 CBV의 view 위에 **async def**가 더해집니다. 장고는 4.1부터 ORM까지 본격 비동기를 지원하기 시작했고, 5.x에서 자리를 잡았습니다.

WSGI vs ASGI #

장고는 오랫동안 WSGI (Web Server Gateway Interface) 위에서 살았습니다. 동기 함수 한 번 호출이 한 요청을 처리하는 모델입니다. 동시성은 워커 프로세스/스레드를 늘려 잡습니다.

ASGI (Asynchronous Server Gateway Interface)는 그 후속 표준입니다. 비동기 함수WebSocket / HTTP/2 같은 장수명 연결을 다룰 수 있습니다.

WSGIASGI
함수 형태def app(environ, start_response)async def app(scope, receive, send)
비동기불가네이티브
프로토콜HTTP만HTTP, WebSocket, HTTP/2
서버gunicorn, uWSGIuvicorn, daphne, hypercorn
동시성 모델프로세스/스레드이벤트 루프 + 코루틴

장고는 둘 다 지원합니다. wsgi.py, asgi.py 두 진입점이 같이 있습니다.

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()

ASGI 진입점을 쓰면 동기 view도 비동기 view도 같이 동작합니다. 즉 마이그레이션은 점진적으로 가능합니다.

첫 async view #

myapp/views.py
import asyncio
from django.http import JsonResponse

async def hello(request):
    await asyncio.sleep(0.1)
    return JsonResponse({"hello": "async"})

defasync def로 바뀐 게 전부입니다. 라우팅은 같습니다.

myapp/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path("hello/", views.hello),
]

장고가 view를 호출할 때 inspect.iscoroutinefunction으로 자동 감지해서, 비동기면 이벤트 루프에서 실행합니다.

언제 가치가 있는가 #

비동기는 무료가 아닙니다. 단순 ORM 한 번 + 응답 한 번이면 동기 view가 더 간단하고 빠릅니다. 비동기가 가치 있는 경우:

  • 외부 HTTP API 여러 개를 동시에 호출
  • WebSocket 같은 장수명 연결 (#6)
  • 외부 IO가 view의 대부분을 차지
  • 같은 프로세스에서 수많은 동시 연결을 다뤄야 할 때

순수 CPU 작업, 단순 CRUD는 비동기로 옮겨도 이득이 거의 없습니다. 상황에 맞게 선택하세요.

ORM 비동기 — a 시리즈 #

장고 ORM의 모든 쿼리 메소드는 a 접두 비동기 버전을 가지고 있습니다.

동기비동기
Post.objects.get(id=1)await Post.objects.aget(id=1)
post.save()await post.asave()
post.delete()await post.adelete()
Post.objects.create(...)await Post.objects.acreate(...)
Post.objects.filter(...).update(...)await Post.objects.filter(...).aupdate(...)
for p in Post.objects.all()async for p in Post.objects.all()
list(qs)[p async for p in qs]
비동기 view 안에서 ORM
from django.http import JsonResponse
from .models import Post

async def post_list(request):
    posts = [
        {"id": p.id, "title": p.title}
        async for p in Post.objects.filter(published=True)[:20]
    ]
    return JsonResponse({"posts": posts})

async def post_detail(request, pk):
    post = await Post.objects.aget(pk=pk)
    return JsonResponse({"id": post.id, "title": post.title})

async for와 async comprehension이 핵심 패턴입니다.

⚠ 관계 접근은 여전히 동기 #

🚫 비동기 view 안에서 관계 lazy
async def post_detail(request, pk):
    post = await Post.objects.aget(pk=pk)
    author_name = post.author.name   # SynchronousOnlyOperation

ORM 객체의 외래키 lazy load는 동기 코드입니다. 비동기 컨텍스트에서 그대로 호출하면 SynchronousOnlyOperation 예외가 나옵니다.

해결:

✅ select_related로 미리
async def post_detail(request, pk):
    post = await Post.objects.select_related("author").aget(pk=pk)
    return JsonResponse({"id": post.id, "author": post.author.name})

미리 JOIN으로 가져오면 lazy load가 일어나지 않습니다. #3select_related / prefetch_related가 비동기에서는 거의 필수입니다.

외부 API 동시 호출 — async의 진가 #

비동기 view가 빛나는 경우입니다. 외부 서비스 셋을 동시에 부르는 페이지를 보겠습니다.

설치
pip install httpx
동시 호출
import asyncio
import httpx
from django.http import JsonResponse

async def dashboard(request):
    async with httpx.AsyncClient(timeout=5.0) as client:
        weather, news, stocks = await asyncio.gather(
            client.get("https://api.weather.example/now"),
            client.get("https://api.news.example/top"),
            client.get("https://api.stocks.example/quote/AAPL"),
        )

    return JsonResponse({
        "weather": weather.json(),
        "news": news.json(),
        "stocks": stocks.json(),
    })

세 호출이 순차 합산이 아니라 가장 느린 것 한 개의 시간 만에 끝납니다. 동기 view + requests로 같은 일을 하면 세 시간을 더한 만큼 걸립니다.

sync_to_async / async_to_sync #

장고 진영의 비동기 어댑터로, asgiref.sync에서 옵니다.

sync_to_async — 동기 함수를 비동기에서 호출 #

비동기 view에서 동기 코드 호출
from asgiref.sync import sync_to_async
from .legacy import compute_report   # 동기 함수

async def report(request):
    data = await sync_to_async(compute_report)(user_id=request.user.id)
    return JsonResponse(data)

기본은 별도 스레드에서 실행돼서 이벤트 루프를 막지 않습니다. ORM의 a 메소드도 내부적으로 이걸 씁니다.

thread_sensitive 옵션
@sync_to_async(thread_sensitive=True)
def db_work():
    ...

thread_sensitive=True항상 같은 스레드에서 실행합니다. 동기 ORM 트랜잭션 같이 스레드 로컬 상태를 가진 코드에 필요합니다. 기본값이 True입니다.

async_to_sync — 비동기 함수를 동기에서 호출 #

동기 view에서 비동기 코드 호출
from asgiref.sync import async_to_sync

def my_sync_view(request):
    result = async_to_sync(fetch_data_async)(request.GET["q"])
    return JsonResponse(result)

매번 새 이벤트 루프를 만들고 닫으니 자주 부르면 비쌉니다. 진짜 필요한 경우에만.

Async middleware #

미들웨어도 비동기 버전을 만들 수 있습니다.

async middleware
class TimingMiddleware:
    async_capable = True
    sync_capable = False

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

    async def __call__(self, request):
        import time
        start = time.perf_counter()
        response = await self.get_response(request)
        elapsed = (time.perf_counter() - start) * 1000
        response["X-Elapsed-Ms"] = f"{elapsed:.1f}"
        return response

플래그 두 개:

  • async_capable = True — 비동기 컨텍스트에서 동작 가능
  • sync_capable = False — 동기는 지원 안 함

둘 다 True 인 미들웨어는 장고가 자동으로 적절한 모드로 호출합니다. 다만 동기/비동기 경계가 생기면 어댑터가 끼어들어 비용이 발생합니다. 가능하면 한 모드로 통일.

ASGI 서버 #

비동기 view를 띄우려면 ASGI 서버가 필요합니다. 셋이 표준입니다.

uvicorndaphnehypercorn
메인테이너encodedjangopgjones
HTTP/2지원미지원지원
WebSocket지원Channels 표준지원
속도가장 빠름보통보통
추천 용도일반 ASGIChannels (#6)HTTP/2 필요 시

uvicorn #

설치 / 실행
pip install "uvicorn[standard]"

uvicorn myproject.asgi:application --host 0.0.0.0 --port 8000 --workers 4

프로덕션은 gunicorn + uvicorn worker 조합이 흔합니다.

gunicorn + uvicorn worker
pip install gunicorn

gunicorn myproject.asgi:application \
  -k uvicorn.workers.UvicornWorker \
  --workers 4 --bind 0.0.0.0:8000

gunicorn이 프로세스 관리 (재시작, 시그널, 로깅)를 맡고, uvicorn이 실제 ASGI 처리를 합니다.

daphne #

daphne
pip install daphne

daphne -b 0.0.0.0 -p 8000 myproject.asgi:application

Channels (#6)의 표준 서버.

동기 view와 비동기 view의 공존 #

같은 프로젝트에 둘이 섞여 있어도 됩니다. 장고가 자동 감지.

urls.py — 섞어 쓰기
urlpatterns = [
    path("classic/", views.classic_view),     # def
    path("modern/", views.modern_view),        # async def
]

ASGI 서버로 띄우면 둘 다 정상 동작. 동기 view는 스레드 풀에서 실행됩니다 (이벤트 루프를 막지 않게).

WSGI 서버(gunicorn 일반 worker, uWSGI)로 띄우면 비동기 view는 매 요청마다 async_to_sync로 실행되니 손해가 큽니다. 비동기 view가 있다면 ASGI 서버로.

FastAPI와의 비교 #

모던 파이썬 실전 시리즈의 FastAPI와 자주 비교됩니다.

Django (async)FastAPI
시작점풀스택 (admin, ORM, 인증 다 옴)마이크로
비동기부분/혼합 (ORM 일부 동기)처음부터 비동기
타입 힌트보조적핵심 동작
OpenAPIDRF + 별도빌트인
학습 곡선가파름평탄
용도풀 사이트, admin 활용순수 API

장고를 처음부터 비동기로 짜는 새 프로젝트는 흔치 않습니다. 보통은 기존 동기 장고에 일부 view만 비동기로 도입합니다 (외부 API, WebSocket). 처음부터 100% 비동기 API 라면 FastAPI가 더 잘 맞습니다.

자주 만나는 함정 #

1) SynchronousOnlyOperation #

비동기 view 안에서 동기 ORM 호출을 하면 납니다. a 메소드 또는 **sync_to_async**로 감싸야 합니다.

🚫
async def my_view(request):
    post = Post.objects.get(pk=1)   # 동기 호출
async def my_view(request):
    post = await Post.objects.aget(pk=1)

2) Lazy 관계 접근 #

위에서 본 그것. **select_related/prefetch_related**로 미리.

3) ORM 트랜잭션은 아직 동기 #

@transaction.atomic 데코레이터는 동기 함수에만 사용 가능합니다. 비동기 view에서 트랜잭션이 필요하면 동기 함수로 묶어 sync_to_async로 호출하는 패턴을 씁니다.

비동기 view의 트랜잭션
from asgiref.sync import sync_to_async
from django.db import transaction

@sync_to_async
def create_with_tx(data):
    with transaction.atomic():
        post = Post.objects.create(**data)
        Tag.objects.create(post=post, name=data["tag"])
        return post

async def my_view(request):
    post = await create_with_tx({"title": "...", "tag": "..."})

장고 6.x에서 비동기 트랜잭션이 정식 지원될 예정입니다. 그 전까지는 이 패턴이 권장.

4) 미들웨어 경계의 어댑터 비용 #

요청 처리 중 동기→비동기 경계가 여러 번 생기면 그때마다 어댑터(sync_to_async/async_to_sync)가 끼어들어 비용이 누적됩니다. 미들웨어 체인은 가능하면 한 모드로.

정리 #

이번 글에서 잡은 것:

  • WSGI는 동기, ASGI는 비동기 + WebSocket/HTTP2
  • async def view(request) — 4.1부터 본격
  • ORM의 a 메소드 — aget, acreate, aupdate, async for
  • 관계 접근은 여전히 동기 — select_related로 미리
  • 외부 API 동시 호출 (asyncio.gather)이 비동기의 진가
  • sync_to_async, async_to_sync로 경계 넘기
  • async middleware — async_capable/sync_capable 플래그
  • ASGI 서버 — uvicorn (일반), daphne (Channels), hypercorn (HTTP/2)
  • 동기/비동기 view 공존 가능, ASGI 서버로 띄우기
  • 비동기는 외부 IO가 많은 경우에 가치, 단순 CRUD는 동기가 낫다
  • @transaction.atomic은 아직 동기 — sync_to_async로 감싸기

다음 글(#2 Custom management commands)에서는 운영의 또 다른 한 축 — 커맨드라인 작업을 다룹니다. 만료 토큰 청소, 일괄 통계 계산, 데이터 마이그레이션 등 cron으로 굴리는 작업을 장고답게 만드는 법.

X