Channels는 Django를 확장해 웹소켓과 같이 HTTP가 아닌 프로토콜을 핸들링할 수 있게 돕고 비동기적인 처리를 가능하게 해주는 ASGI의 구현체로, 장고를 이용한 실시간 채팅 구현 등에 활용할 수 있습니다. 이 글은 채널즈의 공식 문서를 최대한 원어를 살려 번역한 글입니다. 다소 의역하거나 생략한 부분이 있을 수 있음을 너그러이 양해해주시고, 잘못을 자유롭게 지적해주시면 감사하겠습니다.
채널즈에 오신 것을 환영합니다!
채널즈는 장고에서 기본으로 제공하는 비동기 뷰를 감싸, 장고가 HTTP뿐만 아니라 긴시간 연결돼야 하는 웹소켓, MQTT, 챗봇, 라디오 등의 프로토콜을 처리할 수 있게 해줍니다.
이러한 작업은 쉽게 쓸 수 있는 장고의 동기적인 방식과 양립할 수 있고, 장고 뷰를 동기적이든, 완전히 비동기적이든, 아니면 둘을 적절히 섞든, 원하는 방식으로 작성할 수 있습니다. 그 뿐만 아니라 채널즈는 장고의 인증, 세션 시스템과 통합될 수 있고, 따라서 HTTP용으로 개발된 프로젝트를 다른 프로토콜로 쉽게 확장할 수 있습니다.
채널즈는 또한 이러한 이벤트 주도의 구조를 채널 레이어
를 이용해 다루는데, 채널 레이어
는 여러 프로세스 간의 통신을 용이하게 만들면서 프로젝트를 여러 개의 프로세스로 쪼갤 수 있게 해줍니다.
아직 채널즈를 설치하지 않으셨다면 설치하기를 먼저 읽어주세요. 이 글은 직접적인 튜토리얼은 아니지만, 아래 내용을 따라오시면 장고 프로젝트에 변화를 줄 수 있을 것입니다.
채널즈는 "Turtles All The Way Down"의 원리 위에서 작동합니다 - 우리는 채널즈 "애플리케이션"이 무엇인지에 대한 공통된 생각이 있고, 가장 단순한 컨슈머들(장고에서의 뷰와 같은 것입니다)조차도 여러분이 직접 실행 가능한 하나의 완전한 ASGI 애플리케이션입니다.
주의사항
ASGI는 채널즈가 기반을 두고 있는 '비동기 서버 설명서'입니다. WSGI처럼, ASGI는 저희의
Channels
와Daphne
서버만을 사용하도록 강제하지 않고, 여러 서버와 프레임워크 중에 선택할 수 있도록 설계되었습니다. 더 자세한 사항은 http://asgi.readthedocs.io를 참조해주세요.
컨슈머(consumers)는 채팅 메시지나 알림과 같은 것을 처리하는 독립적인 요소입니다. 채널즈는 이러한 기본적인 컨슈머를 작성하고, URL 라우팅에 연결하고, 프로토콜을 감지하는 등의 기능을 제공합니다.
우리는 HTTP와 기존의 장고 애플리케이션을 구성요소로 활용합니다. 전통적인 장고 뷰를 여전히 채널즈와 함께 사용할 수 있고, 거기에 더해 기존의 코드를 유지한 상태로 HTTP Long-폴링이나 웹소켓을 함께 처리할 수 있습니다. URL 라우팅, 미들웨어 모두 ASGI 애플리케이션일 뿐입니다.
우리는 여러분이 대부분의 경우에 장고 뷰와 같이 안전한 동기적인 방식을 사용하길 원한다고 믿습니다. 단지 조금 더 복잡한 작업을 처리할 때는 더 직접적인, 비동기 도구를 사용할 수 있도록 해드리려 합니다.
채널즈와 ASGI는 들어오는 연결을 두개의 컴포넌트로 분리합니다: 하나는 스코프이고, 다른 하는 이벤트들의 리스트입니다.
스코프는 하나의 들어오는 연결에 대한 상세 설명들의 집합으로, 요청이 발생한 URL, 웹소켓이 열린 IP 주소, 아니면 유저 정보와 같은 것들로 구성되어 있으며, 연결이 종료될 때까지 유지됩니다.
HTTP의 경우에 스코프는 하나의 요청동안만 유지됩니다. 하지만 웹소켓의 경우에, 스코프는 소켓의 수명 전체에 걸쳐 유지됩니다(하지만 소켓이 닫히고 재연결될 때 변할 수는 있습니다). 다른 프로토콜의 경우엔 각 프로토콜의 ASGI 스펙이 어떻게 작성되었는지에 따라 다릅니다; 예를 들어, 챗봇 프로토콜은 설령 근본적으로 챗 프로토콜이 상태를 갖지 않는다고(stateless) 하더라도, 스코프가 한 유저의 봇과의 대화가 종결될 때까지 유지될 것입니다.
스코프의 수명 동안, 여러 이벤트들이 발생합니다. 이 이벤트들은 유저와의 상호작용을 나타냅니다. 예를 들어 HTTP 요청을 보낸다든가, 웹소켓 프레임을 전송하는 등의 일들입니다. 채널즈, 혹은 ASGI 애플리케이션은 매 스코프마다 개별 인스턴스화되고, 해당 스코프 안에서 발생하는 이벤트들을 차례로 처리합니다.
HTTP 예시를 들자면:
챗봇의 예시는 다음과 같습니다:
스코프의 수명주기동안 -그것이 채팅이든, HTTP 요청이든, 소켓 연결이든- 스코프 안의 모든 이벤트를 처리하는 하나의 애플리케이션 인스턴스를 만들고, 관련된 데이터를 그 위에 유지해야 합니다. 물론 이러한 작업을 수행하는 ASGI 애플리케이션을 직접 만들 수도 있겠지만, 채널즈는 그러한 일을 해주면서 쉽게 사용이 가능한 추상화된 요소를 제공합니다. 바로 컨슈머(consumers)입니다.
컨슈머는 채널즈의 기본적인 단위요소입니다. 우리는 컨슈머가 이벤트를 받아들이기(consume)때문에 컨슈머라 부릅니다(스스로 동작하는 작은 애플리케이션으로 생각하셔도 좋습니다). 요청이나 새로운 소켓이 들어오면, 채널즈는 라우팅 테이블을 찾아 - 라우팅은 뒤에서 다룹니다 - 해당 연결에 대한 적절한 컨슈머를 찾고 그 인스턴스를 하나 만들어 처리합니다.
이것은 장고 뷰와는 다르게, 컨슈머가 오랫동안 동작함을(long-running) 의미합니다. 컨슈머는 물론 짧은 기간 동안 동작할 수도 있지만- HTTP 요청들 역시 컨슈머에 의해 처리될 수 있으니까요 - 좀 더 긴 수명을 갖도록(스코프가 살아있는 동안 존재하도록) 설계되었습니다.
기본적인 컨슈머의 형태는 다음과 같습니다:
class ChatConsumer(WebsocketConsumer):
def connect(self):
self.username = "Anonymous"
self.accept()
self.send(text_data="[Welcome %s!]" % self.username)
def receive(self, *, text_data):
if text_data.startswith("/name"):
self.username = text_data[5:].strip()
self.send(text_data="[set your username to %s]" % self.username)
else:
self.send(text_data=self.username + ": " + text_data)
def disconnect(self, message):
pass
각 종류의 프로토콜에선 서로 다른 타입의 이벤트들이 발생하고, 각각의 타입은 서로 다른 메소드로 구분됩니다. 여러분이 각 이벤트를 처리하는 코드를 작성하면, 채널즈가 그들을 스케쥴링하고 병렬적으로 작동시키는 역할을 담당합니다.
내부적으로, 채널즈는 완전히 비동기적인 이벤트 루프를 실행합니다. 그리고 만약 위와 같이 코드를 작성하면, 동기적인 쓰레드 상에서 호출될 것입니다. 즉 아래 코드에 나오는 것처럼, 장고 ORM과 같이 다른 작업을 가로막을 수 있는 동작을 안전하게 수행할 수 있습니다:
class LogConsumer(WebsocketConsumer):
def connect(self, message):
Log.objects.create(
type="connected",
client=self.scope["client"],
)
하지만, 만약 더 많은 통제권을 원하고 비동기적인 함수만 사용하고 싶다면, 다음과 같이 완전히 비동기적인 컨슈머를 만들 수도 있습니다:
class PingConsumer(AsyncConsumer):
async def websocket_connect(self, message):
await self.send({
"type": "websocket.accept",
})
async def websocket_receive(self, message):
await asyncio.sleep(1)
await self.send({
"type": "websocket.send",
"text": "pong",
})
더 자세한 것은 컨슈머
페이지에서 확인하세요.
라우팅을 이용하면 여러 개의 컨슈머들을(잊지 마세요, 각각의 컨슈머는 그자체로 ASGI 애플리케이션입니다) 프로젝트와 같이 하나의 거대한 앱으로 합칠 수 있습니다:
application = URLRouter([
url(r"^chat/admin/$", AdminChatConsumer.as_asgi()),
url(r"^chat/$", PublicChatConsumer.as_asgi(),
])
채널즈는 HTTP와 웹소켓의 세계에서만 동작하지 않습니다 - 각 프로토콜을 비슷한 이벤트 집합에 매핑하여, 장고 환경에서 어떠한 프로토콜이든 처리할 수 있게 해줍니다. 예를 들어, 다음과 같이 유사한 방식으로 챗봇을 만들 수 있습니다:
class ChattyBotConsumer(SyncConsumer):
def telegram_message(self, message):
"""
Simple echo handler for telegram messages in any chat.
"""
self.send({
"type": "telegram.message",
"text": "You said: %s" % message["text"],
})
그리고 다른 라우터를 함께 사용해 하나의 프로젝트가 웹소켓와 채팅 요청 둘 모두를 처리할 수 있게 해줍니다.
application = ProtocolTypeRouter({
"websocket": URLRouter([
url(r"^chat/admin/$", AdminChatConsumer.as_asgi()),
url(r"^chat/$", PublicChatConsumer.as_asgi()),
]),
"telegram": ChattyBotConsumer.as_asgi(),
})
채널즈의 목표는 장고 프로젝트가 여러분이 모던 웹에서 마주할 수 있는 어떠한 프로토콜이나 전송방식이든, 모두 여러분이 익숙한 구성요소들이나 코딩 방식을 사용해 처리할 수 있도록 돕는 것입니다.
더 자세한 것이 알고 싶으시다면, 라우팅
페이지를 참조하세요.
표준 WSGI 서버처럼, 여러분의 애플리케이션은 프로토콜 이벤트들을 서버 프로세스 안에서 처리합니다 - 예를 들어 웹소켓을 처리하는 코드는 웹소켓 서버 프로세스 내에서 실행됩니다.
여러분의 애플리케이션에 들어오는 각 소켓이나 연결은 이러한 서버 중 하나의 안에서 애플리케이션 인스턴스에 의해 처리됩니다. 그들은 호출되고, 클라이언트에게 다시 데이터를 직접 보내줄 수 있습니다.
하지만, 더 복잡한 애플리케이션을 만들다 보면 서로 다른 애플리케이션 인스턴스들 간의 통신이 필요한 경우가 생깁니다 - 예를 들어 채팅방을 구현하려면, 하나의 애플리케이션 인스턴스에게 전송된 메시지를 (채팅방 내의 다른 사람들을 나타내는) 다른 인스턴스들에게 전파할 필요가 있을 것입니다.
여러분은 물론 이것을 데이터베이스를 폴링(짧은 주기로 반복 요청을 보내는것)하여 구현할 수 있습니다. 하지만 채널즈는 채널 레이어
라는, 전송 집합과 관련해 저수준에서 추상화한 개념을 도입해 프로세스간의 정보 교환을 가능하게 했습니다. 각각의 애플리케이션 인스턴스는 고유한 채널
명을 갖고 있고, 그룹
에 참가할 수 있으며, 이를 통해 포인트 투 포인트 메시지 전송과 메시지 전파 모두 가능합니다.
역자: 카톡으로 생각하면 각각의 사람(의 매 연결)이 채널이고, 그룹이 채팅방(좀 더 정확히는, 채팅방 안에서 만들어지는 관련 있는 채널의 집합), 포인트 투 포인트는 일대일 채팅(목표 채널로 메시지 전송), 메시지 전파는 채팅방 채팅(그룹 안의 모든 채널로 메시지 전송)에 대응되는 추상화 개념이라 이해하시면 좋을 것 같습니다.
주의사항
채널 레이어는 채널즈의 선택적인 기능입니다. 원하신다면 CHANNEL_LAYERS 세팅값을 빈 값으로 설정해 비활성화할 수 있습니다.
또한 자신의 정해진 채널명에 반응하는 전용 프로세스에 메시지를 보낼 수도 있습니다:
# In a consumer
self.channel_layer.send(
"myproject.thumbnail_notifications",
{
"type": "thumbnail.generate",
"id": 90902949,
},
)
채널 레이어에 대해서는 채널 레이어
페이지에서 더 자세히 확인하실 수 있습니다.
채널즈는 세션, 인증과 같이 장고가 제공하는 간편한 내장 기능들을 똑같이 가지고 있습니다. 여러분은 웹소켓 뷰에 적절한 미들웨어를 덧붙이는 것만으로 인증을 구현할 수 있습니다:
from django.urls import re_path
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(
URLRouter([
re_path(r"^front(end)/$", consumers.AsyncChatConsumer.as_asgi()),
])
),
})
자세한 사항은 세션
과 미들웨어
페이지에서 확인하실 수 있습니다.
설치하기로 이어집니다.