본문 바로가기

개발 프로젝트

NodeJS Chat Service

728x90

Preview

기존 NodeJS 기반 구성되어있던 채팅 서버가 장애 이슈와 개선점이 많아 프로젝트 리팩토링 및 버그 개선을 담당하게 됐다.

자주 발생하는 장애현상 은 이러했다.

  1. 고객의 네트워크 불안정으로 소켓 연결이 끊어진 다음 재접속 하는 과정에서 고객 네트워크를 다시 방에 연결하지 못하는 현상.
  2. 서버 측 소켓 연결 close 이벤트를 늦게 감지하는 현상

대부분이 네트워크 불안정을 해결하지 못해서 발생한 이슈였다.

개선해야 할 점 은 다음과 같다.

  1. 1대1채팅만을 지원했지만 상담사(1)대 고객(N) 채팅을 지원할 것.
  2. 카카오톡처럼 고객과의 과거 대화내역과 통계 내역을 볼 수 있어야 할 것.
  3. 각 상담사 별로 제공되는 기능이 존재할 것. (ex: 기본 메시지 설정).
  4. Javascript 에서 Typescript로의 전환

이번 포스팅에서는 장애현상 해결과정 및 개선점 구현 과정을 담으려고 한다.

Methodology

네트워크 불안정 이슈

프로세스 구성도

 

휴대폰은 가끔 네트워크 환경이 불안정하거나 와이파이가 바뀌는 경우가 있다. 이럴 때 서버와 맺어져 있던 웹소켓은 순간 끊어졌다 다시 인터넷이 연결되면 재연결(재접속)을 시도한다.
모바일의 경우 네트워크 문제뿐만 아니라 OS의 최적화 기능으로 인해 백그라운드에 내려가 있던 앱의 소켓이 끊어지는 경우도 있어 소켓이 끊어졌다가 다시 붙는 경우처럼 예기치 못하게 네트워크가 끊어질 경우에 대한 대비를 해놓아야 한다.

카카오나 라인과 같은 채팅 서비스의 경우 혹은 일반적인 채팅 서비스는 메시지의 전송 시점에 소켓 연결 체크를 해서 전송 처리를 할 수 있다지만
이런 상담사와 1대 1 채팅이 가능한 채팅방은 경우에 따라서 채팅을 종료시켜야 하는 경우가 있다.

가령, 사용자가 오랜 시간 메시지를 입력하지 않았거나 정상적으로 채팅을 종료하지 않은 경우 소켓 연결로만 판단하여 적절하게 채팅을 종료시켜야 상담사는 해당 고객의 이탈을 인지할 수 있다.

자사 서비스의 기존 로직은 사용자의 소켓이 끊어진 경우 채팅방 객체에서 사용자를 바로 제거하고 만약 방에 상담사가 남아있으면 방을 유지하되 상담사도 없는 방이라면 채팅방을 제거했다. 다시 사용자의 소켓이 연결(재접속)됐을때 해당 방이 남아있으면 재진입을 하고 없다면 다시 새로운 채팅 서비스를 시작한다.

 

이 때 문제는, 고객이 생성한 방에 상담사가 입장하기 전에 고객의 네트워크 불안정으로 인해 고객 소켓이 끊어져 방이 없어진다면 고객은 재접속 후에 대기열에서 빠져나와 다시 방을 만들어 대기해야 하는 현상이 있었다.
뿐만 아니라 고객과 상담사가 대화를 이어나가던 중 고객의 네트워크 불안정으로 클라이언트 소켓은 끊어졌다고 판단했지만 서버 소켓은 끊어짐 시그널이 늦게 발생해 이미 재접속 한 후에 소켓이 끊어지는 현상도 자주 발생했다.

이러한 이유로 ping pong 시그널을 1초단위로 체크해 소켓 끊어짐을 늦게 인지하는 현상은 해결했지만 고객이 많아지면 1초에 한번씩 한 소켓마다 발생하는 ping pong 시그널이 부하를 일으킬 수 있다고 판단해 다른 방법을 찾아야했다.
(구글링 결과 대부분의 ping-pong 체크 시간은 20-30초라고 한다.)

대기시간을 둔다?

ping pong 시간은 20-30 초로 설정하되 소켓이 끊어진 이후 바로 방에서 해당 고객을 제거하는 것이 아닌 10초 정도의 시간을 두고 만약 10초 뒤에 고객이 재접속했는지 검사해서 방에서 제거하는 방법으로 수정했다.

간단하게 소스코드로 보면

const socketClosed = () => {
  //socket 이 끊어지면 실행되는 함수
  const logoutFunc = () => {
    //로그아웃 시키는 함수 : 방 객체 제거, 유저객체 제거
  };
  setTimeout(() => {
    if (customerObj.reConnected === true) {
      //채팅 재개
    } else logoutFunc();
  }, 10 * 1000);
};

 

 

처음엔 이렇게 간단하게 해결한 줄 알았다. 하지만 이내 완벽하게 해결한 것이 아님을 깨닫고 새로운 방법을 찾아야 했다. 왜냐하면 고객의 소켓이 끊어진 뒤 10초 이내에 재접속했지만 한번 더 끊어졌는데 10초가 지난 상황이라면?
방에서 고객을 제거할 것이다. 1초만에 두 번째 재접속을 했더라도 말이다. 시간이 몇 초가 됐건 소켓 이탈 감지를 시간으로 판단하는건 옳은 방법이 아니었다.

소켓 < 고객?

고객이 채팅 서버에 로그인하면 고객 객체 생성해 소켓정보 해당 객체에 저장한다. 만약 소켓에서 이벤트(연결 혹은 종료, 메시지 발생 등)가 발생한다면 접속 고객목록에서 해당 소켓을 가진 고객을 찾아 처리한다. 그렇기에 네트워크 불안정으로 소켓이 종료되는 순간 해당 소켓을 가진 고객을 찾아 방에서 제거하는 로직이 가능했다.

하지만 나는 고객객체에 소켓을 저장하는것이 옳지 않다고 생각했다. 고객 목록과 소켓목록 두 가지 관리 포인트가 있는 셈이고 소켓 주인인 고객을 찾는 과정이 매번 발생했다.

소켓에 고객 정보를 담는게 더 좋지 않을까? 소켓 객체에 고객정보를 저장해 소켓 하나만 관리하면 더 편리할 것이다.
소켓 끊어짐 시그널이 늦게 발생해도 상관없다. 이미 해당 소켓이 아닌 다른 소켓을 가진 동일 고객이 해당 방에 진입해있으니 말이다.

1대 다 채팅 지원

UI 변경

상담사는 현재 상담중인 고객이 있으면 다른 상담방에 진입이 불가했다. 클라이언트 단에서 막고 있었던 것인데, 이 제한을 해제하고 나의 채팅방이라는 section 을 추가해 현재 진행중인 대화방 객체들이 저장되도록 했다.

과거 대화 내역 조회 및 통계

기존에는 고객과 과거 나눴던 채팅 내역을 보려면 업무 프로그램에 따로 접속해 데이터베이스 내용을 조회해서 봐야했다.
너무 번거로운 과정이었다.

과거 대화 내역 조회

대화방에 참여시 고객과 상담사 이름을 기준으로 대화 내역을 조회해 최대 30건의 텍스트를 카카오톡처럼 차례대로 쌓아두었다.
만약 추가로 더 봐야하는 경우가 있기 때문에 대시보드를 추가했다.

대시보드에는 과거 채팅 내역을 볼 수 있는 테이블을 배치했다.

통계 데이터

대시보드에는 통계 데이터도 추가했다. 평균 대화시간, 평균 고객 대기 시간 등의 정보를 d3 그래프를 활용해 보여주었다.

상담사 커스텀 화면

기본 메시지는 고객과 대화를 나눌 때 클릭만 해도 해당 메시지가 자동으로 입력되는 기능이다. 기존에는 모든 상담사에게 동일한 기본 메시지를 보여주었다. 하지만 상담사별로 대화 방법도 다를테고 인사법도 다를 것이기에..(온전히 개인적인 의견이었다.) 상담사별로 기본 메시지를 설정할 수 있는 기능을 추가했다.

하지만 기존에 있던 기본 메시지는 그대로 두되, 그 외의 메시지만 개인 설정 할 수 있게 해달라는 의견이 있어서 수긍하고 변경했다.
defaultMsg 라는 msgIdx 를 primary key 로 하는 테이블을 생성해 기본 메시지를 관리한다. CRUD 기능을 구현했다.

Javascript 에서 Typescript로의 전환

이건 개인적인 욕심이었다. Javascript 보단 Typescript 가 안정성을 중요시하는 채팅 서비스에 더 적절할 것이라 판단해 migration 을 진행했지만 내가 없으면 유지보수가 불가능하다는 점으로 인해 더 이상 진행하지 못하고 Javascript 기반으로 구현했다.

다만 디렉토리 구조는 리팩토링했다. 기능별로 나눠져 있던 파일 구조를 3-Layered Architecture 에 맞게 business - (controller - service -repository) - Database 로 변경했다.

3-Layered Architecture

-src - chat - controller
            - service
            - repository
-public

The End


비교적 적은 고객과 소수의 상담직원이 이용하는 서비스지만, 최대한 안정적이고 불편함 없게 개발 및 운영하기 위해 노력하고 있다.