All Articles

[Django] 카카오톡 같은 채팅 시스템 데이터베이스 설계, 구현, 테스트 코드 만들어보기

사이드 프로젝트에서 간단한 채팅 기능이 필요해서, 카카오톡 같은 1:1 채팅을 하루만에 간단하게 구현해 보았다. 코드가 그리 깔끔하지는 않은데, 하루 만에 만든 것임을 감안하고 글을 읽어 주시기를 바란다. 처음에 생각했을 때에는 에이 ㅋㅋ 뭐 채팅 정도야 ㅋㅋ 쉽지 ㅋㅋ 이러면서 덤벼들었는데, 생각보다 데이터베이스 설계가 까다로웠다. 일단 생각해 봐야 할 것들이 꽤나 많았다. 본질적으로, 같은 대화 내용에 대해서 채팅 참여자들이 다르게 반응하는 것을 처리하는 것이 까다로웠다.

  • 내가 메시지를 읽는 행위가 나와 상대방 모두의 상태를 바꾼다. 나는 안 읽은 채팅 개수가 사라지고, 상대방은 1이 사라진다.
  • 보낸 메시지를 읽을 수도 있고, 안 읽을 수도 있다. 읽고 삭제할 수도 있고, 안 읽고 삭제할 수도 있다.
  • 내가 삭제한 대화 방을 상대방은 여전히 읽을 수 있다.
  • 대화 방을 삭제한 후에 메시지를 다시 주고받을 경우, 삭제한 사람은 그 이전 메시지를 읽을 수 없지만 삭제하지 않은 사람은 그 이전 메시지를 읽을 수 있다.
  • 둘 다 대화 방을 나가면 아무도 읽을 수 없게 된다.

개발 시간을 최대한 줄이기 위해 비정규화를 진행하여 1:1 채팅만을 지원하고, 단체 채팅은 지원하지 않는 것으로 스펙 확정 지었다.

데이터베이스 설계

한참 동안 생각해 보아도 명확한 설계가 떠오르지 않아, 구글링을 진행했다. 역시 다른 사람들이 고민해 둔 흔적들을 찾을 수 있었다. 그 중 너무 복잡한 것 말고 가장 심플하게 이 문제를 풀 수 있는 설계를 찾았다. Stack Overflow: Database Structure for Web Messaging System

  • header에는 대화 목록들이 저장된다.

    • from_id에는 최초에 보낸 사람이 저장된다. to_id에는 최초에 받는 사람이 저장된다. 즉, 대화방은 from_idto_id 의 조합으로 생성된다.
    • subject에는 마지막으로 보낸 문자가 저장된다.
    • status에는 누가 메시지를 마지막으로 보냈는지 저장된다.
    • time에는 마지막으로 메시지가 보내진 시간이 저장된다.
  • message에는 각 채팅 하나하나가 저장된다.

    • is_from_senderfrom_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 만으로도 이 정도의 복잡도가 나오는데, 단체 채팅, 실시간 채팅이 곁들여지면 얼마나 더 복잡해질까?