실시간 채팅 서비스 도입에 관하여

상황, 문제 정의

프로젝트 진행 도중 실시간 채팅 서비스를 구축 하는 과정을 정리해 봤습니다. 코드 보다는 채팅을 진행하는 과정에서 기술을 쓴 이유와 겪었던 고민이나 장애를 정리해봤습니다.

채팅 서비스에 웹 소켓 , STOMP PUB/SUB 구조를 통한 서비스를 구현 하기 까지  과정과  Redis도입 과정과 실시간 채팅 구현 과정에서 고민을 정리했습니다.

먼저 채팅 서비스는 크게 3가지로 나누어져 있습니다.

  • STOMP ,Redis를 이용한 Pub/Sub구독
  • 채팅 로그(다른 회원에게 보낼 채팅 메세지 이 메세지는 DB에 저장한다.) 보내기
  • 추가적으로 채팅을 보낼 시 알림 계획( Spring Event를 활용한다.)

고민, 구현 과정

첫 번째 먼저 든 생각은 채팅을 하기 위해서는 polling, websocket 두 가지 중 선택해야겠다는 생각이 들었습니다.

  • WebSocket 방식은 Http가아닌  WS프로토콜을 사용하고 단순한 API로 통신이 가능하고 양방향성과 실시간성을 가지고 있습니다.
  • polling 방식(Long polling포함)은 자체가 서버에 주기적으로 HTTP 요청을 보내는 방식입니다.

요청을 지속적 연결을 통해 보내는 것이 아닌 일정 시간을 가지고 보냅니다.

이점이 실시간 채팅과는 맞지 않는다는 생각이 들었습니다. 또한 오히려  데이터가 자주 변경되면 서버에 부담감을 주어 비용도 만만치 않다고 합니다.

이벤트가 발생하게 되면 클라이언트가 요청을 동시에 보내게 되기 때문에 서버의 부담이 급증하게 됩니다. 만약 ScaleOut으로 서버를 확장한다면 부담이 더 커지게 됩니다.

참고:윈도우 폴링vs웹 소켓

최종적으로는 양방향성과 실시간성, 확정성을 고려해 Spring Websoket과 STOMP를 활용했습니다.

  • 사실 뚜렷하게 목적이 없다면 Http 프로토콜을 사용하는 Polling 방식도 좋은 방식이라는 생각이 들었습니다.  WebSocket의 양방향 연결 자원에 대한 지속적인 업데이트가 필요하지 않다면 즉 지금 저의 채팅 서비스가 1:N이나 실시간이 그렇게 중요하지 않고 단순 1:1 기능만 있다면 굳이 Web Socket은 필요하지 않겠다는 생각을 했습니다.

내가 작성한 메세지를 상대방에게 실시간으로  전송해야 할 때 여러 사용자가 접속할 경우를 대비하기 위해서는 STOMP 또한 사용 해야 합니다. STOMP 사용할 경우

  • 클라이언트와 서버 간의 통신에서 일관성을 유지(메시지 형식을 지정할 수 있습니다.)
  • STOMP의 PUB/SUB을 사용해  메세지 처리에 용이합니다.
  • 메시지 브로커 기능을 수행할 수 있습니다.

Spring에 있는**@EnableWebScoketMessageBroker** 어노테이션을 활용해 구현했습니다.

참고: Stomp

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

  @Override
  public void configureMessageBroker(MessageBrokerRegistry config) {
    config.enableSimpleBroker("/topic");
    config.setApplicationDestinationPrefixes("/app");
  }

  @Override
  public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/gs-guide-websocket");
  }

}

문제 발생

WebSocket STOMP를 활용해 구독/발행(토픽 기준)을 통한 웹소켓 연결  채팅을 simple WebSocket Client을 통해 잘 전달 되는 것 까지 확인했지만.

지금 현재 환경에서의 메시지 브로커, 메시지 큐는 스프링 부트 서버의 내부 메모리에 존재하기 때문에 만약 다수의 서버가 있을 경우 운영하기 쉽지 않다는 문제점이 발생합니다.

그렇다면 이 시점에 일종의 외부 메시지 브로커 기능을 하는 서버를 띄울 필요가 있다는 생각이 들었습니다.

스프링 문서의 그림(외부 브로커 연동전)

(외부 브로커 연동 후)

위의 그림과 같이 메시지 발행 시 인 메모리 기반일 경우 유실 될 가능성이 있지만 외부 브로커를 통해 PUB/SUB 하도록 합니다.  또한 메시지 브로커를 통해 구독하기 때문에  수평적 확장을 통해 인프라를 구축할 때 더 용이 합니다.

개선하기 (feat: Redis)

메시지 브로커 기능을 RabbitMQ, Redis, kafka 등이 많이 쓰이지만 Rdis를 도입 하기로 했습니다. 캐시의 역할도 가능하기 때문에 이를 생각했습니다. (이후에 진행하며 여기 서도 문제를 겪게 되는데요. redis는 인 메모리 기반이기 때문에 서버가 다운 될시 Redis내의 모든 데이터가 날라 가는 경우가 생기기도 합니다.)

또한 Redis-Cluster를 활용해 분산 저장이 가능합니다. 서비스가 수평적 확장 가능성이 크다면 확실히 채팅 구독이나, Global Session을 다루기 쉬워집니다.

즉 단순 Spring Stomp를 이용한 인 메모리 저장 방식에서 벗어나 외부 Message Broker를 활용하여 아래와 같은 이점을 챙겼습니다.

  • 수평적 확장 시 이점
  • 서버가 다수일 경우 한 곳에서 PUB/SUB 하기 때문에 관리하기 용이하다.
  • 인 메모리 방식이 아닐 경우 PUB/SUB 할 때 내용을 저장 가능하다.

다음 WebSocketController를 구현 한 후 소켓을 통해 메시지가 들어오면 받아서 해당되는 채널로 전달하도록 합니다. 채팅 방 정보나 채팅 방 로그는 Mysql에 저장하도록 합니다.

참고

메시지/이벤트 브로커 두 가지로 구분할 수 있습니다.

Kafka(이벤트 브로커 ,메시지 브로커), RabbitMQ,Redis(메세지 브로커)• 메시지 브로커는 이벤트 브로커가 될 수 없으나, 이벤트 브로커는 메시지 브로커가 될 수 있습니다(메시지는 삭제되고, 이벤트는 삭제되지 않기 때문)

정리

프로젝트에 채팅 서비스를 도입하면서 기술적 스택에 대한 고민과 실시간 성을 고려하여 다른 기술로 변경하는 과정을 정리했습니다.

정리하는 시간을 가져보며 단순하게 Webscoket이 폴링,SSE 방식보다 더 나은 기술이고 채팅에서는 Websocket을 무조건 사용하는 것이 아닌  구현해보지는 못했지만 자신의 서비스의 아키텍처 규모나 해당 도메인의 중요도, 실질적인 구현 계획 시간을 고려하여 채팅과 관련된 기술을 채택하는 것이 좋다는 생각이 들었습니다.

추가적으로 다른 메시지 브로커 역할이 가능한 Kafka와 Redis의 차이에 대해 밑의 블로그를 통해 학습했습니다. Spring Session에서 Redis를 통해 Session을 저장 하기도 하는데 Redis의 Group이 없고 모든 Subscriber에서 발생되어야 하는 경우 강점이 있는 특징을 사용한 것 같습니다.

또한   채팅 서비스에서 회원가입당 Subscriber수 와 관계없이 메시지 축하 알림이 하나만 가야 하는 경우  즉 발행된 이벤트에 대해, 특정 작업이 한 번만 발생하여야 할 때 Kafka 도입도 고려해볼 수 있을 거 같습니다.

PUB/SUB, 잘 알고 쓰자!
Kafka와 Redis 사이에서 고민하는 당신을 위한 글