[BE-04] 실시간 전송 계층 (WebSocket/STOMP)
작업 내용 (설계 의도)
변경 사항
근거 TDD: 20260704-채팅시스템고도화-tdd.md (FR-6, 방안 비교 Simple Broker·AFTER_COMMIT 브로드캐스트, STOMP 계약).
실시간 송수신 계층을 신설한다. in-memory Simple Broker, JWT handshake, AFTER_COMMIT 브로드캐스트.
build.gradle.kts:spring-boot-starter-websocket추가.infrastructure/realtime/WebSocketConfig.kt(신규 파일):@EnableWebSocketMessageBroker./wsendpoint 등록,enableSimpleBroker("/topic","/queue"),setApplicationDestinationPrefixes("/app").clientInboundChannelcore8/max16,clientOutboundChannelcore8/max16 명시(TDD 용량 산정).chat.realtime.enabled플래그로 조건부 등록.infrastructure/realtime/StompAuthChannelInterceptor.kt: CONNECT 시Authorization: Bearer파싱을JwtIssuer(=JwtTokenProvider)로 검증, Principal=userId 세팅. 미인증 CONNECT 거부.domain/message/gateway/MessageBroadcastGateway.kt(신규 interface):broadcast(roomId, message),broadcastTyping(roomId, event),broadcastRead(roomId, event). payload는 data class(BroadcastMessage/TypingEvent/ReadEvent) — raw String/Map 금지.infrastructure/realtime/MessageBroadcastGatewayImpl.kt:SimpMessagingTemplate로/topic/rooms/{id}[/typing|/read]발행.presentation/message/stomp/ChatStompController.kt:@MessageMapping("/rooms/{roomId}/send")→SendMessageUseCase,.../typing→ 타이핑 브로드캐스트 UseCase. Consumer/EventListener는 presentation 규칙 준수.domain/message/event/MessageSentEvent.kt(신규, AbstractDomainEvent, topic=null):MessageDomainService.sendMessage가 저장 후DomainEventPublisher.publish. → MessageDomainService.kt에 DomainEventPublisher 주입 + publish 1줄 추가(본 티켓 단독 수정).presentation/message/worker/MessageBroadcastEventWorker.kt:@TransactionalEventListener(AFTER_COMMIT)로MessageSentEvent수신 →MessageBroadcastGateway.broadcast.SecurityConfig.kt:/ws/**handshake 경로 permitAll(인증은 STOMP interceptor가 수행) +/communities/**authenticated()규칙 등록(컨트롤러는 BE-08이 wave3에 추가, 규칙 선등록은 무해). — 본 티켓만 wave2에서 SecurityConfig 수정./rooms/**인증 승격 + 기존 컨트롤러 JWT 전환은 BE-12(wave3)가 담당(다른 wave라 충돌 없음).- 인증 통일: STOMP·신규 엔드포인트 모두
Authorization: Bearer <JWT>기준. WebSocket은StompAuthChannelInterceptor가JwtIssuer로 검증해 Principal=userId 세팅(X-User-Id 미사용). - 롤백:
chat.realtime.enabled=false로 WebSocket 미등록, REST 발화(POST경로) 정상 동작.
의존
- BE-02 (없어도 무방하나 상수 참조 대비)
- BE-03a/BE-03b는 발화 가드(validateCanSpeak/NotExpired)를 위해 참조 — 발화 검증은 BE-05/06 이후 강화, 본 티켓은 전송 파이프라인 확립
다이어그램
처리 흐름
sequenceDiagram participant C as Client participant I as StompAuthInterceptor participant Ctl as ChatStompController participant UC as SendMessageUseCase participant Pub as EventPublisher participant W as BroadcastEventWorker participant G as BroadcastGateway C->>I: CONNECT Authorization Bearer I->>I: JWT 검증, Principal=userId C->>Ctl: SEND /app/rooms/1/send Ctl->>UC: execute UC-->>Pub: MessageSentEvent Pub->>W: AFTER_COMMIT W->>G: broadcast(1, payload) G-->>C: /topic/rooms/1
테스트 케이스
- 유효 JWT로 CONNECT하면 세션이 수립되고 Principal=userId가 설정된다.
- Authorization 헤더 없는 CONNECT는 거부된다.
/app/rooms/{id}/send발화 시 커밋 후/topic/rooms/{id}구독자가 메시지를 수신한다.- 트랜잭션 롤백 시(저장 실패) 브로드캐스트가 발생하지 않는다 (AFTER_COMMIT).
chat.realtime.enabled=false면/wsendpoint가 등록되지 않고 REST 경로만 동작한다.- 타이핑 SEND는
/topic/rooms/{id}/typing구독자에게 전달되고 DB에 저장되지 않는다.