실시간 통신에서 최적화를 하는 방법

CHAN·2024년 2월 14일
1

CS

목록 보기
10/14
post-thumbnail

실시간 통신

사전적 단어를 먼저 보자.

다양한 데이터 소스에서 일련의 데이터를 취합 및 수집하고 해당 데이터를 실시간으로 처리하여 의미와 인사이트를 추출하는 것

많은 구현 방식이 있다. react에서 대표적인 방식은 websocket을 활용하는 것이다. 실제 프로젝트에서, 실시간 채팅과, 화상채팅을 구현해본 경험이 있다. 각각의 구현방식에 따른 기술은 다음과 같다.

  • 실시간 채팅 - stomp
  • 화상채팅 - webRTC Kurento

협업하기 위한 stomp

프로젝트를 진행할 때, 백엔드측의 기술은 springboot를 사용했었다. Spring Boot는 Spring WebSocket 모듈을 지원하므로 STOMP를 사용하여 간단하고 효율적으로 실시간 통신을 구현하기 위해 socket.io 대신 stomp를 선택한 것이다.


실시간 채팅을 위한 요구사항과 로직

stomp를 활용한 실시간 채팅을 구현하기전, 먼저 사용자 요구사항문서를 작성해여 구현했다.

해당기능을 적용하고, 동작과정은 다음과 같다.

① chatting.tsx 파일에서 먼저 websocket을 연결, 이때 토큰방식으로 진행한 로그인에서
accessToken을 같이 넘겨줘, 서버로 부터 사용자를 식별하도록 한다.
② 생성된 websocket에 Stomp 클라이언트를 사용하여 해당 WebSocket에 연결한다.
③ 이후, 버튼(채팅이 켜지는 버튼)을 클릭하게 되면 chattingList.tsx로 이동한다.
④ chattingList.tsx에서 클라이언트측에 처음으로 보이는 화면은, 채팅방 리스트이다.
⑤ 채팅방을 클릭하게되면, subscribe가 동작하게 되는데, 이때, 이전 채팅을 불러오며(axios 사용), 메시지를 보낼때는 send(), 받을때는 subscribe()를 통해 message가 도착하게 된다면, JSON을 파싱하여 화면에 띄우는 역할을 한다.


클라이언트측, 실시간 채팅에서의 최적화

WebSocket은 HTTP 프로토콜과는 다르게 TCP 연결을 유지하는 양방향 통신 프로토콜이다. 그래서 이미 최적화를 기반으로 사용되는 기술이지만, 잘못된 구현방식에서는 오히려 이점을 볼 수 없는 상황이 발생할 수도 있다.

  • 잘못된 구현을 통한, 불필요한 UI 업데이트

간단하게 위와 같은 상황은, 메시지가 도착할 때, 즉, subscribe가 동작할 때, 훅으로 인해 다른 컴포넌트가 동작하는 것을 막아야 하는 것이다.

실제 구현에 사용했던 방식인데, 메시지가 동작하면, 다른 컴포넌트들이 동작하지 못하도록 해야한다. 아래와 같이 useEffect의 인자를 적용하여, 해당 인자의 값이 변경되었을때만, 컴포넌트의 호출이 일어나도록 구현을 하였다.

isVisible, openChatting의 값이 변화할 때, chattingList.tsx에서 이전 메시지를 불러오는 역헐을 하는 handleChattingList()함수를 호출하도록 했다. 다만, subscribe가 있는 함수 handleChatRoomClick에서는 useEffect동작에 관여한, setOpenChatting의 값 변경이 있다. 하지만 이는 채팅방에 제일 처음 입장했을때, 이전 채팅을 불러오는 역할을 관리하는 값으로, useState 훅은 이전 상태와 새로운 상태가 같은 경우에는 상태를 업데이트하지 않는다. 라는 특징을 통해 리렌더링되지 않고 이전 상태를 그대로 유지한다.


또한, 불필요한 UI 업데이트하는 방식 말고도, 클라이언트측에서 최적화를 이루는 방식이 있다.json을 사용하지않고, Protocol Buffers같은 다른 데이터형식을 사용하는 방식이다. JSON은 텍스트 기반이기 때문에 데이터 크기가 상대적으로 크다. 반면에 Protocol Buffers는 바이너리 형식은 데이터를 효율적으로 인코딩하여 저장하므로 데이터 크기가 JSON에 비해 작다. 이는 대규모 데이터 전송 시에는 이러한 크기 감소가 큰 영향을 준다.

리엑트에서 npm install protobufjs라이브러리를 설치한 이후, 정의된 데이터 구조에 따라 JavaScript 객체를 직렬화하여 바이너리 데이터로 변환하거나, 바이너리 데이터를 역직렬화하여 JavaScript 객체로 변환하여 크기를 줄이는 것이다.

serializeMessage 함수는 id와 content를 받아서 Protocol Buffers 형식에 맞게 직렬화된 데이터를 반환한다. 그리고, deserializeMessage 함수는 Protocol Buffers 형식으로 직렬화된 데이터를 받아서 JavaScript 객체로 역직렬화한다. 이러한 방식은 데이터를 더 작은 크기로 표현할 수 있어서 네트워크 대역폭을 절약할 수 있어 특히, 대규모 데이터 전송 시에는 이러한 크기 감소를 이룰 수 있다.

해당 방신은, 서버측에서도 Protocol Buffers를 사용할 때, 가능한 방식이다. 큰 데이터를 보내는 실시간 채팅의 경우에는 이러한 방식을 추천한다. 직렬화, 역직렬화 방식에는 오버헤드는 반드시 존재하지만, 이는 json으로 큰 데이터를 보내는 오버헤드에 비해서는 극소에 불과하다. 나는 협업에서, stomp의 json방식의 통신을 사용했기때문에 실제 구현에서는 적용하지 못했지만, 언젠가 다른 프로젝트를 하게 된다면, 이 방식을 꼭 적용할 것이다.


WebRTC를 통한 실시간 화상채팅

화상채팅이라하면, webRTC가 익숙할 것이다. 하지만 webRTC에서 최적화를 하는 방식은 구현 방식에 차이가 분명하게 있다. 크게 3가지 방식이 있다.

시그널링 방식
피어 간 통신을 설정하기 위한 프로세스
SFU 방식
클라이언트가 전송하는 미디어 스트림을 수신하고 다수의 수신자에게 적절한 형태로 전달
MCU 방식
다수의 피어 간 통신에서 모든 미디어 스트림을 집중화하고 처리

보통 프로젝트에서는 가장 간단한 시그널링 방식을 사용할 것이다. 하지만 시그널링 방식에는 큰 문제가 있다. 평균적으로 6명 이상의 연결부터는 피어간의 모든 연결선때문에 오버헤드가 심하게 보인다. 실제 내가 했던 프로젝트에서는 사용자들이 순차적으로 들어오는 서비스였기 때문에, 몇명 이상부터는 시그널링 방식을 통해서는 대규모 화상채팅을 구현할 수가 없었다. 그럼 남은 것은 SFU방식 그리고 MCU방식인데, 대규모라는 주제 아래, 더 많은 클라이언트를 지원하는 데 유용한 SFU 방식을 선택하게 되었다.
결국, WebRTC에서의 최적화를 이루는 방식은 구현방식중 하나의 SFU를 이용하는 것이다.


SFU방식을 통한 최적화

SFU방식은 각 수신자에게 필요한 미디어 스트림만 전달하기 때문에 대역폭을 효율적으로 사용할 수 있다. 이는 네트워크의 대역폭 사용량을 최소화하고 네트워크의 병목 현상을 줄여, 특히, 다수의 사용자가 참여하는 화상 회의나 그룹 채팅과 같은 다중 사용자 실시간 통신에서 효과적이다. 또한 이는 서버 측에서 수신된 미디어 스트림을 복제하거나 재생성하지 않고 각 클라이언트에게 전달하기 때문에 서버의 부하를 분산하는 데 용이하여 서버를 확장하거나 추가 서버를 도입하는 등의 방법으로 사용자 규모에 따라 시스템을 쉽게 확장가능하다. 그리고 미디어서버를 구현하기 위해서는 라이브러리가 필요하다. 아마 가장 유명한 것이 OpenVidu라고 알고있다. 하지만, OpenVidu의 경우 한 가지 문제점이 있다. 협업하기에는 추천하지 않는다는 것이다. OpenVidu의 경우 근본 로직의 Kurento 라이브러리를 더 쉽게 하기 위해, 서버나 클라이언트측 중 하나가 애플리케이션 역할을 하고, 그대로 가져다 쓰면 끝이기 때문에 커스터마이징에는 상대적으로 불리하기 때문이다. 이후 확장성을 고려해, Kurento를 사용하여 화상채팅에서의 최적화를 이루기로 했다.

위와 같은 아키텍쳐 구성을 통해, 미디어서버를 두고, sfu방식을 구현했다. 그러나, 사실 문제가 하나 있었다. 쿠렌토를 구현하기에는 너무 문서가 부족하다는 것이다. 보통 시그널링방식이나, 응용버전 라이브러리인 Openvidu를 사용하기 때문에 react 환경에서, kurento 구현을 성공한 사례를 인터넷에서 찾지 못했다. Kurento 공식문서에서도, 쿠렌토와 관련된 이전 버전의 참고 라이브러리를 호출하고 있었고, 특히, react에 대한 지원은 따로 없었기 때문에, 직접 jsx 환경에서 작동하도록, 구현해야 했다. 구현한 로직은 다음과 같다.

JSX 환경에서의 Kurento


Kurento 전체 로직

마지막으로, 위의 링크에서, 구현한 sfu방식을 아래의 시퀀스로 나타내며 마무리 하겠다.

위와 같은 로직이 결국 webRTC에서 미디어 서버를 통해 피어간의 연결에서 최적화를 이루는 방법이다.


마무리

실시간 처리에서, 연결을 유지한 상태로, 한 번더 최적화를 고려할 수 있는 좋은 기회가 되어서 좋았습니다.

profile
크게 보는 습관

0개의 댓글