FastAPI와 websocket
라이브러리를 활용한 채팅 및 async 튜토리얼입니다.
여러분들은 이번 튜토리얼에서 기본적인 REST API와 WebSocket API를 구현하게 됩니다.
이번에 구현할 채팅 프로그램은 간단한 구조입니다. 기본적으로 단 하나의 채팅방이 있으며, 아래 로직을 구현해야 합니다.
- 유저는 유저네임을 설정하고 채팅에 참여할 수 있다 (인증 X)
- 채팅에 참여중인 유저는 다른 모든 참가자에게 공개 메세지(broadcast) 또는 DM을 보낼 수 있다
- (Optional/DIY) Logging
해당 repo를 fork해서 과제를 진행해주세요.
venv 설정하고 requirements.txt로 dependency 설치해주세요. venv는 terminal 설정이라 해당 터미널에서만 활성화된다는 점 주의해주세요.
virtualenv venv --python=3.9.1 # 제 로컬에 3.9.1이라 3.9.1로 한 거지 버전은 상관X (typing 생각하면 3.10+ 권장)
source venv/bin/activate
pip install -r requirements.txt
pip install fastapi
uvicorn src.app.main:app --reload
이후 localhost:8000/chat/
으로 들어가보세요.
여러분이 구현한 채팅 서버는 웹소켓 커넥션들을 관리하는 역할입니다.
websocket
라이브러리를 활용해서 웹소켓 연결을 관리하게 됩니다.
클라이언트는 ws://localhost:8000/chat/ws/{user}
엔드포인트를 통해 서버와의 웹소켓 커넥션을 생성할 수 있습니다.
서버는 Chat
클래스를 통해 채팅에 참여한 유저별로 웹소켓 커넥션을 관리합니다.
웹소켓 커넥션 생성시 채팅 멤버에 추가하고, 커넥션이 끊어지면 멤버에서 제거합니다.
커넥션 생성은 accept()
, 커넥션 종료는 close()
메소드를 통해 이루어집니다.
(TIP) 웹소켓 작업은 비동기적으로 이루어집니다. async
/await
을 적절히 활용해야 합니다.
여러분들이 할 일은 다음과 같습니다.
Chat
클래스의join
메소드를 완성하세요.src/app/chat/chat.py
에 있습니다.accept()
메소드로 커넥션을 생성해주세요.- 동일한 이름의 유저가 이미 존재할 경우 채팅에 참여할 수 없어야 합니다.
- 구현상의 이유로
system
이라는 이름을 가진 유저도 참여할 수 없습니다. system은 채팅방 관리자의 이름입니다. - 채팅 참여에 실패했을 경우
close()
메소드로 웹소켓 커넥션을 종료시켜주세요. - 이 때,
User [유저명] already exists
라는 close reason을 함께 명시해주세요.
- 구현상의 이유로
- 채팅 참여에 성공했을 경우 유저를 채팅 멤버에 추가해주세요.
- (Optional) 채팅에 새로운 참여자가 있을 경우 모든 유저에게 아래와 같은 시스템 메세지를 보내세요.
{ "from": "system", "msg": "User [유저명] joined the chat" }
- 메세지는 웹소켓의
send_json
메소드를 통해 보낼 수 있습니다. - 단순히
async
/await
으로 모든 멤버에게 메세지를 보내면 비동기적으로 작동하지 않습니다. - 한 유저에게 메세지를 보내는 것을 기다린 후 (
await
) 다음 유저에게 메세지를 보내게 됩니다. - 동시에 모든 유저에게 메세지를 보내는 방법을 생각해보세요.
- 메세지는 웹소켓의
Chat
클래스의leave
메소드를 완성하세요.- 유저를 채팅 멤버에서 제외시키세요.
- (Optional) join과 마찬가지로 모든 유저에게 아래와 같은 시스템 메세지를 보내세요.
{ "from": "system", "msg": "User [유저명] left the chat" }
websocket
라우터를 완성하세요.src/app/chat/router.py
에 있습니다.- 앞서 완성한
Chat
클래스의 인스턴스가 전역에 선언되어 있습니다. 해당 chat에 join해주세요. - 이 라우터는 주어진 websocket에 대한 전담 event handler가 될 것입니다.
- 웹소켓을 통해 메세지가 도착할 때마다 chat 클래스의
handle_message
메소드를 통해 처리하세요.handle_message
메소드는 다음 Task에서 구현하게 됩니다.
- 유저 메세지는 웹소켓의
receive_json
메소드를 통해 받을 수 있습니다.
- 웹소켓을 통해 메세지가 도착할 때마다 chat 클래스의
- 유저와의 웹소켓 커넥션이 끊어졌을 경우
receive_json
메소드는 익셉션을 발생시킵니다.- 이 경우, 유저를 채팅에서 제외시키세요.
- 또한, 커넥션이 종료되었으므로 event handler 또한 리턴해주세요.
- 앞서 완성한
첫 번째 과제를 끝내고 나면 메세지를 보내는 기능 외에는 모두 구현되어 있어야 합니다.
- 서버를 실행시키고 브라우저를 열어
localhost:8000/chat/
으로 들어가보세요. - 유저네임을 입력하고 채팅에 참여해보세요.
- 다른 브라우저나 탭에서 접속해서, 같은 유저네임으로 채팅에 참여해보세요. 이 때 실패해야 합니다.
- 만약 Optional을 구현헀다면 다른 유저네임으로 채팅에 참여한 후 탭/브라우저를 닫아보세요. 이 때 시스템 메세지가 출력되어야 합니다.
여러분들의 서버가 처리하는 메세지는 두 가지 종류입니다.
- DM
- 전체 메세지
이 두 가지 메세지 타입을 구분하기 위해서 웹소켓 메세지 형식을 정의했습니다. 클라이언트가 서버로 보내는 메세지 타입은 다음과 같습니다.
{
"type": "direct",
"to": "jiho",
"msg": "Hello, jiho!"
}
메세지 타입은 두 가지 (direct
/broadcast
)가 있으며,
broadcast
메세지는 모든 유저가 수신하므로 수신자를 field(to
)는 무시됩니다.
서버에서 클라이언트에게 전달되는 메세지 타입은 다음과 같습니다. 여러분들이 앞서 보았던 system 메세지와 같은 형식입니다.
{
"from": "jiho",
"msg": "I'm not hello fml"
}
여러분들은 Chat
클래스의 handle_message
메소드를 완성시켜야 합니다.
- 먼저, 메세지 타입과 수신자를 파악하세요.
- 알맞은 메세지 수신자에게 메세지 타입에 맞추어 메세지를 보내세요.
- 당연하지만, broadcast 메세지는 송신자는 수신하지 않아야 합니다.
- 만약 DM의 수신자가 현재 채팅 멤버가 아닌 경우 메세지 전송은 실패합니다.
- 이 경우 송신자에게 아래와 같은 시스템 메세지를 보내세요.
{ "from": "system", "msg": "User [유저명] does not exist" }
- 이 경우 송신자에게 아래와 같은 시스템 메세지를 보내세요.
이 과제까지 진행하면 모든 채팅 기능이 완성됩니다. 마찬가지로 로컬에서 테스트해보세요.
과제를 하면서 막히는 부분이 있었을 때 디버깅이 매우 힘들었을 겁니다. 일반적인 프로그램과는 달리 네트워크 프로그램은 환경과의 상호작용 때문에 디버깅이 힘들어집니다.
이것이 logging, testing, 그리고 tracing이 중요한 이유입니다. 모든 백엔드는 개발 편의성을 위해 다양한 정보를 로깅하고 다양한 메트릭을 측정합니다. 이는 DevOps 사이드에서 운영하는 APM 등 Ops 툴들과 결합하여 유저 트렌드 분석, 백엔드 성능 저하 원인 분석 등 다양한 용도로 활용됩니다. 그러나 숙련자가 아닌 경우 종종 불필요한 정보를 로깅하거나 필요한 정보를 누락하곤 합니다.
이 과제는 선택사항입니다. 여러분들이 과제를 하면서 어려움을 느낀 부분을 돌아보고, 어떤 로그를 남기면 좋을 지 고민해보세요. 또한 여러분들이 생각할 때 웹소켓 서버에서 어떤 메트릭을 측정하면 좋을지 고민해보세요. 그리고 출력해보세요. 보통은 라이브러리를 쓰지만 어차피 우리는 로컬에서 돌릴 거니깐 stdout으로 출력해도 충분할 겁니다.
본 repo로 PR을 올려주세요. PR 템플릿이 준비되어 있습니다.