tioon's Devlog

스프링 WebSocket 및 Redis로 채팅 서버 구현기 본문

자바/스프링

스프링 WebSocket 및 Redis로 채팅 서버 구현기

tioon 2024. 4. 22. 02:21

 

 

이번에 미니 사이드 프로젝트로, 실시간 채팅기능을 구현하게 되었는데 이 과정을 한번 정리를 해보려합니다.

 

우선 저희의 요구사항은 다음과 같았습니다.

  1. 사용자간의 실시간 채팅이 구현되어야함.
  2. 방이름을 기반으로 채팅방이 구분되어야함.
  3. 전에 했던 채팅 기능이 저장이 되어야함.
  4. scale-out시에 채팅기능에 문제가 생기지 않아야함.

크게 이렇게 4가지가 있었는데요. 이 요구사항을 지키기위해서 갖은 삽질과 버그가 있었습니다.
이 미니 사이드 프로젝트는 Spring, 타임리프, Redis, mongoDB로 구현되어있으며, 중간에 발생한 문제점으로 인해 Kafka로 마이그레이션을 시도했습니다만.... 시간이 부족해 실패했습니다 이과정에 대해선 아래에서 자세히 설명드리겠습니다.

 

일단 먼저 스프링 WebSokcet과 MessageQueue의 전체적인 설명 먼저 해보겠습니다.

 

 

 

 

 스프링 WebSocket 실시간 통신

 

스프링 WebSocket으로 실시간 채팅을 구현하기 전에, 아래의 사진으로 스프링 WebSocket의 기본적인 구조에 대해 알아보고 가도록하겠습니다.

스프링 웹소켓의 구성은 위와 같습니다.

  • Message Handler
  • Simple Broker
  • Broker Channel

이렇게 3개를 통해 웹소켓의 메세지들을 관리하고, 전달을 할 수 있습니다.

기본적으로 스프링 WebSocket은 ws-stomp 프로토콜 위에서 동작하며, 사용자가 /app으로 요청을 보냈을시엔, Message Handler를 통해 서버에서 추가 가공을 한후, BrokerChannel을 거쳐 다시 SimpleBroker에게 전달되는 형식입니다.
또한, /topic으로 요청을 보냈을 시엔, 서버에서 추가 가공없이 해당 메세지를 구독자들에게 전달하는 방식입니다.

 

따라서 채팅기능을 스프링 WebSocket으로 구현한다 라고 가정을 하면, 사용자들은 스프링 웹소켓에 /topic/{구독정보}으로 구독을 합니다.  구독을 하게 되면 해당 topic으로 메세지가 들어오게될시 SimpleBroker로부터 메세지를 실시간으로 받게 될 수 있습니다.
또한 해당 /app 이나 /topic 으로 메세지를 보내 구독중인 다른 사용자들에게 메세지를 전달을 할 수도 있습니다.

 

이런식으로 채팅서버를 간단하게 구현이 가능합니다.

 

스프링 웹소켓의 내부엔 더 복잡한 기능이 있지만, 웹소켓 설명글이 아니니, 웹소켓의 동작과정은 여기서 생략하고 다음으로 넘어가겠습니다.

 

 

 

 

 

스프링 WebSocket으로만 채팅 구현시 문제점

 

위에 설명한 그림처럼 채팅서버를 스프링 WebSocket으로 만 충분히 구현이 가능하지않나...? 라고 생각할 수 있습니다.
물론, 서버가 1대만 존재할 경우에는 스프링 WebSocket으로만 구현하는것은 문제가 되지않습니다.
하지만, 스프링 Web Socket에 대해 좀 더 깊숙히 생각을 해보면 몇가지의 문제점을 발견할 수 있습니다.

 

  • 서버 재시작 시에 채팅 데이터들은...?
  • 서버가 Scale-Out되면 어떻게 되지...?

 

일단 첫번째로, 스프링 WebSocket은 따로 db에 저장되거나, 백업이 되는 기술이 아닙니다. 즉 memory의 세션방식을 통해 구현되는 기술이기 때문에 서버를 재시작 한다거나, 서버가 갑자기 먹통이 되는경우에 채팅 기록에 문제가 생긴다는 이슈가 있습니다.

두번쨰로, 스프링 WebSocket은 기본적으로 Spring 내부에 존재하는 기술입니다. 즉, Spring 서버가 여러대에 존재한다면, 스프링 WebSocket도 여러개가 존재하는것이죠.... 이렇게 된다면 여러 서버간 채팅이 공유되지않아  다른 서버로 접속한 클라이언트는 채팅 기록을 받지 못하는 문제점이 생깁니다.

 

그렇다면.... 이 문제점들을 해결하기 위해 어떻게 해야할까요??
일단 첫번째로는 DB 혹은 다른 저장소를 이용해 채팅방의 데이터가 계속 유지되도록 처리가 필요하다.

두번쨰로는, 여러 스프링 WebSocket들 끼리 구독할 수 있게 이들을 통합시킬 수 있는 시스템이 필요하다. 입니다.

 

이제 아래에서 이 문제점을 어떻게 해결을 할 수 있는지 알아보도록 하겠습니다.

 

 

 

 

 

스프링 WebSocket의 문제점을 해결할 솔루션

 

위에서 기술했던 스프링 WebSocket의 문제점을 해결하기 위해선 다른 기술을 도입해야합니다.

우선 첫번째 문제였던 데이터 유지처리같은경우는 별도의 DB를 두어서 저장할 수 있게하고,
두번째 문제였던 스프링 WebSocket들의 통합 문제는 Message Queue를 활용해 Pub/Sub 모델로 통합 시켜야합니다.

 

 

우선 DB는 SQL vs NOSQL로 나눌 수 있으나, 채팅의 경우 복잡한 데이터구조가 아니고, 많은 데이터의 이동시에 빠른속도가 중요하기에 Read/Write 시에 적은 오버헤드를 가진 DB가 적합합니다. 따라서, 이를 기반으로 적합한 DB를 고르자면 현재 몽고DB가 제일 적합한 DB인거 같습니다.

 

이제 Message Queue를 선택할 차례입니다. 우선 많은 Message Queue가 있겠지만, 여기서는 제일 유명한 Kafka와 Redis만을 비교해 선택하도록 하겠습니다. 다음 표는 Kafka와 Redis를 비교한 표입니다.

  Kafka Redis
브로커 종류 Event 기반 Broker Message 브로커
속도 비교적 느림 비교적 빠름
구분 방식 topic으로 구분 Channel로 구분
데이터 저장 기본적으로 데이터가 저장 이 됨 데이터 저장 x
scale-out 가능 가능
순서 보장 O X

 

위처럼 Kafka와 Redis는 서로 다른 특징을 가졌습니다.

각각의 특징에 따라 장단점이 명확한데, 여기서 중요한것이 데이터 저장 부분과 순서보장 부분입니다.

Kafka는 Redis와 비교하여 무겁고 속도가 느리지만, 데이터가 저장이 되고, 순서를 보장한다는 측면에서 메세지 유실 및 순서 보장에 대해 확실히 제어를 할 수 있습니다.

다만, Redis같은 경우는 Kafka와 비교하여 가볍고 속도도 빠르지만, 기본적으로 데이터가 저장이 안되고, 순서를 보장하지 않는다는 측면에서 안정성이 떨어집니다.

 

따라서 개발하려는 서비스의 특징에 따라 MessageBroker를 선택하는 기준이 달라져야 합니다.

카톡, 라인처럼 사용자간의 메세지가 순서가 보장이 되어있어야하고, 혹시나 중간에 있을 데이터 유실을 제어하고 싶다면 Kafka를 쓰는게 맞고,
디스코드, 줌 같은 화상채팅같이, 무엇보다 속도가 매우 중요하고 사용자간의 메세지가 순서가 보장이 되지않아도되고, 중간에 데이터 유실이 있어도, 그렇게 치명적이지 않을경우 Redis를 쓰는게 좋습니다.

 

 

 

 

실시간 채팅 아키텍처

 

이제 위의 기술들이 적용된 전체적인 아키텍처를 보겠습니다.

위처럼 여러개의 WebSocket 서버가 있다라고 할때, 각 서버들이 Pub/Sub 기능을 합니다. 이떄 중간에 Message Queue는 Pub으로 메세지가 들어왔다면 해당 메세지를 가공 및 DB에 저장 후, Sub에게 전달하는 기능을 합니다.

즉, 전체적인 과정은 다음과 같습니다.

  1. 사용자가 WebSocket서버로 채팅 전송
  2. WebSocket서버에서 MessageQueue로 Pub 전송
  3. MessageQueue에서 메세지 가공이 필요할 경우 메세지 정보를 가공
  4. DB에 메세지 저장
  5. MessageQueue에서 다른 WebSocket 서버로 Sub 전송
  6. 해당 WebSocket서버에서 사용자에게 채팅 데이터 전송

전체적인 과정은 위와 같으며, 중간에 MessageQueue가 중간 매개체 역할을 하여 WebSocket 서버들을 실시간으로 관리할 수있는 구조입니다.

이때 중간에 MessageQueue가 있기때문에 당연히 WebSocket서버가 scale-out되어 더 많아지는 상황 이라고 해도 문제가 생기지 않습니다. 유연한 서버 구조를 가져갈 수 있습니다.

 

또한, 몽고DB를 연결함으로 실시간 채팅때 전달되는 채팅데이터들을 안정적으로 저장할 수 있고, 데이터 유실시에도 백업이나 롤백등의 기능을 활용하여 안정적으로 데이터 복구를 할 수 있습니다.

 

 

 

 

 

미니 사이드 프로젝트에서 발생한 문제점

 

우선 결론부터 말하자면, 초기에 잘못된 생각으로 설계를 잘못한거 같습니다.

제가 개발한 프로젝트는 실시간 채팅을 통한 감정분석이었습니다.

이때, 저는 실시간 채팅의 경우엔 속도가 매우 중요하다 라는 생각을 하였고, 이 생각 때문에 Kafka보단 Redis가 더 좋은 선택지라는 생각을 하였습니다.

 

그래서, 스프링 WebSocket 서버와 Redis를 이용해 채팅을 구현했고, 테스트시에도 정상적으로 작동을 하였습니다.
하지만, 프로젝트가 거의 끝나갈때쯤에 Redis의 경우 메세지들의 순서가 보장되지 않는다 라는 개념을 알게 되었고, 이를 기반으로 사용자가 많아졌을때, 네트워크 상황에 따라 메세지들의 순서가 서로 꼬일 수 있다 라는것을 알았습니다.
이를 해결하기위해선, Redis 대신 Kafka를 도입했어야 하나, 프로젝트 마감기한이 얼마 남지않아 도입을 하진 못했습니다.ㅜㅜ

 

또한, 감정분석 부분에서도 문제가 생겼습니다. 이는 제가 생각했던 경우는 실시간 채팅이 되면서, 그 채팅데이터를 기반으로 감정분석이 바로 진행되고, 그 사람의 상위 감정 top3를 바로 보여주자! 였습니다.
이 프로젝트에서는 허깅페이스의 go_emotion 이라는 감정분석 API를 활용했고, 채팅데이터가 들어오게되면 해당 채팅 데이터를 기반으로 API 호출을 통해 감정 분석 결과값을 가져와 프론트에 출력하도록 개발을 하였습니다.

 

물론 이 API가 느린 API는 아니여서 문제가 되지않을 거라 생각을 했지만, 테스트시에 API 서버의 상황에 따라 응답속도가 차이나는 현상을 발견했습니다. 따라서 이문제는 API호출을 동기처리하는게 아닌, 비동기 처리로 해결을 하거나,

DB에 따로 저장 후, 감정분석은 시간 텀을 둬 분석을 해야겠다 라는 개선점을 생각하였습니다.

 

 

 

결론

비록 이번 프로젝트를 초기에 설정한 목표점까지 달성하진 못했지만, 많은 점을 배울 수 있었습니다.
이번 프로젝트를 통해 기존 개발할때 느끼지 못했던 문제점들을 다양하게 느꼇던 소중한 기회였던것 같습니다.

특히 많이 배울 수 있었던 건 다음과 같습니다.

 

  • Message Queue 작동원리에 대한 이해
  • 실시간 데이터 처리시 유의점
  • 테스트시에 최대한 실사용 환경과 비슷하게 테스트 환경 구성
  • API 활용시 비동기 처리

 

비록 이번 미니 프로젝트땐 완성도 있는 프로젝트 개발은 실패하였지만,

나중에 진짜로 이런 실시간 데이터 처리 기술이 들어간 프로젝트를 개발하게 된다면 이번에 느꼇던 점들을 설계때 최대한 고려하여 개발을 진행하고자합니다.