Bldev's Blog

R소켓

[네트워크] R소켓 프로토콜

2024. 1. 12.

HTTP는 애플리케이션 프로토콜을 위한 간단한 기능을 제공하지만 애플리케이션 시맨틱스(애플리케이션의 동작과 기능에 대한 의미 해석)를 정의하는데 불충분하다. HTTP는 클라이언트-서버 모델 기반의 단순한 상호작용 모델(interaction model)로서, 클라이언트와 서버 간 보다 복잡한 상호작용을 위해서는 추가적인 기능 지원이 필요하며, 애플리케이션은 이를 위해 요청과 응답 처리, 상태 코드 해석, SSE(server-sent events), 플로우 컨트롤(flow control) 등의 기능을 제공해야 한다.

바이너리 프로토콜인 HTTP/2.0의 다중화(multiplexing) 및 바이너리 프레이밍(binary framing)이 리소스 면에서 더 효율적이고 빠른 네트워크 통신을 가능하게 하지만 양방향(bi-directional)이 아니며, HTTP/2.0가 제공하는 서버 푸시(server push) 기능은 클라이언트의 요청 전에 서버가 클라이언트에 응답을 푸시할 수 있도록 하지만 웹 브라우저가 필요로 하는 리소스인 CSS, 자바스크립트, 이미지 등 웹페이지를 구성하는 정적 파일을 캐싱하고 이를 다운로드하는 용도에 그친다. R소켓(RSocket) 프로토콜은 기존 HTTP 프로토콜의 여러 문제들과 한계점을 해결하고 추가적인 기능을 제공하기 위해 등장하였다.

R소켓은 비동기 리액티브 스트림(asynchronous reactive stream) 시맨틱스를 제공하는 애플리케이션 프로토콜이다. 마이크로서비스 통신과 같은 작업에서 비효율적인 HTTP를 보다 오버헤드가 적은 프로토콜로 대체하기 위해 넷플릭스에서 처음 개발하였다. R소켓은 TCP, 에어론(Aeron), 웹소켓을 통해 바이트 스트림 전송 시 사용하기 위한 바이너리 프로토콜이다. 단일 연결 상에서 비동기 메시지 전달을 통해 다음과 같은 기능을 제공한다.

  • HTTP의 단순 요청-응답 모델 뿐만 아니라 스트리밍 응답, 푸시와 같은 상호작용 모델을 지원
  • 애플리케이션 수준의 플로우 컨트롤 시맨틱스 제공
  • 바이너리 프로토콜 사용, 단일 연결에서 다중화 처리
  • 서버의 작업 취소(cancellation) 및 재개(resumability) 기능

R소켓 프로토콜이 제공하는 상호작용 모델은 다음과 같다.

  • 요청-응답 (request-response): 하나의 메시지를 보내고 응답을 받는다.
  • 요청-스트림 (request-stream): 하나의 메시지를 보내고 메시지 스트림을 받는다.
  • 채널 (channel): 메시지 스트림을 양방향으로 보낸다.
  • 실행 후 망각 (fire-and-forget): 단방향 메시지를 보낸다.

초기 연결이 이루어지면 양측이 대칭이 되고 둘 중 하나가 위의 상호 작용 중 하나를 시작할 수 있으므로 클라이언트와 서버의 구분이 사라진다. 따라서 R소켓 프로토콜에서는 상호작용의 참여 측을 요청자(requester)와 응답자(responder)라고 부르며 위의 상호작용을 요청 스트림(request stream) 또는 간단히 요청이라고 한다.

R소켓은 여러 프로그래밍 언어로 구현되어 있다. 자바 라이브러리는 리액티브 스트림 구현체 중 하나인 프로젝트 리액터(Project Reactor)를 기반으로 구축되었으며, TCP 및 웹소켓 용 전송(transport)은 리액터 네티(Reactor Netty)를 기반으로 한다. 리액터 네티는 HTTP(웹소켓 포함), TCP, UDP 프로토콜을 위한 역압을 지원하는 네트워크 엔진이다. 애플리케이션의 리액티브 스트림 퍼블리셔가 보내는 시그널은 네트워크 전반에서 R소켓 프로토콜을 통해 투명하게(transparently) 전파된다.

R소켓은 리액티브한 특성을 갖는 프로토콜이다. 다음 기능을 통해 애플리케이션 프로토콜 수준의 리액티브 기능을 제공한다.

  • 리액티브 시맨틱스: 애플리케이션 내부가 아닌 네트워크 경계를 가로지르는 리액티브 스트림의 시맨틱스는 다음과 같은 특징을 가진다. 요청-스트림 및 채널과 같은 스트리밍 요청의 경우, 역압(backpressure) 신호가 요청자와 응답자 간에 이동하여 요청자가 응답자의 속도를 늦출 수 있다. 이는 네트워크 계층의 혼잡 제어(congestion control)에 대한 의존도와 네트워크 레벨 또는 모든 레벨에서 버퍼링(buffering)의 필요성을 낮춘다.
  • 요청 스로틀링 (request throttling): 이 기능은 주어진 시간 동안 다른 쪽에서 허용되는 총 요청 수를 제한한다. 각 끝에서 보낼 수 있는 LEASE 프레임의 이름을 따서 이 기능을 임대(leasing)라고 한다. 임대는 주기적으로 갱신된다.
  • 세션 재개 (session resumption): 이 기능은 연결이 끊어지는 상황을 대비하여 일부 상태를 유지하기 위해 설계되었다. 상태 관리는 애플리케이션에 대해 투명하므로 애플리케이션은 이를 신경 쓰지 않고도 세션을 재개할 수 있다. 상태 관리는 프로토콜이 자동으로 처리한다. 상태 관리는 가능한 경우 생산자(producer)를 중지하고 필요한 상태의 양을 줄일 수 있는 역압과 함께 잘 작동한다.

R소켓 프로토콜을 사용한 연결 설정 및 통신 과정

  1. 연결 설정: 처음에 클라이언트는 TCP나 웹소켓과 같은 낮은 수준의 스트리밍 전송을 통해 서버에 연결하고 연결에 대한 파라미터를 설정하기 위해 응답자에게 SETUP 프레임을 전송한다.
    응답자는 SETUP 프레임을 거부할 수 있지만 일반적으로 전송(요청자) 또는 수신(응답자)되고 난 후 양쪽 모두 요청을 시작할 수 있다. SETUP이 요청 횟수를 제한하기 위해 임대 시맨틱스 사용을 타나내지 않는 한 양쪽 모두 요청을 허용하기 위해 상대방의 LEASE 프레임을 기다려야 한다.
  2. 요청: 연결이 설정되면 양쪽 모두 R소켓이 제공하는 상호작용 모델에 대응되는 REQUEST_RESPONSE, REQUEST_STREAM, REQUEST_CHANNEL 또는 REQUEST_FNF 프레임 중 하나를 통해 요청을 시작할 수 있다. 이러한 각 프레임은 요청자로부터 응답자에게 하나의 메시지를 전달한다. 응답자는 응답 메시지와 함께 PAYLOAD 프레임을 반환할 수 있으며, REQUEST_CHANNEL 프레임의 경우 반대로 요청자가 더 많은 요청 메시지가 포함된 PAYLOAD 프레임을 보낼 수도 있다. 요청에 요청 스트림 및 채널과 같은 메시지 스트림이 포함될 경우 응답자는 요청자의 수요(demand) 시그널을 존중해야 한다. 수요는 여러 개의 메시지로 표현된다. 초기 수요는 프레임에 지정됩니다. 후속 수요는 REQUEST_N 프레임을 통해 신호를 보냅니다. 또한 각 측은 개별 요청이 아닌 연결 전체와 관련된 메타데이터 알림을 METADATA_PUSH 프레임을 통해 전송할 수도 있습니다.

플로우 컨트롤

R소켓은 애플리케이션 수준의 플로우 컨트롤 기능을 제공한다. 따라서 애플리케이션의 논리적인 요구사항과 로직에 의한 네트워크 통신량 제어가 가능하다.

R소켓에서는 수신자가 처리할 수 있는 메시지의 수를 조절하기 위해 REQUEST_N이라는 프레임을 사용한다. 이 프레임은 리액티브 스트림 시맨틱스에서 요청자가 응답자에게 보내는 페이로드의 수와 동일한 의미를 갖는다. 송수신자 간에 공유되는 REQUEST_N 프레임을 통해 수신자가 처리할 수 있는 메시지의 수에 따라 통신량이 제어된다.

이러한 애플리케이션 수준의 플로우 컨트롤은 전송 계층의 바이트 기반 플로우 컨트롤과 독립적이다. 따라서 R소켓을 사용하는 경우 애플리케이션의 처리 속도에 따라 수신자의 버퍼가 가득 차기 전에 메시지의 수를 조절할 수 있다.


gRPC와 R소켓

gRPC와 R소켓은 모두 고성능 마이크로서비스 통신을 위해 설계되었지만, 철학과 구현 방식에서 다음과 같은 차이가 있다.

  • 프로토콜 계층: gRPC는 HTTP/2 기반의 RPC(remote procedure call) 프레임워크인 반면, R소켓은 전송 계층 위에서 동작하는 메시지 기반의 애플리케이션 프로토콜이다.
  • 상호작용 모델: gRPC는 단일 요청/응답(unary)과 스트리밍을 지원하지만 기본적으로 클라이언트-서버 모델을 따른다. R소켓은 대칭적(symmetric) 구조를 가져 연결이 맺어지면 누가 요청을 시작하든 상관없는 P2P 방식이며, 4가지 상호작용 모델을 네이티브하게 지원한다.
  • 플로우 컨트롤(역압): gRPC는 HTTP/2의 바이트 기반 플로우 컨트롤에 의존하므로 애플리케이션 로직의 세밀한 제어에는 한계가 있다. 반면 R소켓은 애플리케이션 수준의 요소 기반(logical element-level) 역압을 제공하여 더 세밀한 제어가 가능하다. 리액티브 스트림의 request(n) 시맨틱을 네트워크 경계를 넘어 적용하므로, 애플리케이션이 처리 가능한 실제 아이템 개수 기반의 정교한 제어가 가능하다.
  • 데이터 직렬화: gRPC는 프로토콜 버퍼(Protobuf)에 강하게 결합되어 있는 반면, R소켓은 직렬화 방식에 중립적이어서 JSON, CBOR, Protobuf 등을 자유롭게 선택할 수 있다. R소켓은 바이너리 프레임 내의 페이로드를 어떤 방식으로 직렬화하든 상관없는 직렬화 중립적 프로토콜이다.

스프링과 R소켓

스프링 프레임워크 5.2부터 R소켓을 공식적으로 지원하기 시작했으며, 프로젝트 리액터의 리액터 타입(Mono, Flux)과 완벽하게 통합되어 동작한다.

spring-boot-starter-rsocket 스타터 의존성을 추가하면 R소켓 서버 및 클라이언트를 위한 인프라가 자동으로 구성된다. 컨트롤러에서 @MessageMapping 어노테이션을 사용하여 각 경로(Route)에 대한 요청 핸들러를 정의할 수 있다. 연결 시점의 로직은 @ConnectMapping 어노테이션을 통해 처리한다. @ConnectMapping 어노테이션은 초기 연결 수립 시 인증이나 상태 설정 로직을 처리하는 용도로 사용된다.

RSocketRequester는 클라이언트 측에서 요청을 보낼 때 사용하는 유연한 대화형 API이다. 이를 사용하여 인코딩/디코딩, 메타데이터 설정 등을 간편하게 처리할 수 있다. RSocketRequester는 기존의 R소켓 인터페이스를 직접 다루는 것보다 훨씬 유연하고 편리한 대화형 API를 제공하여 클라이언트 측 개발을 용이하게 한다.

스프링 6부터는 @RSocketExchange 어노테이션을 사용하는 인터페이스를 정의하기만 하면, 구현체 없이도 R소켓 서비스를 호출할 수 있는 선언적 클라이언트 기능을 지원한다. HTTP의 WebClient에 대응하는 HttpServiceProxyFactory처럼 R소켓에서도 인터페이스 기반의 선언적 클라이언트를 생성할 수 있게 되었다.

스프링 시큐리티를 통한 보안 설정, 스프링 클라우드 게이트웨이를 통한 R소켓 라우팅 등 스프링 생태계의 다양한 도구들과 함께 사용할 수 있다.


프로메테우스 R소켓 프록시

프로메테우스 R소켓 프록시(Prometheus RSocket Proxy)링크는 애플리케이션이 인그레스(ingress)를 오픈할 수 없을 때(인바운드 트래픽은 허용하지 않고 아웃바운드 트래픽만 허용할 때) R소켓을 사용하여 애플리케이션으로부터 메트릭을 가져와 프로메테우스에게 제공하는 역할을 하는 프록시이다. 애플리케이션 관찰가능성(observability) 도구인 마이크로미터 프로젝트에 의해 제공되고 있다.

동작 방식은 다음과 같다.

  1. 애플리케이션은 R소켓 프록시 또는 프록시 클러스터에 TCP R소켓 연결을 생성한다. R소켓 연결이 설정되면 클라이언트와 서버의 구분이 사라지며, 프록시는 각각의 애플리케이션 인스턴스에서 메트릭을 가져올 때 요청자의 역할을 할 수 있다.
  2. 프로메테우스는 애플리케이션 인스턴스가 아닌 프록시의 /metrics/connected/metrics/proxy 엔드포인트를 스크래핑한다.
  3. 프록시가 프로메테우스로부터 스크래핑 요청을 받으면 프록시는 요청/응답 시퀀스를 사용하여 여러 애플리케이션 인스턴스와의 각각의 R소켓 연결을 통해 메트릭을 가져온다. 여러 R소켓 연결의 결과들은 서로 합쳐져서 하나의 응답으로 프로메테우스에 표시된다.
sequenceDiagram
프로메테우스->>R소켓 프록시: 애플리케이션 대신 프록시에게 메트릭 스크래핑 요청
R소켓 프록시->애플리케이션: 애플리케이션으로부터 메트릭 조회

애플리케이션 메트릭 정보가 클라이언트로부터 프록시에게 지속적으로 전송되도록 양방향 연결이 오랜 시간 동안 지속될 필요가 없으며, 이는 클라이언트가 프록시에 자동으로 다시 연결되기 때문이다. 따라서 연결된 클라이언트의 중단에 대한 염려 없이 프록시 클러스터는 자동 스케일 아웃(scale-out)되거나 연결의 균형이 재조정되도록 구성될 수 있다.


TLS와 보안

R소켓은 전송 계층(Transport Layer)에 독립적인 프로토콜이므로, 보안 역시 하위 전송 계층(TCP, WebSocket 등)의 보안 메커니즘에 의존한다.

전송 계층 보안 (TLS)

R소켓이 TCP나 웹소켓 위에서 동작할 때, TLS(Transport Layer Security)를 사용하여 통신 과정을 암호화할 수 있다. TLS는 다음과 같은 보안 기능을 제공한다.

  • 암호화 (Encryption): 전송되는 데이터를 암호화하여 도청을 방지하고 기밀성을 보장한다.
  • 인증 (authentication): 디지털 인증서를 통해 서버(및 필요한 경우 클라이언트)의 신원을 확인하여 위장 서버 등을 방지한다.
  • 무결성 (Integrity): 데이터가 전송 중에 변조되지 않았음을 보장한다.

애플리케이션 수준의 보안

전송 계층의 암호화 외에도 R소켓은 메타데이터를 사용하여 애플리케이션 수준의 인증(authentication) 및 인가(authorization)를 구현할 수 있다. R소켓은 메타데이터 기반 인증을 지원한다. R소켓 프레임의 메타데이터 영역에 인증 정보를 포함하여 전달한다. 데이터와 분리된 메타데이터를 사용하므로 페이로드 처리 로직과 보안 로직을 분리할 수 있다.

주요 인증 방식은 다음과 같다.

  • 단순 인증: 사용자 이름과 비밀번호를 전송하는 전통적인 방식이다.
  • JWT (Bearer Token): 토큰 기반 인증 방식으로, 클라이언트가 발행받은 JWT를 메타데이터에 포함하여 서버로 전송하며 서버는 이를 검증하여 사용자를 식별한다.

인증 시점은 다음과 같다.

  • 설정 시점 (Setup-time): SETUP 프레임을 통해 연결이 처음 수립될 때 한 번만 인증을 수행한다. 모바일 앱처럼 단일 사용자가 고유한 연결을 유지하는 환경에 적합하다.
  • 요청 시점 (Request-time): 개별 요청인 PAYLOAD 프레임마다 인증 정보를 포함한다. 웹 애플리케이션이 여러 사용자의 요청을 하나의 R소켓 연결로 중계하는 공유 연결 환경에서 세밀한 권한 제어가 필요할 때 사용한다.

스프링 시큐리티 지원

스프링은 spring-security-rsocket 모듈을 통해 R소켓 보안을 강력하게 지원한다. @EnableRSocketSecurity 어노테이션을 통해 활성화할 수 있으며, 인터셉터(PayloadSocketAcceptorInterceptor)를 사용하여 특정 경로에 대한 접근 권한을 설정할 수 있다. 또한, 핸들러 메서드에서 @AuthenticationPrincipal을 통해 인증된 사용자의 정보를 주입받아 비즈니스 로직에 활용할 수 있다.


참고