RSocket 개념
Application protocol providing Reactive Streams semantics
작성일 2026.01.19
페이지가 생성된 시간 2026.02.03 00:47:07

1

RSocket 개념

실시간 협업이 왜 어려운가

우리 회사 서비스에서는 여러 사용자가 동시에 같은 디자인을 편집한다. A가 텍스트를 수정하면 B의 화면에도 즉시 반영되어야 한다. 이러한 실시간 협업은 일반적인 웹 애플리케이션과는 근본적으로 다른 통신 패턴을 요구한다.

일반적인 웹 통신: Request-Response

대부분의 웹 애플리케이션은 HTTP 요청-응답 패러다임을 따른다.

[클라이언트] ---요청---> [서버] [클라이언트] <--응답--- [서버]

클라이언트가 필요할 때마다 서버에 요청을 보내고, 서버는 그에 대한 응답을 돌려준다. 이 방식은 단순하고 이해하기 쉽지만, 실시간 협업에는 한계가 있다.

  • 서버가 먼저 데이터를 보낼 수 없다: 다른 사용자가 변경을 했을 때, 서버가 능동적으로 알려줄 방법이 없다.
  • 지연 시간(Latency): 클라이언트가 주기적으로 서버에 "변경 사항 있어?"라고 물어봐야(Polling) 한다.
  • 비효율적인 리소스 사용: 변경이 없어도 계속 요청을 보내야 한다.

실시간 협업에 필요한 것

실시간 협업에서는 다음이 필요하다.

  1. 양방향 통신: 클라이언트도 서버도 언제든지 메시지를 보낼 수 있어야 한다.
  2. 지속 연결: 매번 연결을 새로 맺지 않고, 연결을 유지한 채로 통신해야 한다.
  3. 서버 푸시: 서버가 클라이언트의 요청 없이도 데이터를 보낼 수 있어야 한다.

이런 요구사항을 해결하기 위해 WebSocket 이 등장했고, 그 위에 RSocket 이라는 프로토콜이 만들어졌다.


WebSocket: 양방향 통로를 여는 기술

WebSocket이란?

WebSocket은 브라우저와 서버 사이에 지속적인 양방향 연결 을 제공하는 프로토콜이다. HTTP와 달리 한 번 연결이 맺어지면 양쪽 모두 언제든지 메시지를 보낼 수 있다.

[클라이언트] <======양방향 연결======> [서버]

WebSocket의 핵심 특징:

  • Full-duplex: 양쪽이 동시에 데이터를 주고받을 수 있다.
  • Low latency: 연결을 유지하므로 매번 핸드셰이크할 필요가 없다.
  • Browser 지원: 모든 모던 브라우저에서 기본 지원된다.

WebSocket의 한계

WebSocket은 "바이트를 주고받는 통로" 를 제공할 뿐이다. 실제 애플리케이션을 만들려면 다음 질문에 답해야 한다.

  1. 메시지 형식: JSON? Binary? 어떤 구조로 데이터를 주고받을지?
  2. 메시지 구분: 여러 종류의 메시지가 섞여 들어올 때 어떻게 구분할지?
  3. 흐름 제어: 한쪽이 너무 빠르게 보내서 다른 쪽이 처리하지 못하면?
  4. 에러 처리: 연결이 끊기면? 메시지가 유실되면?

이 모든 것을 직접 구현해야 한다. 서비스가 커질수록 이 "자체 프로토콜" 은 점점 복잡해지고, 유지보수가 어려워진다.

예시: WebSocket만으로 협업 편집을 만든다면

WebSocket만으로 협업 편집을 구현한다고 가정해 보자.

// 메시지 타입을 직접 정의해야 함
type Message = {
  type: 'EDIT' | 'CURSOR' | 'COMMENT' | 'PRESENCE' | 'PING' | 'PONG';
  payload: unknown;
  requestId?: string;
};

// 메시지를 받으면 타입별로 분기 처리
websocket.onmessage = (event) => {
  const message = JSON.parse(event.data);
  switch (message.type) {
    case 'EDIT':
      handleEdit(message.payload);
      break;
    case 'CURSOR':
      handleCursor(message.payload);
      break;
    // ... 계속 늘어남
  }
};

서비스가 성장하면 다음 문제가 생긴다.

  • 메시지 타입이 수십 개로 늘어남
  • 각 타입별로 요청-응답 매칭이 필요한 경우가 생김
  • 트래픽이 많아지면 처리 속도 조절이 필요함
  • 연결이 끊겼을 때 재연결/복구 로직이 복잡해짐

RSocket은 이 문제들을 해결하기 위해 만들어진 프로토콜이다.


RSocket: WebSocket 위의 애플리케이션 프로토콜

RSocket이란?

RSocket은 Reactive Streams 기반의 양방향 메시징 프로토콜 이다. WebSocket(또는 TCP) 위에서 동작하며, 다음을 표준화한다.

  • 메시지 프레임 구조: 각 메시지가 어떤 형식으로 전송되는지
  • 스트림 관리: 하나의 연결 안에서 여러 개의 독립적인 스트림을 운영
  • 흐름 제어(Backpressure): 수신자가 처리할 수 있는 만큼만 전송
  • 라이프사이클 관리: 스트림의 시작, 완료, 에러, 취소 처리

WebSocket과 RSocket의 관계

┌─────────────────────────────────────┐
│             Application             │ ← 우리가 만드는 비즈니스 로직
├─────────────────────────────────────┤
│               RSocket               │ ← 메시지 규칙, 스트림 관리, 흐름 제어
├─────────────────────────────────────┤
│              WebSocket              │ ← 양방향 바이트 전송 통로
├─────────────────────────────────────┤
│               TCP/IP                │ ← 네트워크 연결
└─────────────────────────────────────┘
  • WebSocket: 연결을 열고 유지하는 전송 계층(Transport)
  • RSocket: 그 통로 위에서 어떤 방식으로 메시지를 주고받을지 정의한 애플리케이션 프로토콜

비유하자면, WebSocket은 고속도로 이고, RSocket은 교통 법규 다. 고속도로가 있어도 교통 법규가 없으면 차들이 뒤엉켜 혼란스럽다.


RSocket의 4가지 상호작용 모델

RSocket은 상황에 따라 네 가지 통신 모델을 제공한다. 하나의 연결 안에서 이 모델들을 동시에 사용할 수 있다.

1. Request-Response

가장 익숙한 패턴이다. 하나의 요청에 하나의 응답이 돌아온다.

Client ──요청──> Server Client <──응답── Server

사용 예시: 디자인 데이터 조회, 사용자 정보 요청

2. Request-Stream

하나의 요청에 여러 개의 응답 이 스트림으로 돌아온다.

Client  ── 요청 ──> Server 
Client <──응답1── Server
Client <──응답2── Server 
Client <──응답3── Server
Client <── 완료 ── Server

사용 예시: 페이지 목록 스트리밍, 검색 결과 점진적 로딩

3. Fire-and-Forget

요청을 보내고 응답을 기다리지 않는다. 전송 후 잊어버린다.

Client ──요청──> Server (응답 없음)

사용 예시: 분석 이벤트 전송, 로그 기록

4. Channel (양방향 스트림)

양쪽 모두 언제든지 메시지를 보낼 수 있는 지속 스트림이다. 캔버스에서 가장 핵심적으로 사용하는 모델 이다.

Client ──메시지──> Server
Client <──메시지── Server
Client ──메시지──> Server
Client <──메시지── Server
Client ──메시지──> Server ... (계속)

사용 예시: 실시간 협업 편집, 커서 위치 공유, 댓글 실시간 동기화

왜 Channel 모델이 협업 편집에 적합한가

협업 편집에서는 다음이 동시에 일어난다.

  • 클라이언트 → 서버: 내가 만든 편집 내용 전송
  • 서버 → 클라이언트: 다른 사용자의 편집 내용 수신
  • 서버 → 클라이언트: 서버 상태 변화 알림 (예: 일시정지, 재개)

Request-Response로는 서버가 "먼저" 클라이언트에게 메시지를 보낼 수 없다. Channel 모델이 있어야 서버가 필요할 때 즉시 클라이언트에 업데이트를 push 할 수 있다.


Backpressure와 requestN: 흐름 제어의 핵심

왜 흐름 제어가 필요한가

실시간 시스템에서 흐름 제어(Flow Control) 는 매우 중요하다. 다음 상황을 생각해 보자.

[빠른 송신자] ────────────────> [느린 수신자] 초당 1000개 초당 100개 처리 가능

송신자가 초당 1000개를 보내는데, 수신자는 초당 100개만 처리할 수 있다면?

  • 메모리 폭발: 처리 못 한 메시지가 메모리에 쌓임
  • 시스템 다운: 결국 Out of Memory로 크래시
  • 연쇄 장애: 하나의 느린 서비스가 전체 시스템에 영향

Backpressure란?

Backpressure 는 수신자가 "나 지금 이만큼만 받을 수 있어"라고 송신자에게 알려주는 메커니즘이다. 송신자는 그 신호에 맞춰 전송 속도를 조절한다.

비유하자면:

  • Backpressure 없음: 수도꼭지를 최대로 틀어놓고, 컵이 넘치든 말든 상관없이 물을 붓는 것
  • Backpressure 있음: 컵의 물이 줄어들 때마다 조금씩 채우는 것

requestN: RSocket의 흐름 제어 신호

RSocket에서는 requestN 이라는 숫자로 흐름을 제어한다.

  1. 수신자가 requestN(10)을 보냄 → "10개까지 보내도 돼"
  2. 송신자가 10개를 보냄
  3. 수신자가 5개를 처리하고 requestN(5)를 보냄 → "5개 더 보내도 돼"
  4. 이 과정이 계속 반복된다.

requestN이 0이면 어떻게 되나?

requestN이 0이면 송신자는 전송을 멈춰야 한다. 하지만 사용자는 계속 편집을 한다. 이때 발생하는 메시지는 어떻게 할까?

버퍼(requestQueue) 에 쌓아둔다.

send(request):
  if (currentRequestN > 0) {
    // 보낼 수 있으면 즉시 전송
    sendNow(request);
    currentRequestN--;
  } else {
    // 보낼 수 없으면 버퍼에 저장
    requestQueue.push(request);
  }

onRequestN(n):
  currentRequestN += n;
  // 버퍼에 쌓인 요청들을 처리
  while (currentRequestN > 0 && requestQueue.length > 0) {
    const request = requestQueue.shift();
    sendNow(request);
    currentRequestN--;
  }

이 버퍼 관리가 나중에 재연결 시 요청 복구 와 연결된다. (다음 문서에서 자세히 다룬다)


채널(Channel)과 멀티플렉싱

하나의 연결, 여러 개의 스트림

RSocket의 강력한 기능 중 하나는 멀티플렉싱(Multiplexing) 이다. 하나의 WebSocket 연결 안에서 여러 개의 독립적인 채널(스트림) 을 동시에 운영할 수 있다.

왜 멀티플렉싱이 필요한가

서비스마다 WebSocket 연결을 따로 만들면 다음 문제가 생긴다.

  1. 연결 수 폭발: 기능이 늘어날 때마다 연결이 늘어남
  2. 리소스 낭비: 각 연결마다 메모리, 파일 디스크립터 소비
  3. 관리 복잡도: 연결마다 재연결, 인증 등을 개별 관리

RSocket은 하나의 연결 안에서 여러 채널을 독립적으로 관리 한다. 각 채널은 다음 특징을 가진다.

  • 독립적인 requestN (흐름 제어)
  • 독립적인 라이프사이클 (시작, 완료, 에러)
  • 독립적인 메시지 흐름

채널 식별: Stream ID

RSocket은 각 메시지에 Stream ID 를 붙여서 어떤 채널의 메시지인지 구분한다.

┌──────────────────────────────────┐
│          RSocket Frame           │
├──────────────────────────────────┤
│ Stream ID: 5                     │ ← 이 메시지는 5번 스트림에 속함
│ Frame Type: PAYLOAD              │
│ Data: { ... }                    │
│ Metadata: { route: "design" }    │
└──────────────────────────────────┘

이렇게 하면 하나의 연결에서 Design 채널(Stream ID: 5), Comment 채널(Stream ID: 7), Presence 채널(Stream ID: 9)이 동시에 돌아가도 서로 섞이지 않는다.


메타데이터(Metadata)와 라우팅

Data vs Metadata

RSocket 메시지는 DataMetadata 두 부분으로 나뉜다.

  • Data: 실제 비즈니스 데이터 (편집 내용, 응답 결과 등)
  • Metadata: 메시지를 처리하기 위한 부가 정보 (라우팅 경로, 인증 정보 등)
┌──────────────────────────────────┐
│          RSocket Message         │
├──────────────────────────────────┤
│ Metadata:                        │
│ - route: "design.command"        │ ← 어디로 보낼지
│ - auth: "Bearer xxx"             │ ← 인증 정보
│ - traceId: "abc123"              │ ← 추적 ID
├──────────────────────────────────┤
│ Data:                            │
│ { "type": "UPDATE_TEXT",         │ ← 실제 데이터
│   "nodeId": "...",               │
│   "text": "Hello" }              │
└──────────────────────────────────┘

라우팅(Routing)

Metadata에 포함된 route 정보로 서버는 이 메시지를 어떤 핸들러로 보낼지 결정한다. routePath로 채널 종류를 구분한다.

예시:

  • design/{designId} → 디자인 편집 채널
  • comment/{designId} → 댓글 채널
  • client-interaction/{designId} → 접속 상태 채널

RSocket 프레임 구조

RSocket은 다양한 프레임 타입 을 정의한다. 각 프레임은 특정 역할을 수행한다.

주요 프레임 타입

프레임 타입역할프레임 타입역할
SETUP연결 초기화, 설정 정보 전달REQUEST_CHANNEL양방향 채널 요청
PAYLOAD실제 데이터 전송REQUEST_N흐름 제어 (N개 더 받을 수 있음)
CANCEL스트림 취소ERROR에러 발생 알림
KEEPALIVE연결 유지 확인 (heartbeat)

KEEPALIVE: 연결 유지 확인

네트워크 연결은 언제든 끊길 수 있다. RSocket은 KEEPALIVE 프레임 을 주기적으로 주고받아 연결이 살아있는지 확인한다.

Client ──KEEPALIVE──> Server
Client <──KEEPALIVE── Server

일정 시간 동안 KEEPALIVE 응답이 없으면 연결이 끊긴 것 으로 판단하고 재연결을 시도한다. 이것이 "keep-alive timeout"이다.


다이어그램으로 보는 RSocket 채널 통신


요약

이 문서에서 다룬 핵심 개념을 정리한다.

WebSocket과 RSocket

  • WebSocket: 양방향 바이트 전송 통로 (전송 계층)
  • RSocket: WebSocket 위에서 동작하는 애플리케이션 프로토콜
  • WebSocket만으로는 메시지 구조, 흐름 제어, 스트림 관리를 직접 구현해야 한다.

RSocket의 4가지 모델

  • Request-Response: 1:1 요청-응답
  • Request-Stream: 1:N 스트리밍 응답
  • Fire-and-Forget: 응답 없는 일방 전송
  • Channel: 양방향 지속 스트림 (협업 편집의 핵심)

Backpressure와 requestN

  • Backpressure: 수신자가 처리 가능한 만큼만 받는 흐름 제어
  • requestN: "N개 더 받을 수 있다"는 신호
  • requestN이 0이면 버퍼(requestQueue)에 저장한다.

멀티플렉싱과 메타데이터

  • 하나의 연결에서 여러 독립적인 채널 운영
  • Stream ID로 채널 구분
  • Metadata로 라우팅, 인증 등 부가 정보 전달
©2024 dlwl98
github
PostsAbout