사이드 프로젝트에서 간단한 채팅 기능이 필요해서, 카카오톡 같은 1:1 채팅을 하루만에 간단하게 구현해 보았다. 코드가 그리 깔끔하지는 않은데, 하루 만에 만든 것임을 감안하고 글을 읽어 주시기를 바란다. 처음에 생각했을 때에는 에이 ㅋㅋ 뭐 채팅 정도야 ㅋㅋ 쉽지 ㅋㅋ 이러면서 덤벼들었는데, 생각보다 데이터베이스 설계가 까다로웠다. 일단 생각해 봐야 할 것들이 꽤나 많았다. 본질적으로, 같은 대화 내용에 대해서 채팅 참여자들이 다르게 반응하는 것을 처리하는 것이 까다로웠다.
- 내가 메시지를 읽는 행위가 나와 상대방 모두의 상태를 바꾼다. 나는 안 읽은 채팅 개수가 사라지고, 상대방은 1이 사라진다.
- 보낸 메시지를 읽을 수도 있고, 안 읽을 수도 있다. 읽고 삭제할 수도 있고, 안 읽고 삭제할 수도 있다.
- 내가 삭제한 대화 방을 상대방은 여전히 읽을 수 있다.
- 대화 방을 삭제한 후에 메시지를 다시 주고받을 경우, 삭제한 사람은 그 이전 메시지를 읽을 수 없지만 삭제하지 않은 사람은 그 이전 메시지를 읽을 수 있다.
- 둘 다 대화 방을 나가면 아무도 읽을 수 없게 된다.
개발 시간을 최대한 줄이기 위해 비정규화를 진행하여 1:1 채팅만을 지원하고, 단체 채팅은 지원하지 않는 것으로 스펙 확정 지었다.
데이터베이스 설계
한참 동안 생각해 보아도 명확한 설계가 떠오르지 않아, 구글링을 진행했다. 역시 다른 사람들이 고민해 둔 흔적들을 찾을 수 있었다. 그 중 너무 복잡한 것 말고 가장 심플하게 이 문제를 풀 수 있는 설계를 찾았다. Stack Overflow: Database Structure for Web Messaging System
-
header
에는 대화 목록들이 저장된다.from_id
에는 최초에 보낸 사람이 저장된다.to_id
에는 최초에 받는 사람이 저장된다. 즉, 대화방은from_id
과to_id
의 조합으로 생성된다.subject
에는 마지막으로 보낸 문자가 저장된다.status
에는 누가 메시지를 마지막으로 보냈는지 저장된다.time
에는 마지막으로 메시지가 보내진 시간이 저장된다.
-
message
에는 각 채팅 하나하나가 저장된다.is_from_sender
는from_id
가 보냈는지 여부가 저장된다.content
에는 각 메시지가 저장된다.read
는 상대방이 메시지를 읽었는가가 저장된다. (즉… 보낸 사람은 항상 메시지를 읽는다.)time
에는 메시지를 보낸 시간이 저장된다.
안 읽은 메시지 개수 보여주기: 대화방 목록을 보여줄 때 안 읽은 메시지 개수를 추가로 보여주기 위해, status 대신 각각 보낸 사람과 받는 사람의 안 읽은 메시지 개수를 저장하는 것으로 설계를 변경했다.
대화방 나가기 기능: 위의 설계에는 대화방을 나가는 것에 대한 고려가 되어 있지 않다. 추가로 고려할 만한 스펙을 적어 보았다.
- 개별 메시지의 삭제 기능은 제공하지 않는다. 대화 방을 나가는 기능만 제공한다.
-
대화 방을 한 사람이 나가도 다른 사람은 대화 내역을 여전히 볼 수 있다.
- 나간 사람은 대화방 목록에서 보이지 않는다. 나가지 않는 사람은 대화방 목록에서 보인다.
- 이 때 대화가 추가로 이루어지는 경우, 나간 사람도 다시 대화방이 보이게 된다. 나가지 않은 사람은 이전의 대화도 보여야 하나, 나간 사람은 이전의 대화는 보이면 안 된다.
- 즉 대화방 자체는 어떻게든 계속 남아있어야 한다. 삭제 플래그를 사용하면 된다.
-
이는… 삭제에 대해서 채팅방 목록과 메시지 목록에 대해서, 아래의 상태를 저장해야 한다는 뜻이다.
- 둘 다 나가지 않은 상황
- 보낸 사람만 나간 상황
- 받는 사람만 나간 상황
- 보낸 사람과 받는 사람이 모두 나간 상황
- 그리고 위의 상황에서 메시지를 한 번이라도 다시 주고받는 경우 대화 방은 다시 보여야 한다는 점이다. 물론 메시지는 이후에 대해서만 보여야 한다.
어떻게 구현할까 고민하다가… 역시 구글을 찾아보게 되었다. Stack Exchange: Removing chat history for a particular user from database 에서 제안을 찾아볼 수 있었다. MESSAGE_VISIBLE_TO
필드를 추가하고, 아래와 같이 네 가지 상태를 구현하라는 것이었다. 엄청 깔끔하지는 않지만 심플한 해결책이어서 이 방법을 추가하기로 진행했다.
- NULL = 기본 값이며, 양쪽 사용자가 모두 메시지를 읽을 수 있음
- -1 = 어떤 사용자도 읽을 수 없음
- UserID1 = User1만 메시지를 읽을 수 있음
- UserID2 = User2만 메시지를 읽을 수 있음
최종적으로 작성하게 된 모델 코드는 아래와 같다. 읽을 수 있는 상태를 위의 답변처럼 Integer가 아니라 Enum으로 설정해 보았다.
class Room(TimeStampedModel):
sender_user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='+')
sender_unread_count = models.IntegerField("보낸 사람이 읽지 않은 개수")
sender_is_deleted = models.BooleanField("보낸 사람이 지웠는지 여부")
receiver_user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='+')
receiver_unread_count = models.IntegerField("받는 사람이 읽지 않은 개수")
receiver_is_deleted = models.BooleanField("받는 사람이 지웠는지 여부")
latest_text = models.CharField('마지막 메시지', max_length=1000)
class Message(TimeStampedModel):
class VisibleChoices(models.TextChoices):
BOTH = 'BOTH', '두 사람 모두 읽을 수 있음'
ONLY_SENDER = 'ONLY_SENDER', '보낸 사람만 읽을 수 있음'
ONLY_RECEIVER = 'ONLY_RECEIVER', '받는 사람만 읽을 수 있음'
NOBODY = 'NOBODY', '모두 읽을 수 없음'
room = models.ForeignKey(Room, on_delete=models.CASCADE)
is_from_sender = models.BooleanField("보낸 사람이 메시지를 보냈는지 여부")
text = models.CharField('메시지', max_length=1000)
read = models.BooleanField('받는 사람이 읽음 여부')
visible = models.CharField('읽을 수 있는 사람', max_length=16, choices=VisibleChoices.choices)
구현
우선, 메시지를 전송하는 로직을 작성했다. 내가 받는 사람인지, 보내는 사람인지에 따라서 참조하게 되는 필드가 반대가 되기 때문에 해당하는 부분을 신경써서 작성하였다.
@transaction.atomic
def send_message(sender: User, receiver: User, text: str):
if sender == receiver:
return
# 1. 내가 보낸 사람으로 만든 채팅 방이 있는지 확인한다.
try:
room = Room.objects.get(sender_user=sender, receiver_user=receiver)
is_sender = True
except Room.DoesNotExist:
# 2. 내가 받는 사람으로 만든 채팅 방이 있는지 확인한다.
try:
room = Room.objects.get(sender_user=receiver, receiver_user=sender)
is_sender = False
except Room.DoesNotExist:
# 3. 내가 보낸 사람으로 채팅 방을 만든다.
room = Room.objects.create(
sender_user=sender,
sender_unread_count=0,
sender_is_deleted=False,
receiver_user=receiver,
receiver_unread_count=0,
receiver_is_deleted=False,
)
is_sender = True
if is_sender:
# 이전 메시지 읽음 처리
Message.objects.filter(
room=room,
is_from_sender=False,
read=False,
).update(read=True, modified=now())
# 메시지 발신 처리
message = Message.objects.create(
room=room,
is_from_sender=True,
text=text,
read=False,
visible=Message.VisibleChoices.BOTH,
)
# 채팅 방 업데이트 처리
room.sender_unread_count = 0
room.sender_is_deleted = False
room.receiver_unread_count += 1
room.receiver_is_deleted = False
room.latest_text = text
room.save()
else:
# 이전 메시지 읽음 처리
Message.objects.filter(
room=room,
is_from_sender=True,
read=False,
).update(read=True, modified=now())
# 메시지 발신 처리
message = Message.objects.create(
room=room,
is_from_sender=False,
text=text,
read=False,
visible=Message.VisibleChoices.BOTH,
)
# 채팅 방 업데이트 처리
room.sender_unread_count += 1
room.sender_is_deleted = False
room.receiver_unread_count = 0
room.receiver_is_deleted = False
room.latest_text = text
room.save()
그 다음으로는 대화 방을 나가는 함수를 구현했다. 크게 보면 기존 메시지들의 상태를 적절히 변화시키고, 대화방을 보이지 않게 sender_is_deleted
/ receiver_is_deleted
플래그를 설정하는 것이 핵심이다.
@transaction.atomic
def delete_room(me: User, other: User):
# 1. 내가 보낸 사람으로 만든 채팅 방이 있는지 확인한다.
try:
room = Room.objects.get(sender_user=me, receiver_user=other)
is_sender = True
except Room.DoesNotExist:
# 2. 내가 받는 사람으로 만든 채팅 방이 있는지 확인한다.
try:
room = Room.objects.get(sender_user=other, receiver_user=me)
is_sender = False
except Room.DoesNotExist:
return
if is_sender:
# 읽음 처리는 안함
# 메시지 삭제 처리 - 상대방이 삭제 아직 안한 경우
Message.objects.filter(
room=room,
visible=Message.VisibleChoices.BOTH,
).update(
visible=Message.VisibleChoices.ONLY_RECEIVER,
modified=now(),
)
# 메시지 삭제 처리 - 상대방이 이미 삭제한 경우
Message.objects.filter(
room=room,
visible=Message.VisibleChoices.ONLY_SENDER,
).update(
visible=Message.VisibleChoices.NOBODY,
modified=now(),
)
# 대화방 비활성화
room.sender_unread_count = 0
room.sender_is_deleted = True
room.save()
else:
# 읽음 처리는 안함
# 메시지 삭제 처리 - 상대방이 삭제 아직 안한 경우
Message.objects.filter(
room=room,
visible=Message.VisibleChoices.BOTH,
).update(
visible=Message.VisibleChoices.ONLY_SENDER,
modified=now(),
)
# 메시지 삭제 처리 - 상대방이 이미 삭제한 경우
Message.objects.filter(
room=room,
visible=Message.VisibleChoices.ONLY_RECEIVER,
).update(
visible=Message.VisibleChoices.NOBODY,
modified=now(),
)
# 대화방 비활성화
room.receiver_unread_count = 0
room.receiver_is_deleted = True
room.save()
그 다음으로는 대화 방 리스트를 가져오는 부분이다. 내가 sender_user
일 수도 있고, receiver_user
일 수도 있기 때문에, UNION ALL을 걸어서 가져오는 부분이 핵심이다. -modified
으로 정렬함으로서 가장 최신에 대화가 온 사람을 먼저 보여주었다.
def get_room_list(user: User):
q1 = Room.objects.filter(sender_user=user, sender_is_deleted=False).annotate(
user=F('receiver_user'),
unread_count=F('sender_unread_count'),
).values('user', 'unread_count', 'latest_text', 'modified')
q2 = Room.objects.filter(receiver_user=user, receiver_is_deleted=False).annotate(
user=F('sender_user'),
unread_count=F('receiver_unread_count'),
).values('user', 'unread_count', 'latest_text', 'modified')
return q1.union(q2, all=True).order_by('-modified')
마지막으로, 대화 목록을 가져오는 부분이다. 대화 목록을 가져온다는 것은 대화를 읽었다는 것이기 때문에 대화 읽음 처리를 해주는 것을 볼 수 있다. 여기서 흥미로운 점은 1:1 대화에서는 양쪽 다 메시지를 읽지 않는 상황이 안 생긴다는 점이다. 항상 한 명만이 메시지를 읽지 않거나, 메시지를 모두 읽는다. 그리고 내가 보낸 메시지는 항상 읽음 처리가 되어 있다. 그래서 was_read
부분의 분기가 재미있다.
@transaction.atomic
def get_message_list(me: User, other: User):
try:
room = Room.objects.get(sender_user=me, receiver_user=other)
is_sender = True
except Room.DoesNotExist:
# 여기서 실패하면 raise
room = Room.objects.get(sender_user=other, receiver_user=me)
is_sender = False
if is_sender:
# 이전 메시지 읽음 처리
Message.objects.filter(
room=room,
is_from_sender=False,
read=False,
).update(read=True, modified=now())
# 대화방 읽음 처리
room.sender_unread_count = 0
room.save()
# 내가보낸 메시지는 항상 나는 읽었음 -> 안궁금함
# 내가보낸 메시지를 저사람이 읽었는가??? 그것이 궁금한것임.
# 상대방이 보낸 메시지는 항상 상대방이 읽었음
# 내가 보낸 메시지만 적용하면됨
return Message.objects.filter(
room=room,
visible__in=(Message.VisibleChoices.BOTH, Message.VisibleChoices.ONLY_SENDER),
).annotate(
sent_by_me=F('is_from_sender'), # 반대에는 Not 붙이기
was_read=Case(
When(is_from_sender=True, then=F('read')),
default=True,
)
).values('id', 'sent_by_me', 'was_read', 'text', 'created').order_by('-id')
else:
# 이전 메시지 읽음 처리
Message.objects.filter(
room=room,
is_from_sender=True,
read=False,
).update(read=True, modified=now())
# 대화방 읽음 처리
room.receiver_unread_count = 0
room.save()
# 내가보낸 메시지는 항상 나는 읽었음 -> 안궁금함
# 내가보낸 메시지를 저사람이 읽었는가??? 그것이 궁금한것임.
# 상대방이 보낸 메시지는 항상 상대방이 읽었음
# 내가 보낸 메시지만 적용하면됨
return Message.objects.filter(
room=room,
visible__in=(Message.VisibleChoices.BOTH, Message.VisibleChoices.ONLY_RECEIVER),
).annotate(
sent_by_me=Case(When(is_from_sender=False, then=True), default=False), # not 지원안함 -- case when으로 때움
was_read=Case(
When(is_from_sender=False, then=F('read')),
default=True,
)
).values('id', 'sent_by_me', 'was_read', 'text', 'created').order_by('-id')
테스트
기능이 복잡함에도, TDD 방식을 적용해서 코드를 작성해 보았더니 정신줄을 놓지 않고 빠르게 로직을 작성할 수 있었다. 물론 급하게 짜느라 코드 정리는 안했다. ㅋㅋ 중복 코드 엄청 많지만 사이드 프로젝트인데 뭐 어떤가? 잘 동작만 하면 됐지~~
from django.test import TestCase
from message.models import Room, Message
from message.services import send_message, delete_room, get_room_list, get_message_list
from users.models import User
class TestMessageSend(TestCase):
def test_send(self):
# Given: 보낸 사람, 받는 사람 생성
sender = User.objects.create_user('sender')
receiver = User.objects.create_user('receiver')
text = 'message from sender to receiver'
# When: sender -> receiver 메시지 발송
send_message(sender, receiver, text)
# Then: 방이 잘 만들어짐
room = Room.objects.get()
self.assertEqual(room.sender_user, sender)
self.assertEqual(room.sender_unread_count, 0)
self.assertEqual(room.sender_is_deleted, False)
self.assertEqual(room.receiver_user, receiver)
self.assertEqual(room.receiver_unread_count, 1)
self.assertEqual(room.receiver_is_deleted, False)
self.assertEqual(room.latest_text, text)
# Then: 메시지가 잘 보내짐
message = Message.objects.get()
self.assertEqual(message.room, room)
self.assertEqual(message.is_from_sender, True)
self.assertEqual(message.text, text)
self.assertEqual(message.read, False)
self.assertEqual(message.visible, Message.VisibleChoices.BOTH)
def test_send_and_receive(self):
# Given: sender -> receiver 으로 전송
self.test_send()
sender = User.objects.get(username='sender')
receiver = User.objects.get(username='receiver')
text = 'message from receiver to sender'
# When: receiver -> sender 메시지 발송
send_message(receiver, sender, text)
# Then: 방이 잘 업데이트됨
room = Room.objects.get()
self.assertEqual(room.sender_user, sender)
self.assertEqual(room.sender_unread_count, 1)
self.assertEqual(room.sender_is_deleted, False)
self.assertEqual(room.receiver_user, receiver)
self.assertEqual(room.receiver_unread_count, 0)
self.assertEqual(room.receiver_is_deleted, False)
self.assertEqual(room.latest_text, text)
# Then: 메시지가 잘 보내짐
message = Message.objects.last()
self.assertEqual(message.room, room)
self.assertEqual(message.is_from_sender, False)
self.assertEqual(message.text, text)
self.assertEqual(message.read, False)
self.assertEqual(message.visible, Message.VisibleChoices.BOTH)
# Then: 이전 메시지가 읽음 처리 됨
message = Message.objects.first()
self.assertEqual(message.read, True)
def test_send_and_receive_and_send(self):
# Given: sender -> receiver -> sender 으로 전송
self.test_send_and_receive()
sender = User.objects.get(username='sender')
receiver = User.objects.get(username='receiver')
text = 'message from sender to receiver 2'
# When: sender -> receiver 메시지 발송
send_message(sender, receiver, text)
# Then: 방이 잘 업데이트됨
room = Room.objects.get()
self.assertEqual(room.sender_user, sender)
self.assertEqual(room.sender_unread_count, 0)
self.assertEqual(room.sender_is_deleted, False)
self.assertEqual(room.receiver_user, receiver)
self.assertEqual(room.receiver_unread_count, 1)
self.assertEqual(room.receiver_is_deleted, False)
self.assertEqual(room.latest_text, text)
# Then: 메시지가 잘 보내짐
message = Message.objects.last()
self.assertEqual(message.room, room)
self.assertEqual(message.is_from_sender, True)
self.assertEqual(message.text, text)
self.assertEqual(message.read, False)
self.assertEqual(message.visible, Message.VisibleChoices.BOTH)
# Then: 이전 메시지가 읽음 처리 됨
message = Message.objects.all()[1]
self.assertEqual(message.read, True)
class TestRoomRemove(TestCase):
def test_send_and_remove_by_sender(self):
# Given: 메시지 전송
TestMessageSend().test_send()
sender = User.objects.get(username='sender')
receiver = User.objects.get(username='receiver')
# sender_unread_count 강제로 임의로 설정
Room.objects.all().update(sender_unread_count=1)
# When: sender 가 삭제
delete_room(sender, receiver)
# Then: 삭제 처리됨 ; 방은 삭제처리, 메시지 ONLY_RECEIVER 삭제처리됨
message = Message.objects.get()
self.assertEqual(message.read, False)
self.assertEqual(message.visible, Message.VisibleChoices.ONLY_RECEIVER)
# sender 만 제거됨
room = Room.objects.get()
self.assertEqual(room.sender_unread_count, 0) # 강제로 0 셋팅 확인
self.assertEqual(room.sender_is_deleted, True)
# receiver 제거안됨
self.assertEqual(room.receiver_unread_count, 1)
self.assertEqual(room.receiver_is_deleted, False)
def test_send_and_remove_by_receiver(self):
# Given: 메시지 전송
TestMessageSend().test_send()
sender = User.objects.get(username='sender')
receiver = User.objects.get(username='receiver')
# receiver_unread_count 강제로 임의로 설정
Room.objects.all().update(receiver_unread_count=1)
# When: receiver 가 삭제 (안읽씹)
delete_room(receiver, sender)
# Then: 삭제 처리됨 ; 방은 삭제처리, 메시지 ONLY_SENDER 삭제처리됨
message = Message.objects.get()
self.assertEqual(message.read, False)
self.assertEqual(message.visible, Message.VisibleChoices.ONLY_SENDER)
# sender 제거안됨
room = Room.objects.get()
self.assertEqual(room.sender_unread_count, 0)
self.assertEqual(room.sender_is_deleted, False)
# receiver 제거처리
self.assertEqual(room.receiver_unread_count, 0)
self.assertEqual(room.receiver_is_deleted, True)
def test_send_and_remove_by_sender_and_remove_by_receiver(self):
# Given
self.test_send_and_remove_by_sender()
sender = User.objects.get(username='sender')
receiver = User.objects.get(username='receiver')
# receiver_unread_count 강제로 임의로 설정
Room.objects.all().update(receiver_unread_count=1)
# When
delete_room(receiver, sender)
# Then: 삭제 처리됨 ; 방은 삭제처리, 메시지 NOBODY 삭제처리됨
message = Message.objects.get()
self.assertEqual(message.read, False)
self.assertEqual(message.visible, Message.VisibleChoices.NOBODY)
# sender 제거됨
room = Room.objects.get()
self.assertEqual(room.sender_unread_count, 0)
self.assertEqual(room.sender_is_deleted, True)
# receiver 제거처리
self.assertEqual(room.receiver_unread_count, 0) # updated
self.assertEqual(room.receiver_is_deleted, True)
def test_send_and_remove_by_receiver_and_remove_by_sender(self):
self.test_send_and_remove_by_receiver()
sender = User.objects.get(username='sender')
receiver = User.objects.get(username='receiver')
# sender_unread_count 강제로 임의로 설정
Room.objects.all().update(sender_unread_count=1)
# When
delete_room(sender, receiver)
# Then: 삭제 처리됨 ; 방은 삭제처리, 메시지 NOBODY 삭제처리됨
message = Message.objects.get()
self.assertEqual(message.read, False)
self.assertEqual(message.visible, Message.VisibleChoices.NOBODY)
# sender 제거됨
room = Room.objects.get()
self.assertEqual(room.sender_unread_count, 0)
self.assertEqual(room.sender_is_deleted, True)
# receiver 제거처리
self.assertEqual(room.receiver_unread_count, 0)
self.assertEqual(room.receiver_is_deleted, True)
class TestGetRoomList(TestCase):
def test_send_and_read(self):
# Given: 보낸 사람, 받는 사람 생성; sender -> receiver 메시지 발송
sender = User.objects.create_user('sender')
receiver = User.objects.create_user('receiver')
text = 'message from sender to receiver'
send_message(sender, receiver, text)
# When: sender 가 메시지 읽음
sender_result = get_room_list(sender)
# Then: sender에게는 receiver가 보임
sender_result = list(sender_result)
for d in sender_result:
del d['modified']
self.assertEqual(
sender_result,
[{'latest_text': 'message from sender to receiver', 'user': receiver.id, 'unread_count': 0}]
)
# When: receiver 가 메시지 읽음
receiver_result = get_room_list(receiver)
# Then: receiver에게는 sender이 보임
receiver_result = list(receiver_result)
for d in receiver_result:
del d['modified']
self.assertEqual(
receiver_result,
[{'latest_text': 'message from sender to receiver', 'user': sender.id, 'unread_count': 1}]
)
def test_send_and_reply_and_read(self):
# Given: 보낸 사람, 받는 사람 생성; sender -> receiver 메시지 발송
sender = User.objects.create_user('sender')
receiver = User.objects.create_user('receiver')
text = 'message from sender to receiver'
send_message(sender, receiver, text)
text2 = 'message from receiver to sender'
send_message(receiver, sender, text2)
# When: sender 가 메시지 읽음
sender_result = get_room_list(sender)
# Then: sender에게는 receiver이 보임
sender_result = list(sender_result)
for d in sender_result:
del d['modified']
self.assertEqual(
sender_result,
[{'latest_text': 'message from receiver to sender', 'user': receiver.id, 'unread_count': 1}]
)
# When: receiver 가 메시지 읽음
receiver_result = get_room_list(receiver)
# Then: receiver에게는 sender이 보임
receiver_result = list(receiver_result)
for d in receiver_result:
del d['modified']
self.assertEqual(
receiver_result,
[{'latest_text': 'message from receiver to sender', 'user': sender.id, 'unread_count': 0}]
)
def test_send_and_remove_by_sender_and_read(self):
# Given: 보낸 사람, 받는 사람 생성; sender -> receiver 메시지 발송
sender = User.objects.create_user('sender')
receiver = User.objects.create_user('receiver')
text = 'message from sender to receiver'
send_message(sender, receiver, text)
delete_room(sender, receiver)
# When: sender 가 메시지 읽음
sender_result = get_room_list(sender)
# Then: sender에게는 보이지 않음 (삭제했으므로)
sender_result = list(sender_result)
for d in sender_result:
del d['modified']
self.assertEqual(
sender_result,
[]
)
# When: receiver 가 메시지 읽음
receiver_result = get_room_list(receiver)
# Then: receiver에게는 sender이 보임
receiver_result = list(receiver_result)
for d in receiver_result:
del d['modified']
self.assertEqual(
receiver_result,
[{'latest_text': 'message from sender to receiver', 'user': sender.id, 'unread_count': 1}]
)
def test_send_and_remove_by_receiver_and_read(self):
# Given: 보낸 사람, 받는 사람 생성; sender -> receiver 메시지 발송
sender = User.objects.create_user('sender')
receiver = User.objects.create_user('receiver')
text = 'message from sender to receiver'
send_message(sender, receiver, text)
delete_room(receiver, sender)
# When: sender 가 메시지 읽음
sender_result = get_room_list(sender)
# Then: sender에게는 receiver가 보임
sender_result = list(sender_result)
for d in sender_result:
del d['modified']
self.assertEqual(
sender_result,
[{'latest_text': 'message from sender to receiver', 'user': receiver.id, 'unread_count': 0}]
)
# When: receiver 가 메시지 읽음
receiver_result = get_room_list(receiver)
# Then: receiver에게는 sender이 안보임 (삭제했으므로)
receiver_result = list(receiver_result)
for d in receiver_result:
del d['modified']
self.assertEqual(
receiver_result,
[]
)
def test_send_and_remove_by_sender_and_remove_by_receiver_and_read(self):
# Given: 보낸 사람, 받는 사람 생성; sender -> receiver 메시지 발송
sender = User.objects.create_user('sender')
receiver = User.objects.create_user('receiver')
text = 'message from sender to receiver'
send_message(sender, receiver, text)
delete_room(receiver, sender)
delete_room(sender, receiver)
# When: sender 가 메시지 읽음
sender_result = get_room_list(sender)
# Then: sender에게는 receiver가 안보임 (삭제했으므로)
sender_result = list(sender_result)
for d in sender_result:
del d['modified']
self.assertEqual(
sender_result,
[]
)
# When: receiver 가 메시지 읽음
receiver_result = get_room_list(receiver)
# Then: receiver에게는 sender이 안보임 (삭제했으므로)
receiver_result = list(receiver_result)
for d in receiver_result:
del d['modified']
self.assertEqual(
receiver_result,
[]
)
def test_send_and_remove_by_sender_and_remove_by_receiver_and_read(self):
# Given: 보낸 사람, 받는 사람 생성; sender -> receiver 메시지 발송
sender = User.objects.create_user('sender')
receiver = User.objects.create_user('receiver')
text = 'message from sender to receiver'
send_message(sender, receiver, text)
delete_room(sender, receiver)
delete_room(receiver, sender)
# When: sender 가 메시지 읽음
sender_result = get_room_list(sender)
# Then: sender에게는 receiver가 안보임 (삭제했으므로)
sender_result = list(sender_result)
for d in sender_result:
del d['modified']
self.assertEqual(
sender_result,
[]
)
# When: receiver 가 메시지 읽음
receiver_result = get_room_list(receiver)
# Then: receiver에게는 sender이 안보임 (삭제했으므로)
receiver_result = list(receiver_result)
for d in receiver_result:
del d['modified']
self.assertEqual(
receiver_result,
[]
)
class TestGetMessageList(TestCase):
def test_send_and_get_read(self):
# Given: 보낸 사람, 받는 사람 생성; sender -> receiver 메시지 발송
sender = User.objects.create_user('sender')
receiver = User.objects.create_user('receiver')
text = 'message from sender to receiver'
send_message(sender, receiver, text)
# When: sender 가 메시지 읽음
self.assertEqual(Room.objects.get().sender_unread_count, 0)
self.assertEqual(Room.objects.get().receiver_unread_count, 1)
sender_result = get_message_list(sender, receiver)
# Then: 메시지 정상적으로 불러옴
sender_result = list(sender_result)
for d in sender_result:
del d['created']
self.assertEqual(
sender_result,
[{'id': 1, 'text': 'message from sender to receiver', 'sent_by_me': True, 'was_read': False}]
)
# When: receiver 가 메시지 읽음
receiver_result = get_message_list(receiver, sender)
self.assertEqual(Room.objects.get().sender_unread_count, 0)
self.assertEqual(Room.objects.get().receiver_unread_count, 0)
# Then: 메시지 정상적으로 불러옴
receiver_result = list(receiver_result)
for d in receiver_result:
del d['created']
self.assertEqual(
receiver_result,
[{'id': 1, 'text': 'message from sender to receiver', 'sent_by_me': False, 'was_read': True}]
)
# When: sender 가 메시지 읽음
sender_result = get_message_list(sender, receiver)
self.assertEqual(Room.objects.get().sender_unread_count, 0)
self.assertEqual(Room.objects.get().receiver_unread_count, 0)
# Then: 메시지 정상적으로 불러옴 - was_read 업데이트됨
sender_result = list(sender_result)
for d in sender_result:
del d['created']
self.assertEqual(
sender_result,
[{'id': 1, 'text': 'message from sender to receiver', 'sent_by_me': True, 'was_read': True}]
)
def test_send_and_reply_and_get_read(self):
# Given: 보낸 사람, 받는 사람 생성; sender -> receiver 메시지 발송
sender = User.objects.create_user('sender')
receiver = User.objects.create_user('receiver')
text = 'message from sender to receiver'
send_message(sender, receiver, text)
text2 = 'message from receiver to sender'
send_message(receiver, sender, text2)
# When: receiver 가 메시지 읽음
self.assertEqual(Room.objects.get().sender_unread_count, 1)
self.assertEqual(Room.objects.get().receiver_unread_count, 0)
receiver_result = get_message_list(receiver, sender)
# Then: 메시지 정상적으로 불러옴
receiver_result = list(receiver_result)
for d in receiver_result:
del d['created']
self.assertEqual(
receiver_result,
[{'id': 2, 'text': 'message from receiver to sender', 'sent_by_me': True, 'was_read': False},
{'id': 1, 'text': 'message from sender to receiver', 'sent_by_me': False, 'was_read': True}]
)
self.assertEqual(Room.objects.get().sender_unread_count, 1)
self.assertEqual(Room.objects.get().receiver_unread_count, 0)
# When: sender 가 메시지 읽음
sender_result = get_message_list(sender, receiver)
# Then: 메시지 정상적으로 불러옴
sender_result = list(sender_result)
for d in sender_result:
del d['created']
self.assertEqual(
sender_result,
[{'id': 2, 'text': 'message from receiver to sender', 'sent_by_me': False, 'was_read': True},
{'id': 1, 'text': 'message from sender to receiver', 'sent_by_me': True, 'was_read': True}]
)
self.assertEqual(Room.objects.get().sender_unread_count, 0)
self.assertEqual(Room.objects.get().receiver_unread_count, 0)
# When: 다시 receiver 가 메시지 읽음
receiver_result = get_message_list(receiver, sender)
# Then: 메시지 정상적으로 불러옴 - was_read 업데이트됨
receiver_result = list(receiver_result)
for d in receiver_result:
del d['created']
self.assertEqual(
receiver_result,
[{'id': 2, 'text': 'message from receiver to sender', 'sent_by_me': True, 'was_read': True},
{'id': 1, 'text': 'message from sender to receiver', 'sent_by_me': False, 'was_read': True}]
)
class TestGetMessageListAfterRemove(TestCase):
def test_send_and_remove_by_sender_and_get_read(self):
# Given: 보낸 사람, 받는 사람 생성; sender -> receiver 메시지 발송, sender 메시지 삭제
sender = User.objects.create_user('sender')
receiver = User.objects.create_user('receiver')
text = 'message from sender to receiver'
send_message(sender, receiver, text)
delete_room(sender, receiver)
# When: sender 가 메시지 읽음
sender_result = get_message_list(sender, receiver)
# Then: 메시지 정상적으로 불러옴
sender_result = list(sender_result)
for d in sender_result:
del d['created']
self.assertEqual(
sender_result,
[]
)
# When: receiver 가 메시지 읽음
receiver_result = get_message_list(receiver, sender)
# Then: 메시지 정상적으로 불러옴
receiver_result = list(receiver_result)
for d in receiver_result:
del d['created']
self.assertEqual(
receiver_result,
[{'id': 1, 'text': 'message from sender to receiver', 'sent_by_me': False, 'was_read': True}]
)
# When: sender 가 메시지 읽음
sender_result = get_message_list(sender, receiver)
# Then: 메시지 정상적으로 불러옴 - was_read 업데이트됨
sender_result = list(sender_result)
for d in sender_result:
del d['created']
self.assertEqual(
sender_result,
[]
)
def test_send_and_remove_by_receiver_and_get_read(self):
# Given: 보낸 사람, 받는 사람 생성; sender -> receiver 메시지 발송
sender = User.objects.create_user('sender')
receiver = User.objects.create_user('receiver')
text = 'message from sender to receiver'
send_message(sender, receiver, text)
delete_room(receiver, sender)
# When: sender 가 메시지 읽음
sender_result = get_message_list(sender, receiver)
# Then: 메시지 정상적으로 불러옴
sender_result = list(sender_result)
for d in sender_result:
del d['created']
self.assertEqual(
sender_result,
[{'id': 1, 'text': 'message from sender to receiver', 'sent_by_me': True, 'was_read': False}]
)
# When: receiver 가 메시지 읽음
receiver_result = get_message_list(receiver, sender)
# Then: 메시지 정상적으로 불러옴
receiver_result = list(receiver_result)
for d in receiver_result:
del d['created']
self.assertEqual(
receiver_result,
[]
)
# When: sender 가 메시지 읽음
sender_result = get_message_list(sender, receiver)
# Then: 메시지 정상적으로 불러옴 - was_read 업데이트됨
sender_result = list(sender_result)
for d in sender_result:
del d['created']
self.assertEqual(
sender_result,
[{'id': 1, 'text': 'message from sender to receiver', 'sent_by_me': True, 'was_read': True}]
)
def test_send_and_remove_by_both_and_get_read(self):
# Given: 보낸 사람, 받는 사람 생성; sender -> receiver 메시지 발송
sender = User.objects.create_user('sender')
receiver = User.objects.create_user('receiver')
text = 'message from sender to receiver'
send_message(sender, receiver, text)
delete_room(receiver, sender)
delete_room(sender, receiver)
# When: sender 가 메시지 읽음
sender_result = get_message_list(sender, receiver)
# Then: 메시지 정상적으로 불러옴
sender_result = list(sender_result)
for d in sender_result:
del d['created']
self.assertEqual(
sender_result,
[]
)
# When: receiver 가 메시지 읽음
receiver_result = get_message_list(receiver, sender)
# Then: 메시지 정상적으로 불러옴
receiver_result = list(receiver_result)
for d in receiver_result:
del d['created']
self.assertEqual(
receiver_result,
[]
)
# When: sender 가 메시지 읽음
sender_result = get_message_list(sender, receiver)
# Then: 메시지 정상적으로 불러옴 - was_read 업데이트됨
sender_result = list(sender_result)
for d in sender_result:
del d['created']
self.assertEqual(
sender_result,
[]
)
맺으며
다시 한번 말하지만 위의 비즈니스 로직 코드, 테스트 코드는 모두 하루 만에 작성된 것임을 감안해 주길 바란다. 테스트가 모두 통과하기는 하지만, 내가 발견하지 못한 크리티컬한 버그가 있을 수 있다. 실무에서 복붙해서 쓰셔도 되긴 하는데 안정성은 보장 못한다. ㅋㅋㅋ
위에서는 데이터베이스 설계와 비즈니스 로직에 대해서만 생각을 하고 구현을 했다. 즉 REST API로 뽑아 먹을 정도만으로 작성을 해 두었다. 하지만 채팅의 묘미는 실시간성이 아닐까? 일단은 여기서 키보드에서 손을 떼지만, 나중에 개선을 해보게 된다면 실시간성을 구현해보는 데 중점을 둘 것 같다.
그나저나… 왜 센드버드가 대박을 치고 있는지 알거 같다. 채팅이라는 도메인은 생각보다 복잡하다. 1:1 만으로도 이 정도의 복잡도가 나오는데, 단체 채팅, 실시간 채팅이 곁들여지면 얼마나 더 복잡해질까?