WebSocket 트러블슈팅

1. WebSocket 컨트롤러에서 @PathVariable, @RequestBody 사용 시 값이 바인딩되지 않는 문제

문제

REST 컨트롤러에서 사용하던 @PathVariable, @RequestBody를 WebSocket 컨트롤러에서 그대로 사용하면 값이 정상적으로 바인딩되지 않음.

원인

@PathVariable, @RequestBody는 HTTP 요청 처리 전용 어노테이션. WebSocket STOMP는 HTTP와 다른 프로토콜이라 동작하지 않음.

HTTP WebSocket STOMP 역할
@PathVariable @DestinationVariable destination 경로의 변수 추출
@RequestBody @Payload 메시지 본문 역직렬화

해결

STOMP 전용 어노테이션으로 교체. @DestinationVariable/pub/chat/{roomId} 같은 STOMP destination 경로에서 변수를 추출하고, @Payload는 STOMP 프레임의 body를 Java 객체로 역직렬화. @Payload@Valid를 함께 사용하면 Bean Validation도 적용 가능.

@MessageMapping("/chat/{roomId}")
public void sendMessage(
        @DestinationVariable Long roomId,
        @Payload @Valid UserChatMessageRequest request,
        Principal principal
) { ... }

2. @AuthenticationPrincipal이 WebSocket 컨텍스트에서 동작하지 않는 문제

문제

REST 컨트롤러에서 사용하던 @AuthenticationPrincipal CustomUserDetails userDetails 방식을 WebSocket 컨트롤러에서 그대로 적용했으나 userId가 계속 null로 찍힘.

원인

@AuthenticationPrincipal은 Spring Security의 SecurityContextHolder에서 인증 정보를 꺼내오는데, WebSocket 메시지 처리는 HTTP 요청 처리 스레드와 다른 컨텍스트에서 실행되어 SecurityContextHolder에 인증 정보가 없음.

해결

Principal 타입으로 직접 주입받아 UsernamePasswordAuthenticationToken으로 캐스팅해 CustomUserDetails를 추출.

// 변경 전
@AuthenticationPrincipal CustomUserDetails userDetails

// 변경 후
Principal principal

if (!(principal instanceof UsernamePasswordAuthenticationToken token)) {
    return;
}
CustomUserDetails userDetails = (CustomUserDetails) token.getPrincipal();