[Pet-Hub] 1:1 채팅 기능에서 채팅 읽음 여부 표시기능 구현

DevSeoRex·2023년 6월 17일
8
post-thumbnail

💯 해결해야 할 요구 사항

Pet-Hub 서비스는 사용자간 반려동물 분양을 위한 1:1 채팅 기능을 제공하고 있습니다.
프론트엔드 개발자분께서 채팅을 읽었는지에 대한 여부를 화면에 표시하기 위해 안읽음 기능 구현을 위한 readcount
json format에 추가해달라고 요청해주셨습니다.

접속중이지 않을때만 상대방에게 알람이 갈 수 있도록 해달라고 추가 요청을 주셨습니다.

그렇다면 A가 B에게 메시지를 보냈다면 아래와 같은 flow로 채팅이 동작해야 합니다.

  1. Client는 A의 메시지를 서버로 전달한다.
  2. 서버는 A의 채팅 상대방인 B가 서버에 접속중인지 확인한다.
  3. 접속중이라면 읽음 표시를 위해 readcount를 0으로, 비접속중이라면 1로 표시해준다.
  4. 메시지에 보낸 사람과, readcount 시간 등 정보를 채워 Client에게 전달한다.
  5. Client는 받은 메시지를 서버에 다시 보내준다.
  6. Server는 받은 메시지를 DB에 저장하고, readcount가 1일경우 상대에게 알람을 보내준다.

그렇다면 두가지 요구사항을 만족시켜야 합니다.

  1. 현재 채팅방에 몇 명이 접속중인지 알 수 있어야한다.
  2. 채팅방에 회원이 입장하면 어딘가에서 접속 정보를 보관해줘야한다.

🧐 어떻게(How?) 이 문제를 해결할 것인가?

채팅방에 회원이 접속한다는 것은, Stomp를 이용해 connect(연결)하고 해당 채팅방의 번호를 이용해 메시지를 받기 위한 subscribe(구독) 작업을 하는 것을 말합니다.

Stomp에서 connect, send, disconnect, subscribe등의 이벤트가 발생하면 사전 작업을 해줄 수 있는 ChannelInterceptor를 구현해서 채팅방에 연결할때 접속한 유저의 정보를 Redis에 저장하기로 결정하였습니다.

왜 Redis를 사용하기로 결정했을까

채팅방에 접속한 회원의 데이터는 다른 데이터와는 성질이 다르다고 판단했습니다.
게시글이나 댓글, 분양글 같은 데이터는 한번 작성되면 자주 수정되거나 새로운 글이 지속적으로 작성되지 않습니다.

반면에 채팅방은 한 회원이 여러 채팅방을 가질 수 있고, 채팅방에 들어가고 나올때마다 Master DB에 접근하여 쓰기 작업을 한다면 DB에 많은 부담을 줄 수 있다고 생각하여 Redis를 이용하기로 결정했습니다.

😈 StompHandler와 Redis를 이용한 채팅 안읽음 기능 구현

StompHandler의 구현

  • ChannelInterceptor의 preSend 메서드를 먼저 Overriding 해줍니다.

  • Stomp의 이벤트에 따라서 특정 작업을 해주는 handleMessage 메서드를 작성합니다.

stompCommand가 CONNECT(연결 시도)일때 connectToChatRoom 메서드가 실행되게 됩니다.

connectToChatRoom 메서드는 크게 3가지 동작을 하게 됩니다.

  1. Redis에 현재 채팅방에 접속하려는 회원을 저장한다.
  2. 현재 채팅방에 접속하려는 회원이 읽지 않은 채팅이 있다면, 전부 읽음 처리 해준다(readcount 0으로 업데이트)
  3. 현재 채팅방에 접속중인 회원이 있다면, 채팅 리스트 다시 서버에 요청해서 받도록 처리한다.

현재 채팅방에 접속중인 회원이 있다면, 채팅 리스트를 다시 서버에 요청해서 받도록 한 이유는 아래와 같습니다.

현재 채팅방에 접속중인 회원을 A, 현재 채팅방에 접속하려는 회원을 B라고 가정하겠습니다.

A의 화면에서 B가 읽지 않은 채팅들이 안읽음 표시되게 됩니다.

B가 채팅방에 접속을 시도하면, ChannelInterceptor에 Connect 이벤트가 잡히게 되고, B가 읽지 않은 채팅들은 모두 읽음 처리 됩니다.

즉, A는 B가 채팅방에 접속하면서 채팅을 모두 읽었는데 읽지 않은 상태의 데이터를 아직 가지고 있는 것입니다.

따라서 B가 접속하면서 모든 채팅을 읽음처리 했기때문에, A에게 B가 모든 채팅을 읽었으니 최신 버전의 채팅 데이터를 서버에서 가져올 수 있도록 서버에서 알려주게 처리하였습니다.

isConnected 메서드는 현재 채팅방에 한명이 접속중일때 true를 반환하도록 작성했습니다.

sendMessage 메서드 구현

ChatService에서 메시지 전송을 시도하는 sendMessage 메서드의 구현부분입니다.

isAllConnected 메서드를 이용해 채팅방에 모든 유저가 참여중인지 확인합니다.
1:1 채팅이기 때문에 접속자가 2명이면 readCount는 0, 접속자가 1명이면 readCount는 1입니다.

sendNotificationAndSaveMessage 메서드 구현

ChatService에서 메시지를 DB에 저장하고 필요시 알람을 보내는 sendNotificationAndSaveMessage 메서드의 구현 부분입니다.

가장 먼저 메시지를 보낼때 DB에 메시지를 저장하고, 알람을 발송하지 않고 메시지가 Client에 잘 도달하고 callback 함수가 동작할때 다시 서버로 요청을 보내도록한 이유를 말씀드리겠습니다.

메시지를 보내는 sendMessage 메서드에서 메시지를 DB에 저장하도록 개발하였을때 메시지가 서버로 잘 오지만, Client로 되돌아가지 않는 문제가 여러차례 발생한 적이 있었습니다.

DB에는 메시지가 저장됬지만, 화면에는 보여지지 않아서 서로 데이터가 일치하지 않는 문제가 발생해서 해결점을 찾기 위해 고민한 결과, Client로 메시지가 확실히 도달하지 않으면 DB에 메시지를 저장하지 않기로 결정하였습니다.

알람 부분에 있어서는, 모든 참여자가 채팅방에 접속 중이라면 모두 메시지를 읽을 것이고 알람이 필요하지 않기 때문에 알람을 보내지 않도록 처리했습니다.

🤔 그러면 잘 동작할까?

채팅방에 한 명만 접속해있을때


한명이 접속중이지 않기 때문에 알람이 동작하고 있는 걸 확인할 수 있습니다.


한명이 채팅방에서 나가있으면 메시지가 읽음 처리 되지 않고 있습니다.

채팅방에 두명 다 접속해있을때


채팅방에 두명 다 접속해 있을 경우, 메시지가 즉시 읽음 처리 되고 있는 것을 볼 수 있습니다.


두명 다 접속해 있을때 도착한 채팅에 대해서는 알람이 발행되지 않는 것도 확인했습니다.

😊 문제를 해결하며 느낀점

채팅을 개발하다 보니, 실시간으로 여러 대화들이 오가고 그로 인해서 크고 작은 이슈들을 많이 겪게 된 것 같습니다.

이번 안읽음 기능과, 채팅 데이터 불일치 문제를 polling 방식으로 해결해야 하는지 고민을 많이 했었는데 주기적으로 서버에 요청을 보내서 확인하는 것은 사용자가 늘어날수록 서버에 부담이 커질 수 있다는 점이 마음에 걸려서 많은 고민을 했는데, Redis로 채팅 접속자를 관리하는 유연한 방법을 통해서 문제를 해결하게 되어 유익한 시간이였던 것 같습니다.

오늘도 읽어주셔서 감사합니다.

🙇

2개의 댓글

comment-user-thumbnail
2023년 9월 28일

안녕하세요! 저도 Spring과 React를 사용하여 웹 1:1채팅 구현중이며 Stomp를 사용중입니다.
(Redis는 사용하지 않고있습니다.)

올려주신 서민재님 방식대로 구현 한다면 소켓의 connect / disconnect 라이프사이클이 채팅방에 입장하고 퇴장할때 합쳐서 하나의 주기로 이루어져야 구현이 가능한것으로 추측되는데요.

소켓 라이프 사이클에서 1회 연결되는 시간이 생각보다 빠르지 않더라구요
제가 추측한 라이프사이클 방식으로 구현하셨는지 여쭤봅니다^^

이게 어떨때는 10~15초이상 걸릴때도 있고, 이러한 딜레이로 인해 저같은 경우에는 1차원적인 판단으로 플랫폼에 로그인하는 순간 무조건 소켓이 연결되도록 구현을 했거든요.

방이 여러개가 있을수 있고 해당 방을 주기적으로 관리해야 하는 시스템인지라...
(프로젝트 파트별로 지원자를 받고 지원자가 모집자에게 채팅을통해 1차필터링을 거치는 시스템이라... 모집 파트가 많을수록 채팅방이 많이 생성되거든요...)

라이프사이클도 연결이 느리더라도 리소스 측면에서 로그인/로그아웃이 아닌 채팅방입장/퇴장으로 가는게 오히려 더 이상적인지도 여쭤봅니다!

1개의 답글