HTTP는 "클라이언트가 요청하면 서버가 응답하는" 구조다. 그런데 서버가 지속적으로 데이터를 밀어줘야 하고, 클라이언트도 동시에 데이터를 보내야 하고, 받는 쪽이 감당할 수 있는 속도로만 보내야 한다면? HTTP로 이 모든 걸 해결할 수 있을까?

RSocket이란

RSocket은 애플리케이션 레벨의 바이너리 메시징 프로토콜 입니다. TCP, WebSocket, Aeron 같은 전송 계층 위에서 동작하며, 양방향 통신과 멀티플렉싱을 기본으로 지원합니다.

핵심 특징을 한 줄씩 정리하면 이렇습니다.

  • **바이너리 프로토콜 **: HTTP/1.1처럼 텍스트가 아니라 바이너리 프레임으로 통신합니다. 파싱이 빠르고 오버헤드가 적습니다
  • ** 양방향(Bidirectional)**: 클라이언트와 서버 구분 없이 양쪽 모두 요청을 보낼 수 있습니다
  • ** 멀티플렉싱 **: 하나의 연결에서 여러 스트림을 동시에 처리합니다. HTTP/2의 멀티플렉싱과 비슷한 개념입니다
  • ** 배압(Backpressure)**: Reactive Streams 사양을 프로토콜 레벨에서 지원합니다. 수신자가 처리할 수 있는 만큼만 데이터를 요청합니다
  • ** 연결 재개(Resumption)**: 네트워크가 끊겼다 복구되면, 새 연결을 맺지 않고 이전 세션을 이어갈 수 있습니다
PLAINTEXT
┌──────────────────────────────────────────────┐
│              RSocket 프로토콜 스택              │
├──────────────────────────────────────────────┤
│  애플리케이션 계층: 4가지 상호작용 모델           │
│  (Request-Response, Fire-and-Forget,          │
│   Request-Stream, Channel)                    │
├──────────────────────────────────────────────┤
│  프레이밍 계층: 바이너리 프레임 인코딩/디코딩      │
│  (SETUP, REQUEST_RESPONSE, REQUEST_STREAM,    │
│   REQUEST_N, CANCEL, PAYLOAD, ERROR ...)      │
├──────────────────────────────────────────────┤
│  전송 계층: TCP / WebSocket / Aeron            │
│  (Netty가 이 계층을 담당)                       │
└──────────────────────────────────────────────┘

RSocket은 Facebook(현 Meta), Netflix, Pivotal(현 VMware Tanzu)이 공동으로 설계했습니다. 특히 Netflix가 마이크로서비스 간 내부 통신에서 HTTP의 한계를 느끼고 주도적으로 개발에 참여했습니다.


4가지 상호작용 모델

RSocket의 가장 큰 특징은 4가지 상호작용 모델 을 프로토콜 레벨에서 제공한다는 것입니다. HTTP의 요청-응답만으로는 표현할 수 없는 패턴들을 깔끔하게 지원합니다.

1. Request-Response

가장 익숙한 패턴입니다. 요청 하나를 보내고 응답 하나를 받습니다.

JAVA
// 클라이언트 — 요청을 보내고 단일 응답 수신
Mono<Payload> response = rSocket.requestResponse(
    DefaultPayload.create("Hello")  // 요청 페이로드
);

response.subscribe(payload ->
    System.out.println("응답: " + payload.getDataUtf8())
);

HTTP의 GET/POST와 비슷하지만, RSocket은 바이너리 프레임이므로 헤더 오버헤드가 없고 하나의 연결을 멀티플렉싱합니다.

2. Fire-and-Forget

요청을 보내고 **응답을 기다리지 않습니다 **. 반환 타입이 Mono<Void>입니다.

JAVA
// 메트릭 전송 — 응답이 필요 없는 시나리오
Mono<Void> result = rSocket.fireAndForget(
    DefaultPayload.create("{\"cpu\": 72.5, \"memory\": 4096}")
);

result.subscribe();  // 보내기만 하고 끝

메트릭 수집, 로그 전송, 이벤트 알림처럼 "보내기만 하면 되는" 시나리오에 적합합니다. HTTP에서 응답 본문을 무시하더라도 서버는 여전히 응답을 만들어 보내야 하지만, Fire-and-Forget은 프로토콜 수준에서 응답 자체가 없습니다.

3. Request-Stream

요청 하나를 보내고 ** 여러 개의 응답을 스트림으로** 받습니다.

JAVA
// 주식 시세 구독 — 하나의 요청으로 지속적인 데이터 수신
Flux<Payload> stream = rSocket.requestStream(
    DefaultPayload.create("AAPL")  // 애플 주식 시세 요청
);

stream
    .doOnNext(payload ->
        System.out.println("시세: " + payload.getDataUtf8()))
    .take(100)       // 100개만 받겠다는 의미 — 배압 제어
    .subscribe();

SSE(Server-Sent Events)와 비슷하지만 결정적인 차이가 있습니다. ** 배압이 프로토콜에 내장 **되어 있어서 클라이언트가 REQUEST_N 프레임으로 "n개만 더 보내줘"라고 제어할 수 있습니다.

4. Channel (양방향 스트림)

양쪽 모두가 ** 동시에 스트림을 보내는** 패턴입니다. 가장 유연하고 복잡한 모델입니다.

JAVA
// 양방향 채팅 — 클라이언트와 서버가 동시에 메시지 스트림을 교환
Flux<Payload> clientMessages = Flux.interval(Duration.ofSeconds(1))
    .map(i -> DefaultPayload.create("클라이언트 메시지 #" + i));

Flux<Payload> serverResponses = rSocket.requestChannel(clientMessages);

serverResponses
    .doOnNext(payload ->
        System.out.println("서버로부터: " + payload.getDataUtf8()))
    .subscribe();

WebSocket과 비슷해 보이지만, Channel은 ** 배압이 양방향으로 동작 **합니다. 클라이언트가 서버에게, 서버가 클라이언트에게 각각 독립적으로 흐름 제어를 할 수 있습니다.

4가지 모델 요약

모델요청응답반환 타입대표 사용 사례
Request-Response11Mono<Payload>API 호출, 단건 조회
Fire-and-Forget10Mono<Void>메트릭, 로그 전송
Request-Stream1NFlux<Payload>시세 구독, 이벤트 스트림
ChannelNNFlux<Payload>채팅, 양방향 동기화

왜 HTTP가 아닌 RSocket인가

HTTP/1.1과 HTTP/2도 훌륭한 프로토콜이지만, 리액티브 마이크로서비스 환경에서는 구조적인 한계가 있습니다.

배압 (Backpressure)

HTTP에는 배압 메커니즘이 없습니다. 서버가 응답을 쏟아부으면 클라이언트는 그걸 다 받아야 합니다. TCP 레벨의 흐름 제어(윈도우 사이즈)가 있긴 하지만, 이건 바이트 단위이지 메시지 단위가 아닙니다.

RSocket은 REQUEST_N 프레임으로 "앞으로 n개의 메시지만 보내줘" 라고 수신자가 직접 제어합니다.

PLAINTEXT
수신자 → 송신자: REQUEST_N(n=5)    // "5개만 보내줘"
송신자 → 수신자: PAYLOAD (1)
송신자 → 수신자: PAYLOAD (2)
송신자 → 수신자: PAYLOAD (3)
수신자 → 송신자: REQUEST_N(n=3)    // "3개 더 보내도 돼"
송신자 → 수신자: PAYLOAD (4)
송신자 → 수신자: PAYLOAD (5)
송신자 → 수신자: PAYLOAD (6)

이건 Reactive Streams 사양(Publisher.subscribe → Subscription.request(n))이 네트워크 프로토콜로 확장된 것입니다.

양방향 스트리밍

HTTP/2도 스트리밍을 지원하지만 여전히 ** 클라이언트가 먼저 요청 **해야 합니다. 서버가 자발적으로 클라이언트에게 데이터를 보내려면 Server Push라는 별도 메커니즘이 필요하고, 브라우저에서는 이마저도 대부분 비활성화했습니다.

RSocket의 Channel 모델은 연결이 맺어진 순간부터 양쪽이 대등합니다. 역할 구분 없이 양방향으로 스트림을 보낼 수 있습니다.

연결 재개 (Resumption)

모바일 환경이나 불안정한 네트워크에서 TCP 연결이 끊기면, HTTP는 새 연결을 맺고 요청을 처음부터 다시 보내야 합니다. RSocket은 ** 세션 레벨의 Resumption**을 지원합니다.

PLAINTEXT
1. 최초 연결: SETUP 프레임에 Resume Token 포함
2. 연결 끊김 감지
3. 새 TCP 연결 수립
4. RESUME 프레임 전송 (Resume Token + 마지막 수신 위치)
5. 서버가 Resume Token 검증 → 이전 세션 복구
6. 끊긴 지점부터 데이터 재전송

덕분에 스트리밍 중에 네트워크가 잠깐 끊겨도 처음부터 다시 시작할 필요가 없습니다.


Netty Transport — RSocket의 전송 계층

RSocket은 프로토콜 사양일 뿐이고, 실제 바이트를 네트워크로 보내는 건 ** 전송 계층 **의 역할입니다. RSocket-Java는 Netty를 기본 전송 계층으로 사용합니다.

왜 Netty인가

이 시리즈를 따라왔다면 이미 답을 알고 있을 것입니다.

  • EventLoop 기반 비동기 I/O: RSocket은 리액티브 프로토콜이므로, 전송 계층도 논블로킹이어야 합니다. Netty의 EventLoop가 이걸 자연스럽게 해결합니다
  • **ByteBuf와 메모리 풀링 **: RSocket 프레임을 바이트로 인코딩/디코딩할 때 Netty의 PooledByteBufAllocator가 메모리 할당 오버헤드를 줄여줍니다
  • ChannelPipeline: RSocket 프레임 코덱, 길이 기반 프레이밍, SSL/TLS를 파이프라인에 조합할 수 있습니다
  • **TCP + WebSocket 모두 지원 **: 같은 Netty 위에서 TCP 전송과 WebSocket 전송을 선택할 수 있습니다

내부 구조

RSocket-Java가 Netty 위에서 동작하는 구조를 살펴보면 이렇습니다.

PLAINTEXT
┌─────────────────────────────────────────────┐
│  RSocket API (Mono/Flux 기반 상호작용 모델)    │
├─────────────────────────────────────────────┤
│  RSocket-Core (프레임 생성, 스트림 관리)       │
├─────────────────────────────────────────────┤
│  RSocket-Transport-Netty                     │
│  ┌─────────────────────────────────────┐     │
│  │  Netty ChannelPipeline              │     │
│  │  ┌───────────┐  ┌────────────────┐  │     │
│  │  │ LengthField│  │ RSocket Frame │  │     │
│  │  │ Codec     │  │ Codec         │  │     │
│  │  └───────────┘  └────────────────┘  │     │
│  └─────────────────────────────────────┘     │
├─────────────────────────────────────────────┤
│  Netty EventLoop (NioEventLoopGroup)         │
├─────────────────────────────────────────────┤
│  TCP / WebSocket                             │
└─────────────────────────────────────────────┘

TCP 전송의 경우, RSocket 프레임 앞에 3바이트 길이 필드 가 붙습니다. Netty의 LengthFieldBasedFrameDecoder가 이 길이를 읽고 프레임 경계를 잡아줍니다. TCP 프레이밍 글에서 다뤘던 그 패턴입니다.

WebSocket 전송의 경우, WebSocket 프레임 자체가 메시지 경계를 제공하므로 별도의 길이 필드가 필요 없습니다.

JAVA
// RSocket 서버를 Netty TCP 전송으로 시작하는 코드
RSocketServer.create(
        SocketAcceptor.with(new MyRSocketHandler()))  // 요청 처리기
    .bind(TcpServerTransport.create("localhost", 7000))  // Netty TCP 전송
    .subscribe();

// WebSocket 전송으로 바꾸려면 전송 계층만 교체
RSocketServer.create(
        SocketAcceptor.with(new MyRSocketHandler()))
    .bind(WebsocketServerTransport.create("localhost", 8080))  // Netty WebSocket 전송
    .subscribe();

전송 계층을 바꿔도 RSocket 핸들러 코드는 그대로입니다. 이게 프로토콜과 전송을 분리한 RSocket 설계의 장점입니다.


RSocket + Spring Boot

Spring Boot는 spring-boot-starter-rsocket을 통해 RSocket을 1급 시민(first-class citizen)으로 지원합니다. 내부적으로 Spring Messaging + RSocket-Java + Netty가 조합됩니다.

의존성 추가

XML
<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-rsocket</artifactId>
</dependency>

서버 설정

YAML
# application.yml
spring:
  rsocket:
    server:
      port: 7000              # RSocket 서버 포트
      transport: tcp           # tcp 또는 websocket

@MessageMapping으로 요청 처리

Spring의 @MessageMapping을 사용하면, RSocket의 4가지 모델을 메서드 시그니처만으로 구분할 수 있습니다.

JAVA
@Controller
public class GreetingController {

    // Request-Response: Mono 입력 → Mono 출력
    @MessageMapping("greeting")
    public Mono<String> greeting(String name) {
        return Mono.just("안녕하세요, " + name + "님!");
    }

    // Fire-and-Forget: 입력만 있고 반환은 Mono<Void>
    @MessageMapping("log")
    public Mono<Void> log(LogEvent event) {
        System.out.println("로그 수신: " + event);
        return Mono.empty();
    }

    // Request-Stream: 단일 입력 → Flux 출력
    @MessageMapping("stock.{symbol}")
    public Flux<StockPrice> stockStream(@DestinationVariable String symbol) {
        return stockService.getStream(symbol);  // 지속적으로 시세 전송
    }

    // Channel: Flux 입력 → Flux 출력
    @MessageMapping("chat")
    public Flux<ChatMessage> chat(Flux<ChatMessage> incoming) {
        return incoming
            .doOnNext(msg -> broadcast(msg))    // 받은 메시지 브로드캐스트
            .switchMap(msg -> messageStream());  // 다른 사용자 메시지 스트림 반환
    }
}

@MessageMapping의 값(예: "greeting")은 HTTP URL이 아니라 RSocket의 route 메타데이터 에 매핑됩니다. 클라이언트가 요청할 때 이 route를 지정합니다.

클라이언트 코드

JAVA
// RSocketRequester — Spring이 제공하는 클라이언트 API
RSocketRequester requester = RSocketRequester.builder()
    .tcp("localhost", 7000);

// Request-Response 호출
Mono<String> response = requester
    .route("greeting")           // route 메타데이터 지정
    .data("개발자")               // 요청 데이터
    .retrieveMono(String.class); // 단일 응답

// Request-Stream 호출
Flux<StockPrice> prices = requester
    .route("stock.AAPL")
    .retrieveFlux(StockPrice.class);  // 스트림 응답

HTTP의 RestTemplate이나 WebClient처럼, RSocketRequester가 연결 관리와 직렬화를 추상화합니다.


RSocket vs gRPC vs WebSocket

세 기술 모두 "HTTP/1.1보다 효율적인 통신"을 목표로 하지만, 설계 철학과 적합한 시나리오가 다릅니다.

구분RSocketgRPCWebSocket
프로토콜 계층애플리케이션 레벨 (TCP/WebSocket 위)HTTP/2 위TCP 위 (HTTP 업그레이드)
** 메시지 형식**바이너리 (형식 무관)Protocol Buffers (스키마 필수)텍스트 또는 바이너리 (형식 무관)
** 상호작용 모델**4가지 (R-R, FnF, Stream, Channel)4가지 (Unary, Server/Client/Bidi Streaming)양방향 메시지 (모델 정의 없음)
** 배압**프로토콜 내장 (REQUEST_N)HTTP/2 흐름 제어 (바이트 단위)없음 (애플리케이션이 직접 구현)
** 연결 재개**Resumption 지원없음 (재연결 필요)없음 (재연결 필요)
** 스키마 정의**선택사항필수 (.proto 파일)없음
** 브라우저 지원**WebSocket 전송으로 가능grpc-web 프록시 필요네이티브 지원
** 생태계**Spring 중심언어 중립적, 폭넓은 생태계모든 언어/프레임워크
** 대표 사용처**리액티브 마이크로서비스이종 시스템 간 RPC실시간 웹 (채팅, 게임)

정리하면 이렇습니다.

  • gRPC: 스키마(proto) 기반으로 서비스 계약을 강제하고 싶을 때. 언어가 다양한 마이크로서비스 환경에 적합합니다
  • WebSocket: 브라우저와 서버 간 실시간 양방향 통신이 핵심일 때. 프로토콜이 단순해서 진입 장벽이 낮습니다
  • RSocket: 리액티브 스트림 + 배압이 중요한 백엔드 간 통신에 적합합니다. Spring/Reactor 생태계와 궁합이 좋습니다

gRPC도 양방향 스트리밍을 지원하지만, 배압은 HTTP/2의 바이트 단위 흐름 제어에 의존합니다. RSocket처럼 "메시지 n개만 보내줘"라는 논리적 배압은 아닙니다.


실제 사용 사례

Spring Cloud Gateway RSocket

Spring Cloud Gateway는 HTTP 게이트웨이로 유명하지만, RSocket 버전 도 존재합니다. 마이크로서비스들이 게이트웨이에 RSocket으로 등록하고, 클라이언트 요청을 라우팅합니다.

PLAINTEXT
클라이언트 ──RSocket──→ Gateway ──RSocket──→ 서비스 A

                           └──RSocket──→ 서비스 B

HTTP 게이트웨이와 달리 양방향 스트리밍과 배압이 게이트웨이를 통과해도 유지됩니다.

마이크로서비스 간 내부 통신

Netflix가 RSocket을 만든 원래 목적입니다. 서비스 간 통신에서 HTTP의 한계를 느낀 지점들이 있습니다.

  • **연결 비용 **: HTTP/1.1은 요청마다 연결을 맺거나 커넥션 풀을 관리해야 합니다. RSocket은 하나의 연결을 멀티플렉싱합니다
  • ** 불필요한 응답 대기 **: 메트릭이나 로그 전송에 Fire-and-Forget을 쓰면 응답 대기 시간이 사라집니다
  • ** 과부하 방지 **: 트래픽이 몰릴 때 배압으로 수신자가 감당 가능한 속도를 제어합니다

IoT / 모바일 환경

연결이 불안정한 환경에서 Resumption이 빛을 발합니다. 모바일 앱이 Wi-Fi에서 셀룰러로 전환될 때, 기존 세션을 유지하면서 끊긴 데이터만 재전송할 수 있습니다.


정리

RSocket은 "리액티브 시대의 통신 프로토콜"이라고 볼 수 있습니다. HTTP가 해결하지 못하는 배압, 양방향 스트리밍, 연결 재개를 프로토콜 레벨에서 지원하고, Netty가 그 밑에서 효율적인 비동기 I/O를 담당합니다.

기억할 포인트를 정리하면 이렇습니다.

  • RSocket은 **4가지 상호작용 모델 **(R-R, FnF, Stream, Channel)을 제공하고, 메서드 시그니처로 구분합니다
  • ** 배압이 프로토콜에 내장 **되어 있어서 REQUEST_N 프레임으로 수신자가 흐름을 제어합니다
  • Netty가 전송 계층 을 담당하며, TCP와 WebSocket 전송을 바꿔 끼울 수 있습니다
  • Spring Boot에서는 @MessageMapping으로 HTTP만큼 쉽게 RSocket 서버를 만들 수 있습니다
  • gRPC는 스키마 기반 계약이 강점이고, RSocket은 리액티브 배압이 강점입니다. 용도에 따라 선택하면 됩니다
댓글 로딩 중...