Spring RSocket 마스터 노트 시리즈 3편. @Controller + @MessageMapping으로 RSocket 서버 만들기, 라우팅 패턴 매칭과 @DestinationVariable, 4 Interaction Models별 메서드 시그니처, @MessageExceptionHandler로 예외 처리, ConnectionSetupPayload 인증 정보 추출, 서버 설정 옵션(transport·port·SSL)까지.
이 글은 Spring RSocket 마스터 노트 시리즈의 세 번째 편입니다. 2편(Models)까지 개념이었다면, 이번엔 그것을 Spring으로 어떻게 만드나 — 서버 측.
@Controller + @MessageMapping 한 줄로 RSocket 서버 끝. Spring MVC와 비슷한 친화. 다만 라우팅·예외·SETUP 처리가 미세하게 다릅니다.
처음 RSocket 서버가 어렵게 느껴지는 이유
처음 이 단원이 어렵게 느껴지는 이유는 두 가지예요. 첫째, Spring MVC와 비슷해 보이지만 다른 부분이 헷갈립니다. @RequestMapping이 아닌 @MessageMapping. 둘째, 예외 처리·SETUP·인증 같은 RSocket 고유 영역이 막연합니다.
해결법은 한 가지예요. "@MessageMapping = Spring MVC의 @RequestMapping" 한 줄. 차이점은 RSocket 고유 영역(SETUP·메타데이터·연결 인증)만 추가. 이 인식이 시작.
의존성·설정
implementation 'org.springframework.boot:spring-boot-starter-rsocket'
spring:
rsocket:
server:
port: 7000
transport: tcp # tcp / websocket
mapping-path: /rsocket # WebSocket 시 경로
ssl:
enabled: false
Spring Boot 자동 시작.
가장 단순한 컨트롤러
@Controller
public class GreetingController {
@MessageMapping("greet")
public Mono<String> greet(String name) {
return Mono.just("Hello, " + name + "!");
}
}
@Controller + @MessageMapping. Spring MVC와 거의 같은 모양.
4 Interaction Models별 시그니처
@Controller
public class MyController {
// Request-Response
@MessageMapping("rr")
public Mono<Result> rr(Request req) { ... }
// Fire-and-Forget
@MessageMapping("fnf")
public Mono<Void> fnf(Event e) { ... }
// Request-Stream
@MessageMapping("stream")
public Flux<Event> stream(Filter f) { ... }
// Channel
@MessageMapping("channel")
public Flux<Out> channel(Flux<In> input) { ... }
}
Spring이 반환 타입으로 모델 자동 추론:
Mono<X>= Request-ResponseMono<Void>= Fire-and-Forget (또는 RR)Flux<X>(입력 단일) = Request-StreamFlux<X>(입력 Flux) = Channel
여기서 정말 중요한 시험 함정 — 모델은 반환 타입이 결정. 명시 X. 잘못된 시그니처 = 클라이언트 모델과 불일치.
라우팅 패턴
1. 정적 라우트
@MessageMapping("user.list")
public Flux<User> list() { ... }
2. 패턴 매칭
@MessageMapping("user.{id}")
public Mono<User> get(@DestinationVariable String id) { ... }
@MessageMapping("user.{id}.posts.{postId}")
public Mono<Post> getPost(
@DestinationVariable String id,
@DestinationVariable String postId
) { ... }
3. 와일드카드
@MessageMapping("admin.**") # 모든 admin.* 라우트
@MessageMapping("logs.{level:[A-Z]+}") # 정규식
여기서 시험 함정이 하나 있어요. .이 라우팅 구분자. Spring MVC의 /와 달리 . 사용. user.list·user.123 형태가 RSocket 표준.
클래스 레벨 prefix
@Controller
@MessageMapping("user") // prefix
public class UserController {
@MessageMapping("list") // user.list
public Flux<User> list() { ... }
@MessageMapping("{id}") // user.{id}
public Mono<User> get(@DestinationVariable String id) { ... }
}
인자 어노테이션
@MessageMapping("greet.{lang}")
public Mono<String> greet(
String body, // 요청 페이로드
@DestinationVariable String lang, // 라우트 변수
@Header("custom-header") String header, // 메타데이터
@Headers Map<String, Object> all, // 모든 메타데이터
RSocketRequester requester // 양방향 — 클라이언트로 호출 가능
) { ... }
| 어노테이션 | 의미 |
|---|---|
| (없음) | 페이로드 |
@DestinationVariable |
라우트 변수 |
@Header |
메타데이터 단일 |
@Headers |
모든 메타데이터 |
@Payload |
명시적 페이로드 (옵션) |
RSocketRequester |
클라이언트 측 호출 (양방향) |
양방향 호출 — RSocketRequester 인자
서버가 클라이언트로 다시 호출:
@MessageMapping("login")
public Mono<Void> login(LoginRequest req, RSocketRequester clientReq) {
return validate(req)
.doOnSuccess(user -> {
// 클라이언트 라우트로 호출
clientReq.route("notification")
.data("Welcome, " + user.getName())
.send()
.subscribe();
})
.then();
}
Channel 모델 외에도 양방향 가능.
여기서 정말 중요한 시험 함정 — 클라이언트도 RSocket 라우트를 노출 가능. @MessageMapping을 클라이언트 측에도 정의하면 서버가 호출 가능. 양방향 RPC.
예외 처리
@MessageExceptionHandler — 컨트롤러별
@Controller
public class MyController {
@MessageMapping("user.{id}")
public Mono<User> get(@DestinationVariable String id) {
return userRepo.findById(id)
.switchIfEmpty(Mono.error(new UserNotFoundException(id)));
}
@MessageExceptionHandler(UserNotFoundException.class)
public Mono<ErrorResponse> handle(UserNotFoundException e) {
return Mono.just(new ErrorResponse(404, e.getMessage()));
}
}
@ControllerAdvice — 전역
@ControllerAdvice
public class GlobalRSocketExceptionHandler {
@MessageExceptionHandler(BusinessException.class)
public Mono<ErrorResponse> handle(BusinessException e) {
return Mono.just(new ErrorResponse(400, e.getMessage()));
}
@MessageExceptionHandler(Exception.class)
public Mono<ErrorResponse> handleGeneral(Exception e) {
log.error("Server error", e);
return Mono.just(new ErrorResponse(500, "Internal error"));
}
}
여기서 시험 함정이 하나 있어요. 에러 반환은 정상 응답으로 처리. 클라이언트는 응답 받음. 진짜 에러(연결 끊김)는 ERROR 프레임 자동.
ConnectionSetupPayload — 인증
SETUP 프레임의 페이로드를 받음:
@ConnectMapping
public Mono<Void> onConnect(
RSocketRequester requester,
@Payload AuthPayload auth
) {
if (!authService.verify(auth)) {
return Mono.error(new AuthenticationException());
}
// 연결 등록
sessionManager.register(requester, auth);
return Mono.empty();
}
@ConnectMapping = 연결 시작 시 호출. 인증·세션 등록.
여기서 정말 중요한 시험 함정 — SETUP 시 인증이 표준. 매 메시지마다 인증 X (성능). 한 번 SETUP에서 토큰 검증 후 연결 유지.
서버 설정 옵션
spring:
rsocket:
server:
port: 7000
transport: tcp
ssl:
enabled: true
key-store: classpath:keystore.p12
key-store-password: ${KS_PASS}
fragment-size: 16KB # 큰 페이로드 분할
Java Config
@Bean
public RSocketServerCustomizer rSocketServerCustomizer() {
return server -> server
.interceptors(registry -> {
registry.forSocketAcceptor(new MyAcceptorInterceptor());
registry.forResponder(new MyResponderInterceptor());
});
}
RSocketStrategies — 직렬화
@Bean
public RSocketStrategies rSocketStrategies() {
return RSocketStrategies.builder()
.encoders(encoders -> {
encoders.add(new Jackson2JsonEncoder());
encoders.add(new ProtobufEncoder());
})
.decoders(decoders -> {
decoders.add(new Jackson2JsonDecoder());
decoders.add(new ProtobufDecoder());
})
.build();
}
JSON / Protobuf / CBOR 등 다양한 인코딩 지원.
메타데이터 라우팅
@MessageMapping("user.create")
public Mono<User> create(
User user,
@Header("trace-id") String traceId
) {
log.info("Trace: {}", traceId);
return userService.create(user);
}
메타데이터로 라우팅·인증·trace ID·기타 정보 전달. 5편에서 자세히.
TCP vs WebSocket Transport
TCP — 일반
spring.rsocket.server.transport: tcp
- 가장 빠름
- 마이크로서비스 사이
- 방화벽 친화 (단일 포트)
WebSocket — 브라우저
spring.rsocket.server.transport: websocket
spring.rsocket.server.mapping-path: /rsocket
- 브라우저 환경
- HTTP/HTTPS 포트 재사용
- 일반 웹서버와 함께
여기서 시험 함정이 하나 있어요. WebSocket 모드 = 브라우저용. Spring Boot WebFlux 함께 사용. TCP는 마이크로서비스 사이만.
디버깅
logging:
level:
io.rsocket: DEBUG
org.springframework.messaging.rsocket: DEBUG
Actuator
management:
endpoints:
web:
exposure:
include: rsocket-mappings,metrics
# /actuator/rsocket-mappings — 등록된 라우트 목록
시험 직전 한 번 더 — 자주 헷갈리는 함정 모음
여기까지가 3편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.
spring-boot-starter-rsocket의존성- 설정 —
spring.rsocket.server.port·transport·ssl - 컨트롤러 —
@Controller+@MessageMapping - 반환 타입이 모델 결정 (Mono / Flux / Mono<Void>)
- 4 모델별 시그니처 자동 추론
- 라우팅 — 정적 / 패턴 매칭 (
{id}) / 와일드카드 (**·정규식) .이 라우팅 구분자 (/아님)- 클래스 레벨 prefix
- 인자 어노테이션 —
@DestinationVariable·@Header·@Headers·@Payload·RSocketRequester - 양방향 호출 =
RSocketRequester인자로 클라이언트 라우트 호출 - 클라이언트도
@MessageMapping노출 가능 - 예외 —
@MessageExceptionHandler(컨트롤러별) @ControllerAdvice(전역)- 에러 응답 = 정상 응답으로 처리 (정상 ack)
- 진짜 에러 = ERROR 프레임 자동
@ConnectMapping= SETUP 프레임 처리- 인증은 SETUP 시 한 번 (매 메시지 X)
- ConnectionSetupPayload로 토큰 추출
- RSocketStrategies = 인코더·디코더 (JSON·Protobuf·CBOR)
- 메타데이터로 라우팅·trace 전달
- TCP (마이크로서비스) vs WebSocket (브라우저)
- WebSocket = HTTP 포트 재사용
- 디버깅 —
io.rsocket: DEBUG·Actuatorrsocket-mappings
시리즈 다른 편
- 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
공식 문서: Spring RSocket Server 에서 더 깊이.
다음 글(4편)에서는 Spring RSocket 클라이언트 — RSocketRequester·연결 관리·재시도·타임아웃까지 풀어 갑니다.