Skip to content

Latest commit

 

History

History
193 lines (151 loc) · 13.5 KB

File metadata and controls

193 lines (151 loc) · 13.5 KB

05. 설정 DSL과 부트스트랩

무엇을 / 왜

앞선 장들의 부품(HandshakeHandler, SockJsService, SubProtocolWebSocketHandler, 채널, 브로커)을 손으로 조립하려면 빈을 수십 개 정의해야 한다. config.annotation 패키지는 이를 두 개의 애너테이션과 콜백 인터페이스로 압축한다.

  • @EnableWebSocket + WebSocketConfigurer — 저수준. 사용자 WebSocketHandler를 URL에 매핑하고 SockJS 폴백을 켜는 정도.
  • @EnableWebSocketMessageBroker + WebSocketMessageBrokerConfigurer — 풀스택. STOMP 엔드포인트, 메시지 브로커, @MessageMapping 디스패치까지 한 번에.

이 챕터는 두 설정 경로가 어떤 빈들을 어떻게 엮어 내는지를 본다. 핵심은 둘 다 등록 DSL → HandlerMapping 패턴을 따른다는 것이다.

핵심 타입

저수준 경로                                 풀스택(STOMP) 경로
─────────────────────────                  ─────────────────────────────────
@EnableWebSocket                           @EnableWebSocketMessageBroker
  @Import                                     @Import
    ▼                                           ▼
DelegatingWebSocketConfiguration           DelegatingWebSocketMessageBroker
  extends WebSocketConfiguration             Configuration
         Support                              extends WebSocketMessageBroker
    │                                                ConfigurationSupport
    │ 주입: List<WebSocketConfigurer>          extends AbstractMessageBroker
    │                                                Configuration (spring-messaging)
    ▼                                           │ 주입: List<WebSocketMessageBrokerConfigurer>
registerWebSocketHandlers(registry)              ▼
    │                                         registerStompEndpoints(registry)
    ▼                                         configureMessageBroker(registry)  ...
ServletWebSocketHandlerRegistry                   ▼
    │ getHandlerMapping()                     WebMvcStompEndpointRegistry
    ▼                                             │ getHandlerMapping()
@Bean webSocketHandlerMapping                     ▼
   (HandlerMapping)                           @Bean stompWebSocketHandlerMapping
                                                 + subProtocolWebSocketHandler 등 다수

@EnableWebSocket — 저수준 경로

@EnableWebSocket@Import(DelegatingWebSocketConfiguration.class) 한 줄이다. DelegatingWebSocketConfigurationWebSocketConfigurationSupport를 상속하고, 컨테이너의 모든 WebSocketConfigurer를 주입받아 registerWebSocketHandlers로 위임한다.

@Configuration(proxyBeanMethods = false)
public class DelegatingWebSocketConfiguration extends WebSocketConfigurationSupport {
    @Autowired(required = false)
    public void setConfigurers(List<WebSocketConfigurer> configurers) { ... }

    @Override
    protected void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        for (WebSocketConfigurer configurer : this.configurers)
            configurer.registerWebSocketHandlers(registry);
    }
}

WebSocketConfigurer는 콜백 하나뿐이다.

public interface WebSocketConfigurer {
    void registerWebSocketHandlers(WebSocketHandlerRegistry registry);
}

WebSocketConfigurationSupport — 빈을 만드는 본체

WebSocketConfigurationSupport는 실제 @Bean을 정의한다. 핵심은 webSocketHandlerMapping이다.

@Bean
public HandlerMapping webSocketHandlerMapping(DefaultSockJsSchedulerContainer schedulerContainer) {
    ServletWebSocketHandlerRegistry registry = initHandlerRegistry();   // 등록 DSL 채움
    if (registry.requiresTaskScheduler()) {
        registry.setTaskScheduler(schedulerContainer.getScheduler());   // SockJS용 스케줄러
    }
    return registry.getHandlerMapping();    // URL → WebSocketHttpRequestHandler 매핑 생성
}

흐름은 (1) ServletWebSocketHandlerRegistry를 만들고, (2) registerWebSocketHandlers로 사용자 설정을 받아 채운 뒤, (3) registry.getHandlerMapping()으로 02장의 WebSocketHandlerMapping을 빌드하는 것이다. 한 가지 영리한 점은 DefaultSockJsSchedulerContainer다 — SockJS를 쓰는 핸들러가 하나라도 등록됐을 때만(requiresTaskScheduler()) ThreadPoolTaskScheduler(스레드명 SockJS-, 풀 크기 = CPU 수)를 만들어 주입한다. SockJS heartbeat·세션 정리에 필요한 스케줄러(03장)를 사용자가 직접 제공하지 않아도 자동으로 채워 주는 것이다.

등록 DSL — Registry와 Registration

DSL은 두 계층이다. WebSocketHandlerRegistry(여러 핸들러 등록)와 WebSocketHandlerRegistration(한 핸들러의 옵션).

// 사용자 코드 예시
registry.addHandler(myHandler(), "/echo")          // WebSocketHandlerRegistration 반환
        .setAllowedOrigins("https://example.com")
        .addInterceptors(new HttpSessionHandshakeInterceptor())
        .withSockJS();                             // SockJsServiceRegistration 반환

AbstractWebSocketHandlerRegistration이 이 옵션들을 모으고, getMappings()에서 두 갈래로 매핑을 만든다.

protected final M getMappings() {
    M mappings = createMappings();
    if (this.sockJsServiceRegistration != null) {       // .withSockJS() 호출됨
        SockJsService sockJsService = ...getSockJsService();   // DefaultSockJsService
        // path → "/path/**" 패턴으로 SockJsHttpRequestHandler 매핑
        addSockJsServiceMapping(mappings, sockJsService, wsHandler, pathPattern);
    }
    else {                                              // 순수 WebSocket
        HandshakeHandler handshakeHandler = getOrCreateHandshakeHandler();  // DefaultHandshakeHandler
        HandshakeInterceptor[] interceptors = getInterceptors();           // +OriginHandshakeInterceptor
        addWebSocketHandlerMapping(mappings, wsHandler, handshakeHandler, interceptors, path);
    }
    return mappings;
}

withSockJS()를 부르면 SockJsServiceRegistration이 생기고, 매핑은 SockJS 경로 전체(/path/**)를 받는 SockJsHttpRequestHandler로 만들어진다(03장). 부르지 않으면 단일 경로를 02장의 WebSocketHttpRequestHandler로 매핑한다. 어느 경우든 getInterceptors()가 사용자 인터셉터 뒤에 OriginHandshakeInterceptor를 자동으로 덧붙여 Origin 보안 기본값을 보장한다. ServletWebSocketHandlerRegistry/ServletWebSocketHandlerRegistration이 Servlet 환경용 구체 구현이다.

@EnableWebSocketMessageBroker — 풀스택 경로

@EnableWebSocketMessageBroker@Import(DelegatingWebSocketMessageBrokerConfiguration.class)다. 이 설정은 WebSocketMessageBrokerConfigurationSupport(→ spring-messagingAbstractMessageBrokerConfiguration)를 상속해, 04장에서 본 STOMP 풀스택을 통째로 부트스트랩한다. WebSocketMessageBrokerConfigurer로 커스터마이즈한다(STOMP 엔드포인트, 브로커, 인바운드/아웃바운드 채널, 메시지 컨버터 등).

WebSocketMessageBrokerConfigurationSupport가 정의하는 핵심 빈들:

@Bean public WebSocketHandler subProtocolWebSocketHandler(inbound, outbound) {
    return new SubProtocolWebSocketHandler(inbound, outbound);   // 04장의 브리지
}

@Bean public HandlerMapping stompWebSocketHandlerMapping(subProtocolWebSocketHandler, scheduler, inbound) {
    WebSocketHandler handler = decorateWebSocketHandler(subProtocolWebSocketHandler);  // 데코레이터 팩토리 적용
    WebMvcStompEndpointRegistry registry = new WebMvcStompEndpointRegistry(handler, ...);
    registerStompEndpoints(registry);             // 사용자 콜백: registry.addEndpoint("/portfolio").withSockJS()
    return registry.getHandlerMapping();          // STOMP 엔드포인트 URL → 핸들러
}

@Bean public WebSocketMessageBrokerStats webSocketMessageBrokerStats(...) { ... }   // 운영 통계
  • subProtocolWebSocketHandler — 04장의 SubProtocolWebSocketHandler 빈. 채널 두 개를 받아 STOMP 핸들러를 단다(STOMP 핸들러 자체는 상위 AbstractMessageBrokerConfiguration 쪽 설정과 엮인다).
  • stompWebSocketHandlerMappingStompEndpointRegistry DSL로 받은 STOMP 엔드포인트(addEndpoint("/portfolio"))를 URL 매핑으로 변환. decorateWebSocketHandler가 등록된 WebSocketHandlerDecoratorFactory들을 차례로 적용해 핸들러를 감싼다 — 사용자 정의 데코레이터(예: 인증 후처리)를 STOMP 핸들러에 주입하는 확장점이다.
  • webSocketMessageBrokerStats — 04장의 세션 통계, 채널 실행기, SockJS 스케줄러 상태를 한데 모아 노출하는 운영용 빈.

StompEndpointRegistry / StompWebSocketEndpointRegistration은 STOMP 엔드포인트 등록 DSL이고, WebSocketTransportRegistration은 메시지 크기 한도·전송 시간 한도·WebSocketHandlerDecoratorFactory 등 전송 계층 옵션을 모은다.

Lifecycle phase 정렬

여러 인프라 빈(SubProtocolWebSocketHandler, WebSocketHandlerMapping, WebMvcStompEndpointRegistry가 만든 매핑 등)이 SmartLifecycle이며, @EnableWebSocketMessageBroker 구성에서는 phase를 0으로 맞춘다(setPhase(getPhase())). 시작/정지 순서를 일관되게 정렬해, 채널 구독·세션 종료가 올바른 타이밍에 일어나도록 보장한다.

WebSocketScope — "websocket" 스코프

WebSocketMessageBrokerConfigurationSupportwebSocketScopeConfigurer 빈으로 "websocket" 스코프(SimpSessionScope)를 등록한다(SCOPE_WEBSOCKET, 7.0+). 이 스코프의 빈은 WebSocket/STOMP 세션 동안만 살아 있어, 세션별 상태를 컨테이너 관리 빈으로 깔끔히 표현할 수 있다.

동작 흐름 — @EnableWebSocketMessageBroker 부트스트랩

@EnableWebSocketMessageBroker
   │ @Import
   ▼
DelegatingWebSocketMessageBrokerConfiguration
   │ 주입: List<WebSocketMessageBrokerConfigurer>
   │
   ├─ configureMessageBroker(MessageBrokerRegistry)     ← 브로커/목적지 프리픽스
   ├─ registerStompEndpoints(StompEndpointRegistry)     ← addEndpoint("/portfolio").withSockJS()
   ├─ configureClientInboundChannel / Outbound...        ← 채널 인터셉터/스레드풀
   └─ configureWebSocketTransport(...)                   ← 메시지 한도, 데코레이터 팩토리
   │
   ▼ @Bean 생성
 ┌────────────────────────────────────────────────────────────┐
 │ clientInboundChannel / clientOutboundChannel / brokerChannel │ (spring-messaging)
 │ SubProtocolWebSocketHandler  (STOMP 브리지)                   │
 │ StompSubProtocolHandler                                      │
 │ SimpleBroker 또는 StompBrokerRelay                            │
 │ WebSocketAnnotationMethodMessageHandler (@MessageMapping)    │
 │ stompWebSocketHandlerMapping (URL → 핸들러)                  │
 │ webSocketMessageBrokerStats / websocket 스코프                │
 └────────────────────────────────────────────────────────────┘
   │ 애플리케이션 기동(refresh) → SmartLifecycle.start (phase 0)
   ▼
 채널 구독 활성화, 엔드포인트 매핑 라이브 → 클라이언트 연결 수락

핵심 메서드 정리

  • WebSocketConfigurationSupport.webSocketHandlerMapping — 등록 DSL을 채워 WebSocketHandlerMapping 빈을 생성. SockJS 스케줄러를 조건부 자동 주입.
  • AbstractWebSocketHandlerRegistration.getMappingswithSockJS() 여부로 SockJS 매핑 vs 순수 WebSocket 매핑을 분기 생성, Origin 인터셉터 자동 추가.
  • WebSocketMessageBrokerConfigurationSupport.stompWebSocketHandlerMapping / subProtocolWebSocketHandler — STOMP 풀스택 빈 구성과 데코레이터 팩토리 적용.

설계 포인트 / 확장점

  • Configurer 콜백 패턴: @Enable*Delegating*Configuration이 컨테이너의 모든 *Configurer를 주입·위임. 여러 설정 클래스가 협력적으로 구성을 보탤 수 있다(spring-webmvcWebMvcConfigurer와 동일한 관용구).
  • DSL → HandlerMapping 빈: 사용자는 유창한 DSL로 의도만 적고, 복잡한 빈 그래프 조립은 *Support가 책임진다.
  • 확장 훅: WebSocketHandlerDecoratorFactory(핸들러 감싸기), setHandshakeHandler/addInterceptors(핸드셰이크 커스터마이즈), configureWebSocketTransport(전송 한도), "websocket" 스코프(세션 빈).
  • 합리적 기본값: Origin 인터셉터 자동 추가, SockJS 스케줄러 자동 생성, Lifecycle phase 정렬 — 설정을 최소화하면서도 안전·정합성을 확보.

정리

설정 패키지는 앞 장들의 부품을 두 진입점으로 묶는다. @EnableWebSocket은 사용자 WebSocketHandler를 URL에 매핑(SockJS 옵션 포함)하는 저수준 경로, @EnableWebSocketMessageBroker는 STOMP 엔드포인트·채널·브로커·@MessageMapping까지 엮는 풀스택 경로다. 둘 다 *Configurer 콜백으로 의도를 받아 *ConfigurationSupportHandlerMapping을 비롯한 빈 그래프로 구체화하며, Origin 보안·SockJS 스케줄러·Lifecycle 정렬 같은 기본값을 자동으로 채워 준다.