Channels 라이브러리가 개선됨에 따라 해당 시리즈의 내용이 유효하지 않은것 같습니다.
참고용으로만 봐주시길 바라며, 공식 문서를 참고해주세요!
- [Django] 실시간 채팅 - Channels 튜토리얼 (1)
- [Django] 실시간 채팅 - Channels 튜토리얼 (2)
- [Django] 실시간 채팅 - Channels 튜토리얼 (3)
이 글은 Django Channles 공식 문서를 참고하여 정리한 것입니다. ( 링크 )
튜토리얼이 잘 나와있어서 쉽게 적용할 수가 있었는데, 한글로 된 문서가 없는 것 같아서 정리를 하려고 합니다.
번역기의 힘을 빌려가며 해석을 했지만, 맞지 않는 부분이 있을 수 있기 때문에 원문과 참고하여 보시길 바랍니다.
또한 이전 글과 내용이 이어집니다. ( 링크 )
이전 글에서는 클라이언트의 연결을 처리하는 소비자( Consumer )에 대해 알아보고, Channel layer을 통해 Room을 생성하여 클라이언트들이 소통하는 방법에 대해 알아보았습니다.
이 글에서는 소비자를 재작성해볼 것이고, Channels의 Routing에 대해 좀 더 알아보겠습니다.
1. 소비자를 비동기로 재작성
지금까지 작성한 소비자는 현재 동기적으로 작성되어 있습니다.
이 방법은 특별한 코드 없이 동기적인 Django의 I/O함수를 호출 할 수 있어서 편리한 방법입니다.
이와 반대로 비동기적인 소비 방식은 요청을 처리할 때 추가적인 쓰레드를 생성하지 않으므로 높은 성능을 보입니다.
이제부터 작성할 소비자는 Channels와 channel layer의 비동기식 라이브러리( async-native )에만 접근하고, Django의 동기 방식으로 접근하지 않도록 작성할 것입니다.
실시간 채팅에서는 성능 문제가 중요하기 때문에 이 작업은 필요한 과정이라 볼 수 있습니다.
** 참고 **
sync_to_async 같이 Django의 동기적인 코드를 비동기적으로 수행하도록 할 수 있지만, async-native 라이브리러리를 사용했을 때 보다 성능은 좋지 않습니다.
이제 소비자를 재작성해보도록 하겠습니다.
chat/consumers.py
# chat/consumers.py
from channels.generic.websocket import AsyncWebsocketConsumer
import json
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = 'chat_%s' % self.room_name
# Join room group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
# Leave room group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
# Receive message from WebSocket
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
# Send message to room group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': message
}
)
# Receive message from room group
async def chat_message(self, event):
message = event['message']
# Send message to WebSocket
await self.send(text_data=json.dumps({
'message': message
}))
ChatConsumer는 원래 WebsocketConsumer를 상속받았지만, AsyncWebsocketConsumer를 상속받도록 수정했습니다.
그리고 모든 메서드의 선언부에는 async가 붙고, 이를 호출하는 부분에는 await을 붙여서 비동기 처리를 했습니다.
즉 이전에 작성했던 async_to_sync는 더 이상 필요가 없습니다.
2. 테스트
$ python manage.py runserver
브라우저에서 http://127.0.0.1:8000/chat/lobby/에 접속하여 테스트를 해보면, 문제없이 잘 동작합니다.
이것으로 Django Channels을 이용하여 실시간 채팅 튜토리얼이 끝났습니다.
다음으로는 제가 Channels를 보면서 궁금했던 부분인 Channels의 Routing에 대해 알아보고,
그 밖에 추가적인 설정 및 이슈에 대해서도 알아보겠습니다.
3. Routing ( 링크 )
Channels은 연결 상태에 따라 소비자를 결합하여 배포할 수 있는 라우팅 클래스를 제공합니다.
Channels 라우터는 각각의 이벤트가 아닌 scope 레벨로 동작한다는 점에 유의하시길 바랍니다.
이 의미는 특정 연결에 대해 하나의 소비자만 있을 수 있다는 것을 말하며,
라우팅은 하나의 연결에서 여러 소비자에게 이벤트를 배포하는 방법이 아니라, 연결할 단일 소비자를 결정하는 역할을 합니다.
또한 라우터는 그자체로 유효한 ASGI 애플리케이션이고, 중첩으로 작성이 가능합니다.
그래서 mysite/routing.py 파일에서 ProtocolTypeRouter을 가장 바깥에 작성하고, 더 많은 포토토콜 관련 라우팅을 아래에 중첩시켰습니다.
Channels은 단일 root application( ProtocolTypeRouter )을 정의 하여 이에 대한 경로를 ASGI_APPLICATION 설정으로 제공하기를 기대합니다.
mysite/settings.py 파일에 아래의 내용을 추가했었습니다.
라우팅과 root application을 어느 경로에 두어야 할지는 정해진 규칙이 없지만, 프로젝트 파일 단위로 urls.py파일 옆에 routing.py 이름으로 위치하는 것을 추천한다고 합니다.
4. ProtocolTypeRouter
channels.routing.ProtocolTypeRouter 라우팅 파일은 ASGI 애플리케이션 스택에서 가장 상위에 있는 계층입니다.
이는 현재 scope에 있는 type 값을 기준으로 다른 ASGI 애플리케이션 중 하나로 분기할 수 있습니다.
이 때 Protocol들은 고정된 값으로 정의되어야 하며, scope를 포함해야 합니다.
그래야 요청의 연결 타입을 구분할 수 있기 때문입니다.
위의 예를 보면,
http 요청은 어떤 app에서 처리하도록 scope를 지정하고,
websocket은 다른 어떤 app에서 처리할 수 있도록 scope를 달리합니다.
http와 websocket을 처리하는 앱이 다를 것이기 때문에 이와 같이 구분을 짓습니다.
만약 http를 정의하지 않을 경우( 지금까지 살펴봤던 예제의 경우 ), Django View 시스템의 ASGI 인터페이스인 channels.http.AsgiHandler가 기본적으로 처리 됩니다.
long-poll HTTP Handling을 처리하지 않는 프로젝트에서는, 특별히 http 옵션을 지정하지 않고, 정상적인 Django 방식이 작동하도록 두면 됩니다.
5. URLRouter
channels.routing.URLRouter는 HTTP 또는 WebSocket 타입의 연결을 라우팅합니다.
위의 코드는 튜토리얼에서 사용했던 코드입니다.
이를 기준으로 URLRouter를 알아보겠습니다.
['url_route']는 args와 kwargs를 key로 갖는 dictionary입니다.
['url_route']['kwargs']는 kwargs는 정규표현식에서 작성된 이름과 그 값을 key와 value로 갖는 dictionary입니다.
['url_route']['kwargs']['room_name']는 kwargs의 여러 key중에서 room_name의 value를 얻는 코드입니다.
6. 채널 지속 시간 ( Channels Layer Specification - Persistence ( 링크 ) )
Channel layer는 group에 대한 최근 호출 이후에 일정 시간이 지난 후 자동으로 group을 삭제하는데, 그 기본 값은 86,400 초 (1 일)입니다.
time out 값은 channels layer의 group_expiry 속성으로 수정 할 수 있습니다.
7. ORM 사용시 유의사항 ( Database Access ( 링크 ) )
Django ORM은 동기식 코드입니다.
그런데 Channels는 비동기로 동작하기 때문에 비동기 코드에서 DB에 접근하려는 경우, DB 연결과 닫는 과정을 잘 처리해야 합니다.
물론 튜토리얼 2처럼 Channesl를 동기적으로 작성할 수 있는데, 이 경우에는 별도의 작업이 필요 없습니다.
반면 튜토리얼 3과 같이 비동기로 작성했다면, database_sync_to_async 메서드 또는 데코레이션을 통해 DB에 접근해야 합니다.
이상으로 Django Channels 튜토리얼을 모두 마치도록 하겠습니다.
공식 문서를 보고 해석을 해서 작성을 해보았는데, 글이 매끄럽지가 못하네요...
설명이 애매한 부분과 더 많은 정보는 공식 문서를 참고하시면 좋을 것 같습니다 !
마지막으로 Channels 애플리케이션을 배포하는 방법은 여기를 참고해주세요.
[ 참고 ]
https://channels.readthedocs.io/en/latest/index.html#django-channels