팀 프로젝트를 하다 보며 채팅에 대한 기능을 자주 언급되지만 막상 도입하는 일은 없었다.
따로 개인 프로젝트로 학습해보며 적용한 과정을 소개해보려 한다.
먼저 Spring에서는 SimpleBroker라고 하는 메시지 브로커를 활용해서 채팅을 구현할 수 있다.
다만, 멀티 서버 환경에서는 동기화 이슈가 발생할 수 있다.

따라서 외부 메시지 브로커인 RabbitMQ vs ActiveMQ vs Kafka vs Redis(pubsub)을 후보지로 두고 조사했다.
| 항목 | Kafka | RabbitMQ | ActiveMQ | Redis (Pub/Sub) |
|---|---|---|---|---|
| 메시징 모델 | 분산 로그 기반 Pub/Sub | 큐 기반 Pub/Sub | 큐 기반 Pub/Sub, JMS 기반 | 단순 Pub/Sub |
| 큐 존재 | O | O | O | X |
| 메시지 순서 보장 | O | O | O | X |
| 중복 메시지 처리 | O | O | O | X |
| 재전송/확인 | O | O | O | X |
| 라우팅 지원 | Topic + Partition | Exchange + Routing Key | Queue or Topic + Selector | 직접 구독자에게 broadcast 필요 |
| 운영 복잡도 | 높음 | 중간 | 중간 | 낮음 (구성 간단) |
| 표준 프로토콜 | 자체 프로토콜 (Kafka client) | AMQP, STOMP, MQTT 등 | AMQP, STOMP, MQTT 등 | 자체 Redis Pub/Sub |
| 확장성 | 수평 확장 매우 우수 (샤딩) | 가능하나 복잡 | 가능하나 복잡 | Redis Cluster 구성 필요 |
| 성능 (TPS) | 매우 높음 (수십만 TPS 이상) | 중간 (수천~수만 TPS) | 중간 (RabbitMQ와 유사) | 높음 (in-memory 처리) |
우선 단순 채팅을 적용하는 데에 Kafka는 러닝 커브가 높아보여서 제외했다.
간단하게 채팅을 구현하기에는 Redis도 간단하고 편리해보였지만, 채팅을 구현해보며 MQ를 학습해보고 싶었기에 Redis도 제외했다.
최종 후보지는 RabbitMQ vs ActiveMQ였다.
ActiveMQ는 JMS(Java Message Service) 기반으로 Java와 호환성이 뛰어나다. 반명 RabbitMQ는 다양한 언어, 환경에서 잘 호환되기 떄문에 상대적으로 많이 사용되고, 자료 또한 많았기 때문에, RabbitMQ를 채택하게 되었다.
메인 DB는 RDBMS-MySQL을 사용하지만, 채팅 내역만 MongoDB에 저장했다.
이유는 다음과 같다.
채팅은 기본적으로 데이터 삽입이 많고, 이 프로젝트에서 채팅은 수정 기능은 존재하지 않는다.
RDBM는 트랜잭션, 제약 조건 설정 등으로 인해 NoSQL보다 성능이 떨어질 것으로 판단
스키마가 존재하지 않기 때문에, 샤딩이 비교적 쉽다고 한다. 채팅 특성상 데이터 양이 방대해지기 쉽기 떄문에 수평 확장이 쉬운 NoSQL이 적합
Spring에서는 JPA와 비슷하게 사용이 가능한 Spring Data MongoDB를 지원해주기 떄문에 NoSQL 중에서 MongoDB를 채택하였다.
Redis는 메시지 브로커가 아닌 현재 채팅방의 접속 유저를 관리하기 위해 사용된다.
이 데이터는 각 채팅의 안읽은 수를 계산하기 위함이다.
다음과 같은 기능을 의미한다.

채팅 읽음 내역을 저장해서 이 기능을 구현할 수 있었지만, 상당히 비효율적이라고 판단했다.
채팅방 N개, 각 채팅방마다 채팅 M개, 각 채팅방의 인원 k명이라고 가정하고, 최악의 경우인 모든 채팅을 읽었을 때를 생각해보자.
DB에는 총 N*M*K개의 데이터가 쌓인다. 단순히 100개, 500개, 5명으로 계산해도 250,000개이다.
따라서 DB에는 유저마다 각 채팅방의 접속 시간과 현재 채팅방의 접속 중인 유저를 관리하여 계산하였다.
각 메시지의 안 읽은 수 = '채팅방 인원' - '현재 입장 중인 유저의 수' - '현재 입장 중이지 않은 유저 중 마지막 입장 시간>메시지 생성시간을 만족하는 유저의 수'를 통해 계산된다
여기서 '현재 채팅방의 접속 유저'는 Redis를 활용했고, 그 이유는 다음과 같다.
위에서 정한 서비스를 활용한 최종 플로우는 다음과 같다.

채팅방에 입장하면 채팅을 조회하게 되고, 각 채팅마다 안 읽은 수가 계산되어 반환된다.
하지만, 채팅 조회 이후에 새로운 유저가 채팅방에 입장하게 된다면 내가 조회했던 채팅의 안 읽은 수는 Old data가 될 것이다.
따라서 새로운 유저 채팅방에 입장했다면, 이미 입장해있는 채팅방 유저에게 싱크 요청 메시지를 보내, 메시지 재조회를 하도록 했다.
public enum MessageType {
CHAT_MESSAGE, CHAT_SYNC_REQUEST
}
구현 과정과 코드는 다음 포스팅에 이어서 작성해보겠습니다