Django 4.x에서 비동기 기능을 활용하여 확장 가능한 백엔드 구축하기
Min-jun Kim
Dev Intern · Leapcell

소개
끊임없이 진화하는 웹 개발 환경에서 응답성과 확장성은 더 이상 단순한 장점이 아니라 필수적인 요소입니다. 전통적인 동기 프로그래밍 모델은 직관적이지만, 데이터베이스 쿼리, 외부 API 호출 또는 파일 시스템 상호 작용과 같은 I/O 바운드 작업을 처리할 때 종종 병목 현상을 일으킵니다. 현대 웹 애플리케이션의 요구 사항이 급증함에 따라, 메인 실행 스레드를 차단하지 않고 여러 요청을 동시에 처리하는 능력은 무엇보다 중요해졌습니다. 강력하고 인기 있는 Python 웹 프레임워크인 Django는 역사적으로 동기식이었습니다. 그러나 Django 3.0의 등장과 Django 4.x에서의 상당한 개선으로 프레임워크는 특히 비동기 뷰 및 확장되는 비동기 ORM에 대한 지원을 통해 비동기 기능을 채택했습니다. 이러한 패러다임 전환은 개발자에게 현대 웹 애플리케이션 요구 사항의 복잡성을 직접적으로 해결하는, 보다 성능이 뛰어나고 확장 가능한 백엔드를 구축할 수 있는 강력한 길을 제공합니다. 이 글에서는 Django 4.x가 비동기 뷰 및 ORM 지원을 활용하여 보다 효율적이고 반응성이 뛰어난 사용자 경험을 제공하는 방법을 자세히 살펴봅니다.
비동기 Django 이해하기
Django 4.x의 비동기 뷰 및 ORM에 대한 구체적인 내용을 자세히 살펴보기 전에, 이 기능의 기반이 되는 몇 가지 핵심 개념을 명확히 하는 것이 중요합니다.
비동기 프로그래밍 (Async/Await): 비동기 프로그래밍의 핵심은 차단되지 않는 실행을 허용하는 것입니다. 오래 실행되는 작업이 완료될 때까지 기다리는 대신, 프로그램은 다른 작업에 제어권을 "양보"하고 작업이 완료되면 실행을 재개할 수 있습니다. Python에서는 asyncio
라이브러리의 일부인 async
및 await
키워드를 사용하여 이를 달성합니다. async
는 코루틴 함수를 정의하고, await
는 기다리는 작업이 완료될 때까지 코루틴의 실행을 일시 중지합니다.
ASGI (Asynchronous Server Gateway Interface): ASGI는 비동기 Python 웹 서버 및 애플리케이션을 처리하도록 설계된 WSGI(Web Server Gateway Interface)의 정신적 후계자입니다. WSGI는 동기식이지만, ASGI는 Uvicorn 또는 Hypercorn와 같은 웹 서버와 Django 애플리케이션 간의 비동기 통신을 가능하게 하여 차단되지 않는 I/O 작업을 촉진합니다. Django 3.0 이상 버전은 ASGI를 지원합니다.
코루틴: 일시 중지되고 다시 시작될 수 있는 특수 함수입니다. async def
로 정의되며 await
를 사용하여 다른 코루틴 또는 awaitable 객체가 완료될 때까지 실행을 일시 중지할 수 있습니다.
Django 4.x의 비동기 뷰
Django 4.x는 비동기 뷰를 완벽하게 지원하여 개발자가 기존 동기 함수 대신 코루틴으로 뷰를 정의할 수 있습니다. 이는 상당한 I/O 작업을 수행하는 뷰의 경우 특히 유익합니다.
비동기 뷰를 만들려면 뷰 함수를 async def
로 정의하기만 하면 됩니다.
# myapp/views.py from django.http import JsonResponse from asgiref.sync import sync_to_async from .models import MyModel # MyModel이 있다고 가정 async def async_data_view(request): # 오래 실행되는 I/O 작업 시뮬레이션 (예: 외부 API에서 데이터 가져오기) import asyncio await asyncio.sleep(2) # 비차단 슬립 # 이것은 실제 비동기 작업에 대한 플레이스홀더입니다. # 동기 함수를 호출해야 하는 경우 sync_to_async로 래핑합니다. data = await sync_to_async(list)(range(10)) return JsonResponse({'message': 'Data fetched asynchronously!', 'data': data}) # urls.py from django.urls import path from . import views urlpatterns = [ path('async-data/', views.async_data_view), ]
이 예에서 async_data_view
는 비동기 뷰입니다. await asyncio.sleep(2)
호출은 비차단 일시 중지를 시뮬레이션하여 그동안 서버가 다른 요청을 처리할 수 있도록 합니다. sync_to_async
래퍼는 비동기 컨텍스트 내에서 동기 함수(예: 기술적으로 동기 작업인 list(range(10))
)를 호출해야 할 때 중요합니다. 이는 동기 호출을 별도의 스레드 풀로 오프로드하여 메인 이벤트 루프를 차단하는 것을 방지합니다.
비동기 ORM 지원
Django 4.x는 비동기 뷰를 도입했지만, ORM의 비동기 기능은 점진적인 롤아웃이었습니다. 이전 버전은 주로 비동기 뷰에서 ORM과 상호 작용하기 위해 sync_to_async
유틸리티에 의존했습니다. 그러나 Django는 네이티브 비동기 ORM 지원을 적극적으로 개선하여 보다 직접적인 비동기 데이터베이스 작업을 허용하고 있습니다.
sync_to_async
를 ORM과 함께 사용하는 방법과, 이후 네이티브 비동기 ORM 메서드를 살펴보겠습니다.
ORM과 sync_to_async
사용 (이전 접근 방식 또는 아직 비동기화되지 않은 메서드의 경우):
# myapp/views.py from django.http import JsonResponse from asgiref.sync import sync_to_async from .models import MyModel async def get_my_model_data_sync_wrapper(request): try: # 스레드 풀에서 동기 ORM 메서드 .objects.all()을 호출합니다. my_objects = await sync_to_async(MyModel.objects.all)() # 필요한 경우 각 접근에 대해 sync_to_async를 호출하여 결과 반복 # values_list와 같은 간단한 직렬화의 경우 괜찮을 수 있지만 복잡한 객체 상호 작용에는 더 주의가 필요합니다. data = await sync_to_async(lambda qs: list(qs.values('id', 'name')))(my_objects) return JsonResponse({'data': data}) except Exception as e: return JsonResponse({'error': str(e)}, status=500)
이 접근 방식은 sync_to_async
를 활용하여 동기 ORM 호출을 비차단 상태로 만듭니다. 작동하지만 네이티브 비동기만큼 우아하지는 않습니다.
네이티브 비동기 ORM (Django 4.x 이상):
Django 4.x는 ORM에 네이티브 비동기 메서드를 점진적으로 추가해 왔습니다. 예를 들어 .aget()
, .afirst()
, .acount()
, .aexists()
, .aiterator()
, .abulk_create()
, .abulk_update()
, .aupdate()
, .adelete()
와 같은 메서드가 사용 가능해지고 있습니다. 이러한 메서드는 직접 await
할 수 있도록 설계되었습니다.
# myapp/views.py from django.http import JsonResponse from .models import MyModel # id와 name 필드가 있는 MyModel이 있다고 가정 async def get_my_model_data_async_orm(request): try: # 네이티브 비동기 ORM 메서드 사용 # .all()은 일반적으로 동기식이지만 비동기적으로 반복할 수 있습니다. # 여러 객체를 비동기적으로 가져오려면 .aiterator() 또는 유사한 메서드를 사용합니다. all_objects = await MyModel.objects.all().aall() # .aall()은 목록으로의 비동기 반복에 대한 일반적인 패턴입니다. # 또는 네이티브 비동기 반복을 사용하여 data = [] async for obj in MyModel.objects.all(): # 이는 비동기 반복을 지원하는 데이터베이스 커넥터를 필요로 합니다. data.append({'id': obj.id, 'name': obj.name}) # .aget() 예시 # first_object = await MyModel.objects.aget(id=1) # await first_object.asave() # 비동기 저장 return JsonResponse({'data': data}) except MyModel.DoesNotExist: return JsonResponse({'error': 'Object not found'}, status=404) except Exception as e: return JsonResponse({'error': str(e)}, status=500)
네이티브 비동기 ORM 지원의 전체 범위는 특정 Django 버전과 기본 데이터베이스 커넥터에 따라 달라진다는 점에 유의해야 합니다. 예를 들어, psycopg3
(PostgreSQL용)는 Django의 비동기 ORM에서 활용할 수 있는 좋은 비동기 지원을 제공합니다. 비동기 ORM 기능에 대한 최신 정보는 항상 공식 Django 문서를 확인하십시오.
애플리케이션 시나리오
비동기 뷰와 ORM은 다양한 시나리오에서 빛을 발합니다.
- 장기 폴링/웹소켓 (일반적으로 Channels에서 처리하지만): Django Channels는 실시간 애플리케이션의 표준이지만, 비동기 뷰는 초기 핸드셰이크 또는 특정 메시지 처리를 HTTP 서버를 차단하지 않고 처리함으로써 이를 보완할 수 있습니다.
- 외부 API 통합: 뷰에서 여러 외부 API를 호출해야 할 때, 모든 API에서 동시에 비동기적으로 데이터를 가져오면 응답 시간을 크게 줄일 수 있습니다.
- 데이터 집계: 한 페이지에서 여러 독립적이고 잠재적으로 느린 데이터 소스의 데이터가 필요한 경우, 비동기 뷰를 사용하여 병렬로 가져올 수 있습니다.
- 외부 서비스와의 배치 작업: 여러 이메일, 알림을 보내거나 외부 서비스와 동시에 이미지를 처리합니다.
예를 들어, 두 개의 외부 API에서 동시에 데이터를 가져오는 경우:
import httpx # 권장 비동기 HTTP 클라이언트 from django.http import JsonResponse import asyncio async def fetch_multiple_apis(request): async with httpx.AsyncClient() as client: # 두 API 모두 동시에 가져오기 시작 task1 = client.get('https://api.example.com/data1') task2 = client.get('https://api.example.com/data2') # 두 작업 모두 await response1, response2 = await asyncio.gather(task1, task2) data1 = response1.json() data2 = response2.json() return JsonResponse({ 'source1': data1, 'source2': data2 })
결론
Django 4.x가 비동기 뷰와 ORM 비동기 지원의 발전을 채택한 것은 고성능, 확장 가능한 웹 애플리케이션을 구축하는 데 있어 중요한 도약입니다. 차단되지 않는 I/O 연산을 허용함으로써 개발자는 동시 요청을 효율적으로 처리하는 보다 반응성이 뛰어난 백엔드를 만들 수 있으며, 이는 사용자 경험과 리소스 활용도를 향상시킵니다. 동기 패턴에 익숙한 개발자에게는 전환이 필요하지만, 애플리케이션 성능과 확장성 측면에서의 이점은 상당합니다. 이러한 비동기 기능을 활용하면 개발자는 내일의 요구 사항을 충족할 준비가 된 현대적인 Django 애플리케이션을 만들 수 있습니다.