프로젝트를 진행하면서 실시간 유저 조회에 대해서 어떻게 설계할 수 있을 지를 고민해봤다. websocket 방식과 polling 방식 중 고민을 해봤는데 polling 방식이 맞는 거 같아 어떻게 분석을 했는지 이야기를 해보고자 한다.
실시간 유저 조회의 특성 분석
실시간 유저 조회의 특성은 아래와 같다.
- 정확할 필요가 없다. 그 사람이 로그인해서 연락을 한 뒤 연락이 안 오다가 끊긴 표시로 바뀌면 그 사람이 나갔나보다 한다. 반대로 접속하지 않은 상태에서 접속한 상태로 바뀌는 것을 즉시 알 건 20-30초 뒤에 알 건 유저로서 불편함을 느끼지 않는다.
- 지속적으로 연결이 되어있는 지를 확인해야 한다. 마치 DB 서버 사이에서 서로 살아있는지 확인하기 위해 ping-pong하면서 health 체크하듯 각 유저 별로 체크를 해줘야 한다.
- health 체크마다 자신의 정보에 대해서 쓰고, 필요한 유저에 대해서 읽는다.
- 유저가 나가서 신호가 끊겼을 때 어떻게 표시할 수 있을 지를 생각해봐야 한다.
실시간 유저 조회를 위한 선택
- 쓰기가 자주 일어난다는 특성 때문에 MySQL 같은 RDB를 통해서 관리하기 보다는 DB를 통해서 관리하는 게 좋을 것이라 생각한다. 그래서 Redis를 선택했다.
- 자료구조는 메모리를 아낄거면 비트맵을 쓰라고도 이야기를 하지만, 사실 유저의 Key만 저장한다면 유저가 만 명 미만인 경우 key-value 길이가 50byte이라 할 때, MB 단위로도 가지 않는다. 따라서 key마다 개별로 O(1)의 속도로 TTL을 걸고 데이터를 가져올 수 있는 String 자료구조를 사용하는 게 적합하다 생각한다.
- mattermost와 같은 실시간 유저 현황을 보여주는 곳에서도 대략 실제 접속과 다른 사용자가 접속했는지 확인할 때까지 30초정도의 딜레이가 존재함을 보았다. 간격은 20초에서 30초 사이로 두어도 불편함을 느끼지 않는다고 판단을 했다. 만 명의 유저가 있다고 하더라도 모든 유저가 접속해 있지는 않을 것이다. 따라서 20퍼센트의 유저만 동시에 접속한다고 했을 때 초당 health check를 위해서 보내는 요청의 개수는 10000 X 0.2 X 0.05 = 100 정도의 TPS가 나온다. 따라서 polling으로 구현을 하더라도 문제가 없다.
※ 추가정보: polling 구조의 장점
redis를 여러 서버가 공유하게 되면 서버 자체는 실시간 유저 상태에 대해서 저장을 하지 않는다. 따라서, redis 서버가 처리하는데 문제가 없다고 할 때 글로벌 캐시 데이터 역할을 하기 때문에 실시간 유저를 확인할 유저가 많다면 scale out을 해도 Redis 서버의 TPS만 버틴다면 문제가 없다.
또한, 제목과는 다른 이야기지만 이전의 채팅 서버와 다르게 실시간 유저 여부는 중요한 데이터가 아니다. 채팅은 채팅 서버가 죽어버리는 순간 채팅방 정보와 채팅 기록 중 DB에 동기화되지 않은 데이터가 날아가는 것이고, 그냥 Redis 서버가 죽어버리거나 요청이 늦어져서 Timeout 나버리면 모두 다 접속 중이지 않다 등으로 처리해버리면 서비스 자체가 돌아가는 데에는 문제가 없다.

따라서, 지금 프로젝트에서는 그렇게 하지 않을 예정이지만 회사라면 별도로 아주 작은 인스턴스에 실시간 유저 확인용으로 Redis 서버를 별도로 하나 띄울 것 같다.
실시간 유저 로직
이 처리로직을 생각하면서 참고한 서비스는 mattermost라는 메신저 어플리케이션이다. 아래의 이미지를 참고하면 다음 두 부분을 볼 수 있다.
- 같이 채팅한 사람에 대해서 최신 채팅방 정보와 해당 사람의 실시간 접속 여부를 확인할 수 있다.
- 본인이 지금 보고 있는 채팅 부분에 대해서 실시간 접속 여부를 확인할 수 있어야 한다.

따라서, 해당 기능을 위해서는 채팅방 목록이나 채팅을 가져올 때 상대방에 대한 식별자 값을 key나 value에 넣어놓는다는 선행조건이 필요하다. 아마, 생각한 방식은 정말 별 거 없을 것이다.
API 동작 방식
- 프론트엔드 측에서는 주기적으로 실시간 유저를 확인하는 API를 호출한다. 실시간성이 크게 중요하지는 않고 그로 인한 자원 낭비가 클 가능성이 높으니 30초 이상을 권장한다.
- 프론트엔드는 서버로 사용자에 대한 토큰이 담긴 헤더와 내가 원하는 사용자의 식별자 List만 채팅방목록이나 채팅에서 꺼내서 넘겨준다.
- 백에서는 헤더에서 추출한 식별자로 Redis 서버에 String 자료구조로 식별자를 key로 하고 value는 아무 값이나 넣고 지정된 TTL을 준다. 아마도 45초-60초 사이로 아무 값이나 줄 것 같다.
- 넘겨 받은 식별자들에 대해서 쭉 내부에 값이 있는 지를 확인해보고 있다면 접속, 없다면 비접속으로 응답을 놓는다.
- Redis 서버로부터 응답이 오지 않는 경우 모두 비접속 처리로 응답을 보낸다. 대신, 로그를 남기고 Slack 알람을 보내서 확인하는 로직을 처리하여 빠르게 Redis 서버를 복구하고 원인을 파악하는 로직은 작성해주어야 할 것이다. 그 이유는 오류를 보낼 수도 있겠지만 서비스를 사용을 불가하는 중요한 이슈는 아니기 때문이다. 따라서, 실시간 체크용 Redis 서버만을 분리하는 것도 좋은 선택일 거라 생각된다.
※ String 자료구조 사용 이유?
String 자료구조를 사용하는 이유는 개별 값에 대해서 TTL을 걸 수 있기 때문이다. 메모리를 줄이기 위해서 bitmap과 같은 자료구조를 사용할 수도 있겠지만, 개별 TTL을 걸 수 없기 때문에 접속이 끊겼을 때 비접속으로 바꾸는 기준이 모호해지게 된다. 사용자가 수 많지 않은 경우 그렇게 큰 메모리를 사용할 일도 없다.