Spring RSocket 마스터 — 서버·@MessageMapping·라우팅

2026-05-03확률과 통계 마스터 노트

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-Response
  • Mono<Void> = Fire-and-Forget (또는 RR)
  • Flux<X> (입력 단일) = Request-Stream
  • Flux<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·Actuator rsocket-mappings

시리즈 다른 편

공식 문서: Spring RSocket Server 에서 더 깊이.

다음 글(4편)에서는 Spring RSocket 클라이언트 — RSocketRequester·연결 관리·재시도·타임아웃까지 풀어 갑니다.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!