기술 블로그/ToValley

[Spring Boot, WebSocket, Kafka, MongoDB] 채팅 기능 구현#1. 기술 선정

개발자가 될 사람 2024. 3. 20. 01:16

성장🪜

CI/CD 환경 구축, 모니터링 시스템 구축, 기본적인 CRUD 작업, AOP 구현 등 여러 기술을 활용한 프로젝트를 경험해봤다.

이러한 경험을 바탕으로 새로운 기능을 구현해봄으로써, 더 넓은 경험을 쌓고 싶다는 목표를 세웠다.

그 과정에서 관심을 끈 것이 바로 채팅 기능이었다.

채팅 기능 구현을 위해 필요한 WebSocket, STOMP, Kafka, MongoDB는 모두 이전에 경험해보지 못한 새로운 기술들이었다.

Kafka를 대학교 수업 시간에 배운 적이 있지만, 실제 프로젝트에 적용해본 경험은 없었다.

이러한 새로운 기술들을 프로젝트에 도입하고자 하는 것은 분명 큰 도전이었지만, 동시에 새로운 지식을 습득하고 성장할 수 있는 기회라고 생각했다.

 

이에 따라, 이 새로운 기술들을 하나씩 탐구하고 공부하기 시작했다.

 

* 이 블로그 시리즈에서는 제가 채팅 기능을 구현하기 위해 공부하고, 실험하며 얻은 지식과 경험을 공유하고자 합니다. 각 기술의 기본 개념부터 시작하여, 실제 프로젝트에 적용하는 과정까지 단계별로 소개할 예정입니다.

 

🧑‍💻 채팅 기능에 필요한 각 기술들


채팅 서버 메시지 처리 방식 (Polling vs. Long Polling vs. WebSocket)


채팅 서비스 구현에 있어 메시지 처리 방식은 사용자 경험에 직접적인 영향을 미친다.

Polling

Polling은 가장 기본적인 메시지 처리 방식이다.

클라이언트(예: 웹 브라우저)가 정해진 시간 간격으로 서버에 요청을 보내어 새로운 메시지나 데이터가 있는지 확인한다. 서버는 요청을 받을 때마다 해당 시점의 데이터를 클라이언트에게 응답한다.

Polling 방식은 WebSocket과 다르게 Connection을 맺고 있지 않아도 돼서, 서버 리소스를 상당히 절약할 수 있으며 API 서버 개발만으로 간단하게 기능 구현이 가능하다.

그러나, 일정 주기로 메시지를 클라이언트에서 가져가는 방식이기 때문에 다른 사용자가 작성한 메시지가 전달되는 데까지 일정 시간 지연이 발생할 수 있고, 클라이언트 별로 Polling 되는 시점에 따라 서로 보고 있는 메시지가 다르기 때문에 실시간으로 보이는 대화 내용이 수초 가량 불일치하는 문제가 발생할 수 있다는 단점이 존재한다.

 

Long Polling

Long Polling은 Polling의 단점을 일부 해결하기 위한 방식이다.

클라이언트가 서버에 요청을 보내면, 서버는 새로운 메시지가 도착할 때까지 응답을 지연시킨다. 새로운 메시지가 도착하면 그 즉시 응답을 보내고, 클라이언트는 즉시 다시 요청을 보낸다. 이 방식은 Polling에 비해 실시간성이 개선되지만, 여전히 서버의 리소스를 많이 사용하고, 연결 지연이 발생할 수 있다는 단점이 있다.

 

WebSocket

WebSocket은 대부분의 채팅 기능에서 사용되는 메시지 전달 방식으로, HTML5에서 도입된 기술이며 클라이언트와 서버 간에 전이중(full-duplex) 통신 채널을 제공한다. 연결이 한 번 수립되면, 클라이언트와 서버는 서로에게 실시간으로 데이터를 보낼 수 있다. 이 방식은 높은 실시간성과 효율성을 제공하며, 채팅 애플리케이션에서 널리 사용된다. WebSocket은 Polling이나 Long Polling에 비해 서버 리소스 사용이 적고, 통신 지연이 거의 없다는 장점이 있다.

WebSocket 방식은 클라이언트와 서버 간의 Connection이 맺어져 있는 상태이기 때문에 메시지가 발생하는 즉시 전달할 수 있다는 장점이 있으나, 서버와 연결이 지속되어야 하므로 서버당 처리할 수 있는 클라이언트의 수에 제한이 있다.

 

채팅 메시지 처리 방식 선정 (WebSocket📌)

  • Polling: 주기적으로 서버에 요청을 보내는 방식
    • 구현이 간단하지만, 실시간성이 떨어지고 리소스 낭비가 심할 수 있다.
  • Long Polling: 클라이언트의 요청에 대해 서버가 새로운 데이터가 있을 때까지 응답을 지연시키는 방식
    • Polling에 비해 실시간성이 개선되지만, 여전히 리소스 사용이 많고 지연이 발생할 수 있다.
  • WebSocket: 클라이언트와 서버 간의 전이중 통신 채널을 수립하는 방식
    • 높은 실시간성과 효율성을 제공한다.
    • 사용자가 작성한 메시지의 전달을 서버에서 진행하므로 즉각 전달이 가능하다.
    • 모든 시점에서 사용자가 보고 있는 채팅의 내용이 일치한다.

 


STOMP


채팅 메시지 처리 방식으로 WebSocket을 선택했음에도 불구하고, STOMP를 사용하려는 이유가 무엇일까?

 

WebSocket 방식을 선택하였음에도 불구하고 STOMP를 사용하는 이유는 WebSocket 자체가 낮은 수준의 프로토콜이기 때문에, 실제 애플리케이션에서 효율적으로 통신을 관리하기 위해 더 고수준의 프로토콜을 필요로 하기 때문이다.

STOMP( Simple Text Oriented Messaging Protocol)는 WebSocket 위에서 작동하는 메시징 프로토콜로, 클라이언트와 서버 간의 메시지 기반 통신을 간소화하고 표준화한다.

  1. 메시지 브로커와의 통합: STOMP는 다양한 메시지 브로커와의 호환성을 제공한다. 이것은 특정 메시지 브로커 기술에 종속되지 않고, 다양한 시스템과 통합할 수 있음을 의미한다. 예를 들어, ActiveMQ, RabbitMQ와 같은 인기 있는 메시지 브로커들은 STOMP 프로토콜을 지원하며, 이를 통해 더욱 유연한 개발 환경을 가질 수 있다.

  2. 구독/발행 모델 지원: STOMP는 구독(subscribe) 및 발행(publish) 모델을 지원한다. 클라이언트는 특정 주제나 대상에 대한 구독을 선언할 수 있고, 서버는 해당 구독자에게 메시지를 보낼 수 있다. 이는 효율적인 데이터 분배 및 관리를 가능하게 한다.

  3. 헤더 기반 메시징: STOMP 메시지는 헤더바디로 구성되어 있으며, 헤더를 통해 메시지의 라우팅, 우선순위, 유효성 등을 관리할 수 있다. 이러한 메시지 구조는 사용자가 메시지의 메타데이터를 쉽게 정의하고 사용할 수 있게 하여, 보다 복잡한 애플리케이션 요구사항을 충족시킬 수 있다.

  4. 간단한 텍스트 기반 프로토콜: STOMP는 간단한 텍스트 기반 프로토콜이기 때문에 개발 및 디버깅이 비교적 용이하다. 복잡한 바이너리 프로토콜에 비해, 텍스트 기반 프로토콜은 우리가 읽을 수 있으며, 이는 네트워크 트래픽 분석 및 문제 해결 과정을 단순화하다.

  5. 크로스-플랫폼 호환성: STOMP 클라이언트 라이브러리는 다양한 프로그래밍 언어 및 플랫폼에서 사용할 수 있다. 이는 다양한 기술 스택을 가진 시스템 간의 통합을 용이하게 하며, 프로젝트의 기술 선택 폭을 넓힐 수 있다.

 


메시지 브로커(Redis vs.  RabbitMQ vs. Apache Kafka)


메시지 브로커(Message Broker)

채팅 기능을 구현하기 위해서는 실시간으로 메시지를 전송하고 받을 수 있는 메시징 시스템이 필요하다.

메시지 브로커를 사용하게 되면  대규모, 분산된 환경에서 메시지의 신뢰성있는 전송, 확장성, 그리고 유연한 아키텍처 구성이 가능하다.

  • 비동기 통신: 메시지 브로커는 비동기 메시징을 지원한다.
    • 채팅 애플리케이션에서 사용자가 메시지를 전송할 때 수신자가 온라인 상태가 아니더라도 메시지를 보낼 수 있다.
    • 메시지 브로커는 메시지를 메시지 큐에 저장하고, 수신자가 다시 온라인 상태가 되면 메시지를 전달한다.
  • 확장성: 채팅 애플리케이션은 사용자 수가 증가함에 따라 유연하게 확장되어야 한다.
    • 서버 부하가 증가할 때 메시지 브로커는 메시지를 여러 서버에 균등하게 분산시켜 처리할 수 있으며, 이는 시스템의 성능을 유지하는 데 중요하다.
  • 신뢰성 있는 메시지 전달
    • 메시지가 성공적으로 전달되지 않는 경우, 메시지 브로커는 자동으로 메시지를 재시도하거나, 문제를 로깅하고 관리자에게 알림을 보낸다.
  • 보안
    • 데이터 암호화, 접근 제어, 안전한 인증 방식을 통해 메시지의 안전을 보장한다.

이를 위해 여러 메시지 브로커가 사용될 수 있으며, 각각의 메시지 브로커는 고유의 특징과 장점을 가지고 있다.

 

STOMP를 사용하면 기본적으로 In-Memory Messge Broker를 사용하게 되는데, 해당 메시지 브로커는 데이터의 내구성과 신뢰성, 메시지 관리 기능 측면에서 문제가 존재한다. 이러한 이유로 외부의 메시지 브로커를 사용한다.

 

Redis Pub/Sub

Redis는 오픈 소스, 인메모리 데이터 구조 저장소로, 키-값 저장소로서의 기능 외에도 메시지 브로커로도 사용될 수 있다.

Redis의 Pub/Sub 모델을 통해 메시징 기능을 구현할 수 있으며, 빠른 속도낮은 지연시간이 특징이다.

채팅 애플리케이션에서는 주로 실시간 메시지 전달에 사용된다.

 

그러나 Redis를 사용한 방식은 다음과 같은 치명적인 결함을 가진다.

대규모 트래픽이 부과되는 시스템에서 Redis를 사용하는 경우, Redis Pub/Sub으로 메시지 발행 시 모든 Redis 클러스터의 노드에 메시지를 발행한다. 그래서 클러스터의 노드를 확장할수록 Pub/Sub 속도 저하가 발생하게 된다.

그리고 특정 채털에 메시지 발행 시, 채널을 구독하는 모든 subscribe를 순회하면서 메시지를 발행한다. 즉, 채널을 구독하는 subscriber의 수가 많아질수록 메시지 발행 속도도 지연된다.

 

RabbitMQ

RabbitMQ는 AMQP(Advanced Message Queuing Protocol)를 구현한 오픈 소스 메시지 브로커이다.

복잡한 라우팅, 메시지 큐잉, 메시지 확인(acknowledgement) 같은 기능을 제공하여 메시지의 신뢰성 있는 전달을 보장한다.

또한, 고급 메시징 패턴을 지원하며, 다양한 프로그래밍 언어와 통합하기 쉽다.

Redis Pub/SubRabbitMQ의 경우 전통적인 의미에서 메시지 브로커에 해당한다.
메시지 브로커는 메시지의 전송(Producing)과 수신(Consuming) 사이에서 메시지를 중개하는 역할을 한다. 이는 메시지를 큐에 저장하고, 메시지가 소비될 때까지 관리하는 것을 포함한다.

Redis Pub/Sub은 기본적인 메시지 큐잉 기능을 제공하지만, 주로 메모리 기반의 키-값 저장소로 사용된다. 이는 메시지가 영구적으로 저장되지 않고, 실시간 메시지 전달에 중점을 둔다.

RabbitMQ는 복잡한 라우팅, 메시지 확인, 지속성을 제공하는 고급 메시지 큐잉 시스템이다. 메시지의 신뢰성 있는 전달을 보장하며, 다양한 메시징 패턴을 지원한다.

 

Apache Kafka

Apache Kafka는 대규모 메시지 처리를 위해 설계된 분산 스트리밍 플랫폼입니다.

Kafka는 고처리량, 내구성, 확장성을 제공하며, 실시간 데이터 피드를 처리하기 위해 널리 사용된다.

Apache Kafka스트림 처리 시스템이다.
스트림 처리 시스템은 데이터를 연속적인 스트림으로 처리하는 데 중점을 둔다. Kafka는 메시지들을 토픽이라는 카테고리에 저장하며, 해당 토픽들은 시간 순서에 따라 저장되는 레코드의 불변 시퀀스로 구성된다.

Kafka는 높은 처리량, 내구성, 확장성을 특징으로 하며, 대규모 데이터 스트림을 효율적으로 처리할 수 있다. 또한, 데이터를 디스크에 저장하고, 복제를 통해 높은 내구성을 제공한다.
메시지 브로커 vs. 스트림 처리
메시지 큐잉 시스템(메시지 브로커)은 일반적으로 메시지를 한 번에 하나씩 처리하며, 메시지가 소비되면 큐에서 제거된다. 이는 메시지의 신뢰성 있는 전달과 순서 보장에 중점을 둔다.
반면, 스트림 처리 시스템은 데이터 스트림을 연속적으로 처리하는 데 초점을 맞추며, 대량의 데이터를 빠르게 처리하고 분석할 수 있다. 이는 실시간 데이터 처리와 대규모 데이터 집합에 적합하다.

 

메시지 브로커 선정(Kafka📌)

우리 시스템에서는 채팅 기능 구현 후 대용량 트래픽 부하 테스트를 진행할 예정이므로, 높은 처리량과 대규모 데이터 처리에 적합한 Apache Kafka를 선정하였다.

 


채팅 메시지 영구 저장소(MySQL vs. MongoDB)


 

채팅 메시지를 저장하기 위해 다음 요소를 고려해야 한다.

  • 데이터 구조: 메시지의 형태가 단순 텍스트인지, 이미지나 비디오 같은 멀티미디어를 포함하는지 등 데이터 구조를 고려해야 한다.
    • 우리 시스템에서 제공할 채팅 메시지는 유형은 텍스트와 이미지이며, 해당 데이터 구조는 변경이 빈번하게 발생할 수 있다.
  • 데이터베이스 확장성: 사용자 수와 메시지 양이 증가함에 따라 데이터베이스를 쉽게 확장할 수 있어야 한다.
  • 실시간성
    • 메시지는 실시간으로 전송되고, 사용자는 즉시 메시지를 받기를 기대한다. 따라서 데이터베이스는 빠른 쓰기와 읽기 성능을 제공해야 한다.
    • 수정 또는 삭제 보다는 저장 및 조회 작업이 주로 이루어진다.

 

채팅 메시지를 저장하기 위해 MySQL과 같은 관계형 데이터베이스(RDBMS) 대신 MongoDB와 같은 NoSQL 데이터베이스를 선택하는 이유는 여러 가지가 있다.

 

스키마의 유연성

RDBMS고정된 스키마를 가지고 있어서, 데이터 구조가 변경될 때마다 데이터베이스 스키마를 수정해야 한다.

채팅 애플리케이션에서는 사용자가 메시지를 보내는 형식이 다양할 수 있고, 새로운 기능이 추가될 때마다 데이터 모델을 변경해야 하는 경우가 많다. 이러한 환경에서는 RDBMS의 고정된 스키마가 제약이 될 수 있다.

NoSQL은 스키마리스(Schema-less)이므로, 데이터 구조의 변화에 유연하게 대응할 수 있다.

채팅 메시지와 같이 다양한 형태의 데이터를 저장할 때 유리하다. 예를 들어, MongoDB에서는 한 컬렉션 내의 문서마다 다른 구조를 가질 수 있어, 메시지 형식의 변화에 쉽게 적응할 수 있다.

확장성

RDBMS수직적 확장(스케일업)에 의존하는 경향이 있다. 즉, 데이터베이스 서버의 성능을 향상시켜 처리 능력을 증가시키는 방식이다.

대규모 트래픽이나 데이터 양 증가에 대응하기 위해서는 비용이 많이 드는 하드웨어 업그레이드가 필요할 수 있다.

NoSQL은 수평적 확장(스케일아웃)이 가능하다. 즉, 더 많은 서버를 데이터베이스 클러스터에 추가하여 처리 능력을 증가시킬 수 있다.

이는 대용량 데이터와 높은 트래픽을 처리하기에 적합한 방식이다.

 

쿼리 성능

RDBMS는 복잡한 JOIN 연산 등을 사용하여 관계형 데이터를 효율적으로 처리할 수 있으나, 대용량 데이터에서는 성능 저하가 발생할 수 있다.

NoSQL은 문서 기반의 쿼리 시스템을 사용하여, 대용량 데이터에서도 빠른 조회 성능을 제공한다. 특히, 채팅 메시지와 같이 특정 사용자나 시간대별로 조회하는 경우에 효율적이다.

채팅 메시지 영구 저장소 선정(MongoDB📌)

위와 같은 이유들로 채팅방에서 생성되는 채팅 메시지들은 MongoDB에 저장하기로 하였다.

그러나 채팅방의 경우 사용자, 설정 등 관게형 데이터를 포함하게 된다. 또한, 채팅방 생성, 수정, 삭제와 같은 작업에 트랜잭션 처리가 필요하므로 채팅방의 경우 채팅 메시지와 달리 MySQL에 저장한다.

이는 각 데이터의 특성과 요구 사항에 맞춰 최적의 데이터베이스 시스템을 선택한 결과이다.

 


🐏 기본 동작 방식


  1. 클라이언트 구독
    • 클라이언트는 STOMP 프로토콜을 사용하여 WebSocket 연결을 통해 특정 채팅방에 대한 경로(예: "/sub/chat/room/1")를 구독한다.
  2. 메시지 발행
    • 어떤 클라이언트나 시스템이 채팅에 대한 메시지를 카프카 토픽에 발행한다. 이 토픽은 특정 채팅방의 메시지를 처리하기 위한 것이다.
  3. 카프카 컨슈머
    • 카프카 컨슈머는 해당 토픽으로부터 메시지를 받아온다. 컨슈머는 STOMP를 통해 메시지를 관리한다.
  4. 메시지 전송
    • 컨슈머는 받아온 메시지를 STOMP를 사용하여 해당 채팅방 경로로 메시지를 전송한다. STOMP는 이 경로를 구독하는 모든 클라이언트에게 메시지를 전달한다.
  5. 클라이언트 수신
    • 해당 채팅방 경로를 구독한 모든 클라이언트는 실시간으로 메시지를 받게 된다.