장고 고급 #1 Async views와 ASGI
장고 기초 7편 + 중급 7편을 마쳤다면, 이제 고급으로 들어갑니다. 고급 7편은 대규모/장기 운영 장고 프로젝트에서 마주치는 주제들 — 비동기, 커맨드, 쿼리 최적화, 캐싱, 시그널 깊이, WebSocket, 배포 보안 — 을 다룹니다.
- #1 Async views와 ASGI ← 이번 글
- #2 Custom management commands
- #3 쿼리 최적화
- #4 캐싱
- #5 Signals 깊이와 트랜잭션 후 처리
- #6 Django Channels — WebSocket
- #7 배포 보안
기초 #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 같은 장수명 연결을 다룰 수 있습니다.
| WSGI | ASGI | |
|---|---|---|
| 함수 형태 | def app(environ, start_response) | async def app(scope, receive, send) |
| 비동기 | 불가 | 네이티브 |
| 프로토콜 | HTTP만 | HTTP, WebSocket, HTTP/2 |
| 서버 | gunicorn, uWSGI | uvicorn, daphne, hypercorn |
| 동시성 모델 | 프로세스/스레드 | 이벤트 루프 + 코루틴 |
장고는 둘 다 지원합니다. wsgi.py, 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 #
import asyncio
from django.http import JsonResponse
async def hello(request):
await asyncio.sleep(0.1)
return JsonResponse({"hello": "async"})def가 async def로 바뀐 게 전부입니다. 라우팅은 같습니다.
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] |
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이 핵심 패턴입니다.
⚠ 관계 접근은 여전히 동기 #
async def post_detail(request, pk):
post = await Post.objects.aget(pk=pk)
author_name = post.author.name # SynchronousOnlyOperationORM 객체의 외래키 lazy load는 동기 코드입니다. 비동기 컨텍스트에서 그대로 호출하면 SynchronousOnlyOperation 예외가 나옵니다.
해결:
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가 일어나지 않습니다. #3의 select_related / prefetch_related가 비동기에서는 거의 필수입니다.
외부 API 동시 호출 — async의 진가 #
비동기 view가 빛나는 경우입니다. 외부 서비스 셋을 동시에 부르는 페이지를 보겠습니다.
pip install httpximport 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 — 동기 함수를 비동기에서 호출
#
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 메소드도 내부적으로 이걸 씁니다.
@sync_to_async(thread_sensitive=True)
def db_work():
...thread_sensitive=True는 항상 같은 스레드에서 실행합니다. 동기 ORM 트랜잭션 같이 스레드 로컬 상태를 가진 코드에 필요합니다. 기본값이 True입니다.
async_to_sync — 비동기 함수를 동기에서 호출
#
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 #
미들웨어도 비동기 버전을 만들 수 있습니다.
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 서버가 필요합니다. 셋이 표준입니다.
| uvicorn | daphne | hypercorn | |
|---|---|---|---|
| 메인테이너 | encode | django | pgjones |
| HTTP/2 | 지원 | 미지원 | 지원 |
| WebSocket | 지원 | Channels 표준 | 지원 |
| 속도 | 가장 빠름 | 보통 | 보통 |
| 추천 용도 | 일반 ASGI | Channels (#6) | HTTP/2 필요 시 |
uvicorn #
pip install "uvicorn[standard]"
uvicorn myproject.asgi:application --host 0.0.0.0 --port 8000 --workers 4프로덕션은 gunicorn + uvicorn worker 조합이 흔합니다.
pip install gunicorn
gunicorn myproject.asgi:application \
-k uvicorn.workers.UvicornWorker \
--workers 4 --bind 0.0.0.0:8000gunicorn이 프로세스 관리 (재시작, 시그널, 로깅)를 맡고, uvicorn이 실제 ASGI 처리를 합니다.
daphne #
pip install daphne
daphne -b 0.0.0.0 -p 8000 myproject.asgi:applicationChannels (#6)의 표준 서버.
동기 view와 비동기 view의 공존 #
같은 프로젝트에 둘이 섞여 있어도 됩니다. 장고가 자동 감지.
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 일부 동기) | 처음부터 비동기 |
| 타입 힌트 | 보조적 | 핵심 동작 |
| OpenAPI | DRF + 별도 | 빌트인 |
| 학습 곡선 | 가파름 | 평탄 |
| 용도 | 풀 사이트, 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로 호출하는 패턴을 씁니다.
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으로 굴리는 작업을 장고답게 만드는 법.