2026년 Django 비동기 뷰와 ASGI: 성능과 면접 질문

2026년 Django 비동기 뷰와 ASGI 심층 분석: 내부 동작 원리, 배포할 서버 선택, 비동기 ORM과 SynchronousOnlyOperation 함정, 그리고 면접 질문까지 다룹니다.

동시 요청을 처리하는 Django 비동기 뷰와 ASGI 아키텍처

Django의 비동기 뷰는 각 커넥션마다 thread를 하나씩 할당하지 않고도 단일 worker가 수백 개의 I/O 바운드 요청을 동시에 처리할 수 있게 합니다. 다만 뷰에서 모든 네트워크 호출에 이르기까지 코드 경로가 계속 비동기로 유지될 때만 그렇습니다. Django 3.1이 async def 뷰를 도입하고 Django 4.1이 비동기 ORM 인터페이스를 추가한 이후로, 이 프레임워크는 신뢰할 만한 ASGI 플랫폼으로 자리 잡았습니다. 그럼에도 대부분의 프로덕션 배포는 여전히 WSGI에서 실행되며 그 동시성을 그대로 방치합니다. 이 글은 비동기 뷰가 ASGI에서 어떻게 동작하는지, 2026년에 어떤 서버를 운영해야 하는지, SynchronousOnlyOperation을 발생시키는 ORM 함정은 무엇인지, 그리고 비동기 Django를 실제로 출시해 본 엔지니어와 글로만 접한 엔지니어를 가르는 면접 질문은 무엇인지 설명합니다.

비동기가 실제로 도움이 되는 경우

비동기 뷰는 요청이 대부분의 시간을 스스로 제어할 수 없는 I/O를 기다리는 데 쓸 때 효과를 냅니다. 서드파티 HTTP API, 느린 다운스트림 서비스, 오래 유지되는 스트리밍 응답 등이 그런 경우입니다. CPU 바운드 작업이나 빠른 로컬 데이터베이스 쿼리에는 Gunicorn worker를 더 투입한 동기 뷰가 보통 더 빠르고 추론하기도 훨씬 간단합니다.

Django 비동기 뷰가 ASGI에서 동작하는 방식

WSGI는 근본적으로 동기 방식입니다. 처리 중인 각 요청이 시작부터 끝까지 worker thread나 프로세스를 하나씩 점유하므로, 느린 요청 50개를 처리하려면 worker 50개가 필요합니다. ASGI는 이 모델을 event loop로 대체합니다. event loop는 단일 worker에서 여러 요청을 번갈아 처리하며, 코루틴이 I/O를 await할 때마다 그 코루틴을 일시 중단했다가 데이터가 도착하면 재개합니다. Django 공식 비동기 문서는 이 공존을 가능하게 하는 두 개의 어댑터를 설명합니다. ASGI에서 Django는 async def 뷰를 loop 위에서 네이티브로 실행하고 동기 뷰는 sync_to_async로 threadpool에 밀어 넣습니다. WSGI에서는 비동기 뷰를 async_to_sync로 감싸 완료될 때까지 실행하며, 모든 동시성 이점을 버립니다.

실질적인 결과는 냉정합니다. WSGI 서버에 배포된 async def 뷰는 더 느린 동기 뷰처럼 동작합니다. 동시성은 오직 ASGI 서버에서만 실현됩니다. 외부 API를 호출하는 최소한의 비동기 뷰는 동기 버전과 거의 똑같아 보이며, 차이는 await에 집약되어 있습니다.

python
# views.py
import httpx
from django.http import JsonResponse

async def dashboard(request):
    # ASGI에서 이 코루틴은 event loop 위에서 실행되므로, httpx가
    # 네트워크 왕복을 await하는 동안 worker는 다른 요청을 자유롭게 처리합니다.
    async with httpx.AsyncClient(timeout=5.0) as client:
        response = await client.get("https://api.example.com/metrics")
    return JsonResponse(response.json())

여기서 얻는 이득은 단일 요청이 더 빨리 끝나는 것이 아닙니다. 벽시계 시간(wall-clock time)은 동일합니다. 이득은 await가 진행되는 동안 worker가 차단되지 않는다는 데 있습니다. 그래서 프로세스 수는 그대로인데도 동시 부하에서의 처리량(throughput)이 올라갑니다.

같은 모델은 WSGI에서는 다루기 껄끄러운 응답 패턴도 열어 줍니다. Django 4.2부터 StreamingHttpResponse는 async generator를 받으므로, 뷰는 업스트림 소스가 데이터를 생성하는 대로 청크를 내보낼 수 있습니다. 이는 커넥션이 수 초간 열려 있는 Server-Sent Events나 프록시된 LLM 토큰 스트림에 적합합니다. WebSocket 같은 지속적 양방향 프로토콜은 여전히 일반 HTTP 뷰가 아니라 Django Channels를 거치지만, 둘 다 동일한 ASGI 기반 위에 놓여 있습니다. 따라서 비동기 뷰를 채택한 프로젝트는 나중에 실시간 기능을 위한 인프라 비용을 이미 치른 셈입니다.

2026년 Django ASGI 서버 선택

Django는 asgi.py에 ASGI 애플리케이션 객체를 제공하지만, 그 자체로 실행되지는 않습니다. 전용 ASGI 서버가 event loop를 구동합니다. ASGI 명세는 이 서버들이 모두 구현하는 인터페이스를 정의하며, 그래서 이들은 프로토콜 수준에서 서로 교체 가능하고 주로 성능과 프로토콜 커버리지에서만 차이가 납니다.

서버구현 언어가장 적합한 용도
UvicornPython (uvloop)일반적인 ASGI, HTTP/1.1, 흔한 기본 선택
DaphnePythonDjango Channels, WebSocket, HTTP/2
GranianRust최고 수준의 순수 처리량, HTTP/1과 HTTP/2
HypercornPythonHTTP/2와 HTTP/3(QUIC)

2026년의 대부분의 배포에서는 Gunicorn worker 클래스로 실행하는 Uvicorn이 여전히 믿을 만한 선택입니다. Gunicorn이 프로세스 수명 주기를 관리하고 죽은 worker를 재시작하며 시그널을 처리하는 동안, Uvicorn은 각 worker 안에서 event loop를 구동합니다. 업그레이드에서 걸려 넘어지기 쉬운 마이그레이션 세부 사항이 하나 있습니다. Uvicorn 0.30부터 Gunicorn worker 클래스가 Uvicorn 코어에서 별도의 uvicorn-worker 패키지로 옮겨져 import 경로가 바뀌었습니다.

bash
# 먼저 worker 패키지를 설치합니다: pip install uvicorn-worker
gunicorn myproject.asgi:application \
    --workers 4 \
    --worker-class uvicorn_worker.UvicornWorker \
    --bind 0.0.0.0:8000

최대 처리량을 추구하는 팀은 점점 더 Granian으로 눈을 돌립니다. Granian은 Python 쪽 HTTP 파싱 오버헤드를 제거하는 Rust 기반 서버입니다. 어느 쪽을 선택하든, 이 배포 과정이야말로 비동기가 의도적으로 구성했을 때만 프로덕션에 나타나는 이유입니다. ASGI 전용 설정은 Django 배포 면접 질문 모듈에서 다루는 운영상의 주의와 맥을 같이합니다.

비동기 ORM과 SynchronousOnlyOperation 함정

Django 4.1은 ORM 전반에 비동기 쿼리 메서드를 추가했습니다. aget, acreate, asave, adelete, acount, aexists, aaggregate, 그리고 async for를 통한 순회가 그것입니다. 이들은 비동기 친화적인 표면을 제공하지만, 2026년까지 남아 있는 결정적인 단서가 하나 있습니다. 그 아래의 데이터베이스 계층은 네이티브 비동기가 아니라는 점입니다. psycopg3를 쓰더라도 Django는 쿼리를 threadpool에서 실행하고 이를 async 래퍼로 노출합니다. 그래서 비동기 ORM은 진정한 종단 간(end-to-end) 비동기 데이터베이스 I/O를 제공하지는 못한 채, 편의성을 높이고 loop 차단을 피하게 해 줍니다.

가장 흔한 실패 유형은 비동기 뷰 안에서 동기 ORM 메서드를 직접 호출하는 것입니다. Django는 비동기 컨텍스트를 감지하고 이를 거부하며 SynchronousOnlyOperation을 발생시킵니다. 차단성 호출이 그 worker를 공유하는 다른 모든 요청을 위해 event loop를 얼려 버리는 것을 막기 위해서입니다.

SynchronousOnlyOperation은 버그가 아니라 기능

이 예외는 버그가 아니라 가드레일입니다. 이를 잠재우려고 DJANGO_ALLOW_ASYNC_UNSAFE=true에 손을 대면, 비동기가 없애려던 바로 그 차단 동작을 다시 끌어들이게 됩니다. 올바른 해결책은 비동기 ORM 메서드(get 대신 aget)를 쓰거나, 비동기 대응물이 없는 코드를 sync_to_async로 명시적으로 감싸는 것입니다.

비동기 쿼리 메서드가 동기 쌍둥이를 대체하고 나면 비동기 뷰는 깔끔하게 읽힙니다. 아래 패턴은 event loop를 한 번도 벗어나지 않고 행 수를 세고 처음 열 개의 레코드를 스트리밍합니다.

python
# views.py
from django.http import JsonResponse
from .models import Article

async def latest_articles(request):
    # acount()와 async 순회는 ORM을 감싼 async 안전 래퍼입니다.
    count = await Article.objects.acount()
    titles = []
    async for article in Article.objects.order_by("-created_at")[:10]:
        titles.append(article.title)
    return JsonResponse({"count": count, "titles": titles})

어떤 코드에는 비동기 대응물이 없습니다. 동기 호출만 제공하는 서드파티 SDK나, 다시 작성할 가치가 없는 촘촘한 ORM 순회 체인이 그렇습니다. 이를 asgiref 라이브러리sync_to_async로 감싸면 작업이 thread로 넘어가고 loop는 계속 응답성을 유지합니다. thread_sensitive=True 인자가 안전한 기본값인 이유는, 감싼 모든 호출을 하나의 공유 executor thread로 라우팅하여, 호출이 임의의 thread로 흩어지면 깨질 데이터베이스 커넥션과 트랜잭션 상태를 보존하기 때문입니다.

python
# views.py
from asgiref.sync import sync_to_async
from .services import build_report  # 동기 함수

async def report_view(request):
    # thread_sensitive=True는 감싼 호출을 하나의 공유 thread에 유지하여,
    # 경계를 넘나드는 동안 커넥션과 트랜잭션 상태를 보존합니다.
    report = await sync_to_async(build_report, thread_sensitive=True)(request.user.id)
    return JsonResponse(report)

Django 비동기 성능: 동시성이 이기는 순간

Django 비동기 성능은 순수 속도가 아니라 I/O 중첩에 관한 이야기입니다. 가장 분명한 이득은 asyncio.gather로 독립적인 네트워크 호출을 팬아웃할 때 나옵니다. 각각 200ms인 외부 요청 세 개는 순차 실행하면 대략 600ms가 들지만, 동시 실행하면 약 200ms 만에 완료됩니다. await들이 같은 loop 위에서 중첩되기 때문입니다.

python
# views.py
import asyncio
import httpx
from django.http import JsonResponse

async def aggregate(request):
    async with httpx.AsyncClient(timeout=5.0) as client:
        # 독립적인 세 호출을 하나씩 차례로가 아니라 동시에 실행합니다.
        weather, prices, news = await asyncio.gather(
            client.get("https://api.example.com/weather"),
            client.get("https://api.example.com/prices"),
            client.get("https://api.example.com/news"),
        )
    return JsonResponse({
        "weather": weather.json(),
        "prices": prices.json(),
        "news": news.json(),
    })

반대 경우도 그만큼 중요합니다. 빠른 로컬 쿼리 하나만 실행하는 뷰는 비동기에서 아무것도 얻지 못하며 오히려 손해를 보는 경우가 많습니다. 이제 모든 요청이 event loop 스케줄링에 더해 동기 데이터베이스 드라이버로 들어가는 threadpool 홉 비용까지 치르기 때문입니다. 비동기는 또한 모든 sync/async 경계에서 더 미묘한 세금을 물립니다. 동기 middleware와 비동기 middleware를 섞으면 Django는 전환할 때마다 loop와 thread 사이를 오가야 하고, 응답 한 번에 그 경계를 여러 번 넘나드는 요청은 실질적인 지연(latency)을 쌓게 됩니다. middleware 스택을 전부 비동기로, 또는 전부 동기로 유지하면 이러한 전환이 사라집니다.

이 결정은 최신 문법에 대한 선호가 아니라 워크로드의 형태로 귀결됩니다.

워크로드더 나은 기본 선택
요청당 여러 개의 느린 외부 API 호출asyncio.gather를 쓰는 비동기 뷰
오래 유지되는 스트리밍 또는 Server-Sent Eventsasync generator를 쓰는 비동기 뷰
빠른 데이터베이스 쿼리 하나, 가벼운 CPU 부하동기 뷰, Gunicorn worker 증설
CPU 바운드 렌더링 또는 연산동기 뷰, 태스크 큐로 오프로드

진짜 오래 걸리는 작업에는 비동기 뷰가 전적으로 잘못된 도구입니다. 몇 분짜리 작업을 하려고 요청을 열어 두는 것은 동시성 모델과 무관하게 커넥션을 낭비합니다. 그런 작업은 백그라운드 worker의 몫이며, 이 트레이드오프는 Django의 Celery 비동기 태스크 가이드에서 다룹니다.

도입은 직관이 아니라 측정을 따라야 합니다. 비동기를 정당화하는 정직한 방법은 Locust나 k6 같은 도구로 현실적인 동시성 아래 특정 엔드포인트를 부하 테스트하여, N개의 worker 위 동기 버전과 동일한 하드웨어 위 비동기 버전을 비교하는 것입니다. 지연이 큰 단일 외부 호출이 지배하는 엔드포인트는 비동기 쪽에 유리하게 큰 격차를 보입니다. 반면 빠른 쿼리가 지배하는 엔드포인트는 threadpool 홉까지 계산하고 나면 비동기가 근소하게 뒤처지는 경우가 잦습니다. 요청을 먼저 프로파일링하면 병목이 정말 I/O인지도 드러납니다. 인덱스 없는 쿼리 때문에 느린 뷰에는 비동기가 아무런 도움이 되지 않는데, 이 문제는 Django ORM 쿼리 최적화 안내에서 다룹니다.

Django 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

2026년 Django 비동기 면접 질문

Django 비동기 면접은 지원자가 모델을 이해하는지 아니면 키워드만 알아보는지를 파고듭니다. 아래 질문들은 2026년 시니어 면접에서 실제로 묻는 내용을 반영하며, 각 질문에는 경험 많은 엔지니어가 내놓는 답을 함께 실었습니다.

비동기 뷰가 ASGI가 아니라 WSGI에서 실행되면 무엇이 달라지나요? WSGI에서 Django는 코루틴을 async_to_sync로 감싸 요청 thread에서 완료될 때까지 실행합니다. 그래서 동시성 이점이 전혀 없는 더 느린 동기 뷰처럼 동작합니다. event loop를 구동하는 ASGI 서버만이 await 동안 worker가 다른 요청을 처리하게 해 줍니다.

비동기 뷰 안에서 Article.objects.get(pk=1)은 왜 SynchronousOnlyOperation을 발생시키나요? 동기 ORM 호출은 event loop를 차단하여 그 worker의 다른 모든 요청을 멈춰 세우기 때문입니다. Django는 비동기 컨텍스트를 감지하고 이를 거부합니다. 해결책은 비동기 메서드 await Article.objects.aget(pk=1)이거나, 비동기 대응물이 없는 코드에는 sync_to_async입니다.

Django의 비동기 ORM은 정말로 논블로킹인가요? 아닙니다. 비동기 메서드는 편의를 위한 래퍼입니다. 데이터베이스 드라이버가 종단 간 네이티브 비동기 실행에 통합되어 있지 않기 때문에, 그 아래의 쿼리는 여전히 threadpool에서 실행됩니다. 이점은 loop를 자유롭게 유지하는 것이지, 드라이버 수준의 병렬 데이터베이스 I/O가 아닙니다.

Celery 같은 태스크 큐 대신 비동기 뷰를 골라야 하는 때는 언제인가요? 비동기 뷰는 응답 전에 반드시 끝나야 하는 요청 범위의 I/O 동시성에 적합합니다. 여러 API를 집계하는 경우가 그렇습니다. 요청보다 오래 사는 작업, 재시도가 필요한 작업, 몇 분간 실행되는 작업은 Celery나 Django의 백그라운드 태스크의 몫입니다. 그런 작업을 위해 HTTP 커넥션을 열어 두는 것은 서버 자원을 낭비하기 때문입니다.

동기 middleware와 비동기 middleware를 섞는 비용은 무엇인가요? 동기 컴포넌트와 비동기 컴포넌트 사이의 각 전환은 Django가 sync_to_asyncasync_to_sync를 통해 event loop와 thread 사이를 오가게 만듭니다. 그 경계를 반복해서 넘나드는 요청은 누적 오버헤드를 치르므로, 뒤섞인 스택보다 균일한 middleware 스택이 더 나은 성능을 냅니다.

결론

  • 비동기 뷰는 ASGI 서버에서만 동시성을 제공합니다. 같은 뷰가 WSGI에서는 async_to_sync를 거쳐 동기로 실행됩니다.
  • Uvicorn은 Gunicorn 아래에서 uvicorn-worker 패키지로 배포하고, 최대 처리량이 필요하면 Granian을 배포하며, ASGI 설정은 명시적인 배포 단계로 다뤄야 합니다.
  • 요청이 여러 개의 느린 외부 I/O 호출을 기다릴 때 비동기 뷰를 선택하고, 빠른 쿼리나 CPU 바운드 엔드포인트는 worker를 늘려 동기로 유지하는 것이 좋습니다.
  • 비동기 뷰 안에서는 aget, acreate, async for를 사용하되, ORM이 여전히 네이티브 비동기가 아니라 threadpool에서 쿼리를 실행한다는 점을 기억해야 합니다.
  • SynchronousOnlyOperation은 가드레일로 취급해, 검사를 비활성화하지 말고 비동기 메서드나 sync_to_async(thread_sensitive=True)로 해결해야 합니다.
  • 경계를 넘을 때마다 thread 전환 페널티를 물지 않으려면 middleware 스택을 전부 동기로 또는 전부 비동기로 유지해야 합니다.

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

오늘의 챌린지

Django 코드의 버그를 찾을 수 있나요

실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

Anthony Fillion-Maillet

작성자

Anthony Fillion-Maillet

SharpSkill 창업자

10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.

2026년 7월 7일 업데이트

태그

#django
#async
#asgi
#python
#performance

공유

관련 기사