Spring RSocket 마스터 노트 시리즈 2편. 4 Interaction Models의 결정적 차이와 선택 기준, Request-Response가 HTTP와 비슷하지만 다른 점, Fire-and-Forget의 적합·부적합 영역, Request-Stream의 백프레셔 자연스러움, Channel(N:N 양방향)이 풀어내는 채팅·실시간 협업 시나리오, 4 모델별 코드 패턴까지.
이 글은 Spring RSocket 마스터 노트 시리즈의 두 번째 편입니다. 1편(기초)에서 RSocket 큰 그림을 다졌다면, 이번엔 그 핵심 — 4 Interaction Models.
이 4 모델로 거의 모든 통신 패턴 커버. 어떤 모델을 어디에 쓸지가 RSocket 설계의 핵심.
처음 4 Models가 어렵게 느껴지는 이유
처음 이 단원이 어렵게 느껴지는 이유는 두 가지예요. 첫째, Channel과 Request-Stream 차이가 막연합니다. 둘 다 스트림인데? 둘째, Fire-and-Forget이 진짜 필요한가 의문이 듭니다.
해결법은 한 가지예요. "입력 N개 / 출력 N개" 표로 묶기. 입력 1·출력 1=Request-Response, 입력 1·출력 0=FNF, 입력 1·출력 N=Stream, 입력 N·출력 N=Channel. 이 표만 잡으면 끝.
4 모델 한 줄 정리
| 모델 | 입력 | 출력 | Mono/Flux |
|---|---|---|---|
| Request-Response | 1 | 1 | Mono → Mono |
| Fire-and-Forget | 1 | 0 | Mono → void |
| Request-Stream | 1 | N | Mono → Flux |
| Channel | N | N | Flux → Flux |
1. Request-Response — HTTP 같음
Client ─request 1─→ Server
←─response 1─
전형적인 요청-응답. HTTP GET/POST 비슷.
서버
@Controller
public class UserController {
@MessageMapping("user.{id}")
public Mono<User> getUser(@DestinationVariable String id) {
return userRepo.findById(id);
}
}
클라이언트
Mono<User> user = requester.route("user.123")
.retrieveMono(User.class);
user.subscribe(System.out::println);
사용처
- CRUD 조회
- 인증 확인
- 단일 변환·검증
여기서 시험 함정이 하나 있어요. HTTP보다 가볍고 빠름. 단일 연결·바이너리·작은 헤더. 단순 RPC도 RSocket이 효율 좋음.
2. Fire-and-Forget — 응답 없음
Client ─request 1─→ Server
(응답 X)
서버가 처리하지만 응답 안 함.
서버
@MessageMapping("event.log")
public Mono<Void> logEvent(LogEvent event) {
return eventService.persist(event); // Mono<Void>
}
클라이언트
requester.route("event.log")
.data(event)
.send() // .send() — Mono<Void>
.subscribe();
retrieveMono() 아닌 send() 사용.
사용처
- 로깅·메트릭 (응답 불필요)
- 이벤트 발행 (한 번 알리고 끝)
- 분석 데이터 수집
- 알림 발송 (성공 여부 무관)
여기서 정말 중요한 시험 함정 — FNF는 보장 X. 네트워크 끊김 시 손실. 보장 필요 = Request-Response (ack 받음). 또는 Kafka 같은 영속 메시지 큐.
3. Request-Stream — 1:N
Client ─request 1─→ Server
←─response 1─
←─response 2─
←─response 3─
...
←─complete─
한 요청에 여러 응답. 백프레셔 자연스러움.
서버
@MessageMapping("stocks.watch")
public Flux<StockPrice> watchStock(String symbol) {
return stockService.priceStream(symbol); // 무한 또는 종료
}
클라이언트
Flux<StockPrice> prices = requester.route("stocks.watch")
.data("AAPL")
.retrieveFlux(StockPrice.class);
prices
.take(100) // 100개만
.subscribe(p -> System.out.println(p));
백프레셔 동작
prices.subscribe(new BaseSubscriber<>() {
@Override
protected void hookOnSubscribe(Subscription sub) {
sub.request(10); // 10개만 처음 요청
}
@Override
protected void hookOnNext(StockPrice price) {
// 처리
process(price);
// 1개 더 요청
request(1);
}
});
내부적으로 REQUEST_N 프레임 자동.
사용처
- 실시간 시세 (주식·암호화폐)
- 로그 스트리밍
- 이벤트 구독
- 검색 결과 대량 조회 (페이징 대신)
- 알림 채널
여기서 정말 중요한 시험 함정 — Request-Stream = HTTP의 SSE(Server-Sent Events) 대체. 더 효율적·백프레셔 지원·양방향 가능 (Channel로 발전).
4. Channel — N:N 양방향
Client ─request 1─→ Server
←─response 1─
Client ─request 2─→
←─response 2─
Client ─request 3─→
...
양방향 N개 메시지 동시 흐름. 가장 강력한 모델.
서버
@MessageMapping("chat.room.{id}")
public Flux<ChatMessage> chat(
@DestinationVariable String id,
Flux<ChatMessage> incoming
) {
return incoming
.doOnNext(msg -> log.info("Received: {}", msg))
.flatMap(msg -> chatRoom.broadcast(id, msg)) // 다른 클라이언트에 전달
.mergeWith(chatRoom.subscribe(id)) // 자신도 받음
.delayElements(Duration.ofMillis(10));
}
Flux<T> 입력 + Flux<R> 출력.
클라이언트
Flux<ChatMessage> outgoing = userInputFlux(); // 사용자 입력 스트림
Flux<ChatMessage> incoming = requester.route("chat.room.123")
.data(outgoing)
.retrieveFlux(ChatMessage.class);
incoming.subscribe(msg -> displayMessage(msg));
사용처
- 채팅·실시간 협업
- 온라인 게임
- 실시간 협업 편집 (Google Docs 같은)
- 양방향 텔레메트리 (IoT)
- 트레이딩 시스템
여기서 정말 중요한 시험 함정 — Channel = WebSocket의 강력한 대체. WebSocket은 메시지 의미 X, Channel은 Reactive Streams + 백프레셔 + 메타데이터까지.
모델 선택 결정 표
| 시나리오 | 선택 |
|---|---|
| 단일 조회·생성·수정 | Request-Response |
| 로깅·이벤트 발행 (응답 X) | Fire-and-Forget |
| 실시간 시세·로그 스트림 | Request-Stream |
| 채팅·협업·게임 | Channel |
| 대용량 검색 결과 | Request-Stream |
| 양방향 IoT 텔레메트리 | Channel |
결합 — 한 서버에 여러 모델
같은 컨트롤러에 4 모델 모두:
@Controller
public class TradingController {
@MessageMapping("order.place") // Request-Response
public Mono<OrderResult> place(Order order) { ... }
@MessageMapping("event.log") // Fire-and-Forget
public Mono<Void> log(Event event) { ... }
@MessageMapping("price.subscribe") // Request-Stream
public Flux<Price> subscribe(String symbol) { ... }
@MessageMapping("trade.session") // Channel
public Flux<TradeMessage> session(Flux<TradeMessage> input) { ... }
}
같은 RSocket 연결에 4 모델 동시.
백프레셔 자연스러움
// 클라이언트가 천천히 처리
Flux<Event> events = requester.route("events")
.retrieveFlux(Event.class);
events
.flatMap(event -> processSlowly(event), 4) // 동시 4
// 자동으로 서버에 "4개씩만 보내라"
.subscribe();
내부적으로 RSocket이 REQUEST_N 프레임 자동 관리.
모델 변경 — 같은 라우트에 다른 모델
여기서 시험 함정이 하나 있어요. 같은 라우트는 한 모델만. @MessageMapping("foo") 메서드가 Mono<X> 반환 = Request-Response 또는 FNF만. Flux 반환 = Stream·Channel만.
다른 모델이 필요하면 다른 라우트.
에러 전파
// 서버
return Flux.error(new BusinessException("Invalid"));
// 클라이언트
flux.doOnError(e -> {
if (e instanceof BusinessException) {
// 비즈니스 에러
}
}).subscribe();
여기서 시험 함정이 하나 있어요. 에러는 ERROR 프레임으로 전송. 일반 RuntimeException → ApplicationErrorException. 비즈니스 에러는 명시적 응답 또는 메타데이터로.
시험 직전 한 번 더 — 자주 헷갈리는 함정 모음
여기까지가 2편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.
- 4 Interaction Models — Request-Response / Fire-and-Forget / Request-Stream / Channel
- 분류 — 입력 1/N, 출력 0/1/N
- Request-Response = HTTP 같음, Mono → Mono
- HTTP보다 가볍고 빠름 (단일 연결·바이너리)
- Fire-and-Forget = 응답 X, Mono → void
.send()사용 (retrieveMono아님)- FNF는 보장 X — 손실 가능
- 보장 = Request-Response 또는 Kafka
- Request-Stream = 1 요청 → N 응답, Mono → Flux
- 백프레셔 자연스러움
- HTTP SSE 대체
- 실시간 시세·로그·이벤트 구독
- Channel = N:N 양방향, Flux → Flux
- 가장 강력
- WebSocket의 강력한 대체 (메시지 의미 + 백프레셔)
- 채팅·협업·게임·IoT
REQUEST_N프레임이 백프레셔 자동 관리flatMap(concurrency)→ 자동으로 서버에 페이스 통보- 같은 라우트 = 한 모델만
- Mono 반환 = RR/FNF / Flux 반환 = Stream/Channel
- 에러 = ERROR 프레임 →
ApplicationErrorException - 같은 컨트롤러에 4 모델 모두 가능
시리즈 다른 편
- 1편 — 기본 개념·프레임
- 2편 — 4 Interaction Models (현재 글)
- 3편 — Spring RSocket 서버
- 4편 — Spring RSocket 클라이언트
- 5편 — 메타데이터·Composite Metadata
- 6편 — 보안·Spring Security RSocket·TLS
- 7편 — 로드 밸런싱·확장
- 8편 — 테스트
- 9편 — RSocket vs gRPC vs WebSocket
공식 문서: RSocket Interaction Models 에서 더 깊이.
다음 글(3편)에서는 Spring RSocket 서버 — @MessageMapping·@DestinationVariable·라우팅·예외 처리까지 풀어 갑니다.