Skip to content

[BE] 안읽은 사람수 계산하기

Jungmin edited this page Dec 14, 2023 · 1 revision

작성자 : 창한

🤷‍♂️ 각 채팅마다 읽은 사람 리스트를 저장하면 안되나..?

@창한 강 의 채팅 기능 개발 경험

예전에 안드로이드로 채팅 기능을 구현해본 경험이 있었습니다.

당시 Firebase를 사용해 서버리스로 개발했었는데, DB는 고사하고 자바도 제대로 모르던 초보시절이라 채팅에 대한 설계를 어떻게 가져가야할지 몰라서 아래 블로그의 코드를 (거의 그대로) 클론 코딩했던 경험이 있었습니다.

Reference: Firebase 기반 Android 메신저 앱 - DirectTalk9 [SW 개발이 좋은 사람:티스토리]

해당 블로그에서 설계된 구조는 각 채팅마다 읽은 사람의 리스트를 관리하고 이를 통해 읽지 않은 사람의 수를 보내주고 있었습니다.

그래서 Firebase도 MongoDB와 동일하게 NoSQL 중 document-oriented 기반으로 사용되어 문제없지 않을까..? 생각했습니다.

🤷‍♂️ 갱신은 어떻게…?

하지만 문제점이 많았습니다.

누군가가 채팅방에 접속하면 해당 유저가 읽지 않은 모든 채팅들에 해당 유저가 읽은 유저임을 추가해줬어야 합니다.

심지어 이 관리가 백엔드의 DB에만 적용되는 것이 아니라, 프론트엔드에서 관리하는 채팅 데이터들에도 갱신된 데이터를 전달할 필요가 있었어요.

말도 안되는 로직을 생각했었지만, 마땅히 해결책이 떠오르지 않았습니다.

🙋‍♂️ 멘토링

week3 멘토링 일지

그래서 멘토링 시간이 조언을 듣기로 했습니다.

역시나 이 방식은 말도 안되는 방식이니 “절대” 사용해선 안된다 라고 조언을 받았고, 로그아이디에 대한 학습을 하면 해답을 얻을 수 있을 것이라는 힌트를 받았습니다.

🤷‍♂️ 그래서 로그아이디가 뭐지?

처음에는 로그아이디 자체가 어떤 표준화된 규격이 있는건가 해서 로그아이디가 뭔지 찾아봤습니다.

하지만 관련 개념들을 찾아볼 수 없었습니다.

그래서 우리가 내린 결론은 “생성일 순으로 정렬 가능한 생성된 key”를 의미한다고 생각했습니다.

그러던 중, MongoDB의 ObjectId가 우리의 니즈를 충족시켜주고 있음을 알았습니다.

Reference ObjectId

부록
https://www.mongodb.com/docs/manual/reference/method/ObjectId/

👨‍💻 유저마다 마지막으로 읽은 채팅 기록하기

여기까지 생각하고나니 읽지 않은 사람의 수는 결국 순차적으로 증가하고, 시작~끝이 존재하는 구간 값임을 깨달을 수 있었습니다.

또 숫자가 0, 3, 1, 2 와 같이 비순차적으로 등장할 일이 없기도 합니다.

그래서 가장 오래전에 채탕방에서 나간 유저부터 인원을 누계해서 읽은 채팅 로그아이디를 넘겨주면 이에 맞게 렌더링할 수 있겠다 생각했습니다.

🤷‍♂️ 갱신은 언제 어떻게??

처음에는 단순히 채팅방에 접속할 때 읽은 채팅을 지우고, 접속이 끊어질 때 채팅방의 마지막 채팅을 마지막 읽은 채팅으로 기록하는 방법을 사용했습니다.

Feature/BE/#168: 사용자의 채팅방(소켓) 입장시 필요한 유저 관련 쿼리 개발 by Capoomaru · Pull Request #181 · boostcampwm2023/web03-LockFestival

(사실 여기서부터 개념이 아니라 MongoDB 쿼리문에 막혀 있었습니다….)

하지만, 새로운 유저가 들어오거나 퇴장할 때도 이 문제를 해결해야 했습니다.

그래서 아래와 같은 경우에 어떻게 변화하는지 검증했습니다.

  • 그룹에 추가(새로운 인원)
    • 방 입장 당시에 있던 채팅까지는 기본으로 읽었다고 생각
    • 입장 당시 마지막 채팅(입장메세지)를 마지막 읽은 로그아이디로 저장
    • 갱신된 정보를 채팅방 내부 인원에게 전달
  • 그룹 나가기/강퇴(인원 감소)
    • 해당 유저의 마지막 읽은 로그아이디 제거
    • 갱신된 정보를 채팅방 내부 인원에게 전달
  • 채팅방 접속(connect)
    • 해당 유저의 마지막 읽은 로그아이디 제거
    • 갱신된 정보를 채팅방 내부 인원에게 전달
  • 채팅방 종료(disconnect)
    • 종료 당시 마지막 채팅을 마지막 읽은 로그아이디로 저장
    • 갱신된 정보를 채팅방 내부 인원에게 전달

🤷‍♂️ 중복 세션 처리

채팅방 중복 세션 처리

사용자가 브라우저 2개로 동일한 채팅방에 들어왔을 때 처리가 필요했습니다.

2 개의 세션이 존재했고 한 쪽에서 disconnect로 나갔다고 마지막 읽은 메세지를 갱신하면, 다른 쪽에서는 해당 방에 참여하고 있지 않은 것처럼 표기되고, connect/disconnect가 반복되면 에러를 발생시킬 여지가 있었습니다.

그래서 아래처럼 중복 세션을 검증하고 이에 대한 처리를 기존과 다르게 진행했습니다.

  • 채팅방 중복 접속(connect)
    • X(해당 유저의 마지막 읽은 로그아이디 제거) ⇒ 하지만 이미 삭제된 상태라 다시 실행해도 무관
  • 채팅방 중복 종료(disconnect)
    • 중복일 경우 수행 X

👨‍💻 코드에 적용하기

반환 데이터

프론트에 반환할 때 {[읽지 읽은 사람 수]: 변화되기 직전의 로그 아이디} 의 형태로 반환합니다.

이를 통해 프론트에서 각 채팅별 상수값을 저장&렌더링 하는 것이 아니라, 직접 동적으로 확인하여 렌더링할 수 있습니다.

//unreadCountMap
{
	1:"65734cf14971278141fb2944"
	2:"65734dc04971278141fb2a87"
}

Reference: 소켓 명세 Socket 채팅-2 읽지 않은 사람 수를 어떻게 계산할까?

계산하기

Untitled (3)

데이터베이스에서 가지고 있는 값은 아래와 같이 사용자별 마지막 읽은 메세지 id 입니다.

[
	"창한": "111",
	"정민": "113",
	"정석": null
]

Untitled (4)

이 값을 가지고 가장 오래된(작은) 값부터 누계해서 위와 같이 안읽은 사람을 계산할 수 있습니다.

makeUnreadCountMap(chatUsers: ChatUserInfoDto[]) {
  const countMap: { [k: string]: number } = chatUsers
    .filter((user: ChatUserInfoDto) => {
      return !!user.lastChatLogId;
    })
    .map(({ lastChatLogId }) => {
      return lastChatLogId;
    })
    .reduce((acc, cur) => {
      acc[cur] = acc[cur] ? acc[cur] + 1 : 1;
      return acc;
    }, {});

  let count: number = 0;
  return Object.entries(countMap)
    .sort(([key1], [key2]) => {
      return key1.localeCompare(key2);
    })
    .reduce((acc, [key, value]) => {
      count += value;
      acc[count] = key;
      return acc;
    }, {});
}

정렬된 로그아이디를 순서대로 누적 개수와 로그 아이디를 매핑하여 키값을 읽지않은 사람의 수와 로그아이디를 프론트에게 넘겨주었습니다.

부록

우리의 생각(ObjectId 사용)이 맞았다!!

개발 단계가 종료되고 해당 문서를 작성하기 위해 지난 검색 기록들을 살펴보다가 아래와 같은 블로그의 내용을 볼 수 있었습니다. Untitled (5)

멘토님께서 말씀하시고자 하던 로그아이디는 위와 같이 “시간 순서와 동일하게 정렬 가능한 ID값”을 의미하셨던 것 같습니다.

우리의 예측과 동일하게, ObjectId는 시간순 정렬이 가능한 값으로 구성되어 우리의 생각이 맞았음을 알 수 있었습니다.

Reference https://dong-life.tistory.com/117

Lock Festival 🔒

Rules

개발일지

Description

학습 노트

회의록

사전 회의
1주차 회의록
2주차 회의록
3주차 회의록
4주차 회의록
5주차 회의록
6주차 회의록

데일리 스크럼

1주차
2주차
3주차
4주차
5주차
6주차

회고록

1주차 회고록
2주차 회고록
3주차 회고록
4주차 회고록
5주차 회고록
6주차 회고록

스프린트

멘토링 일지

Clone this wiki locally