[Springboot, Vue.js] STOMP를 사용한 SpringBoot RestAPI 채팅 - 2

2 분 소요

업데이트:

stomp를 활용한 채팅 하기 - springboot 코드 뜯어보기

Springboot에서 stomp를 사용하기 위한 준비부터 클라이언트와 프레임을 주고 받는 과정을 정리해보자
(채팅방, 메세지 객체 등 다른 부가적인 api구현보다는 STOMP 중점)

0. 개발 환경

springboot 2.3.3 , java 1.8 (STS)
vue CLI 4.5.3 (VS code)
mySQL 8.0

1. WebSocketConfig.java

WebSocketConfig.java파일은 stomp의 엔드포인트, 그리고 메세지 프레임의 라우팅 방향을 지정한다

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		registry.addEndpoint("/ws").setAllowedOrigins("*").withSockJS();
	}

	@Override
	public void configureMessageBroker(MessageBrokerRegistry registry) {
		registry.setApplicationDestinationPrefixes("/pub"); // publish 
		registry.enableSimpleBroker("/sub"); // subscribe
	}
}
  • @EnableWebSocketMessageBroker
    WebSocket을 통해 브로커 메시징을 활성하기 위해 사용한다
  • implements WebSocketMessageBrokerConfigurer
    WebSocket 클라이언트로 받은 메세지 핸들링을 구성하는 메소드들이 정의할때 사용한다
  • registerStompEndpoints
    클라이언트가 웹 소켓 서버에 연결하는 데 사용할 웹 소켓 엔드 포인트를 등록한다
    엔드 포인트 구성에 withSockJS ()를 사용한다
    SockJS는 웹 소켓을 지원하지 않는 브라우저에 폴백 옵션을 활성화하는 데 사용된다

  • /ws
    /ws는 WebSocket 클라이언트가 Handshake를 위해 연결해야하는 end-point의 url이다
    프론트에서 websocket에 접속할 때 sockJS를 사용한다면 다음과 같이 연결하게 된다
    let socket = new SockJS('http://localhost:8080/ws')
    this.stompClient = Stomp.over(socket)
    
  • configureMessageBroker
    메세지 브로커 옵션을 설정한다
  • /pub
    /pub는 destination이 /pub으로 된 메세지를 @Controller@MessageMapping으로 라우팅한다
  • /sub
    /sub는 /sub로 시작하는 목적지를 가진 메세지가 메세지브로커로 라우팅한다

2. MessageController.java

MessageController.java는 전달받은 메세지를 처리하는 컨트롤러다

@RequiredArgsConstructor
@RestController
public class MessageController {
	private final IMessageService messageService;
	private final SimpMessagingTemplate template;

	@MessageMapping("/message")
	public void sendMessage(@Payload Message chatMessage) {
		messageService.insertMessage(chatMessage); //db 저장
		template.convertAndSend("/sub/" + chatMessage.getChatroomId(), chatMessage);
	}
}

전달받은 메세지 프레임의 데이터(payload)는 내가 미리 선언해둔 Message객체로 받아온다
받은 메세지의 형식이 다음과 같다고 할때, /sub/14를 구독한 클라이언트에게 전달된다

chatroomId : 14
content : Hi~
senderId : 2

정리

  1. 클라이언트가 http://localhost:8080/ws에 연결되고 WebSocket 연결이 설정되고나면 STOMP 프레임이 공급된다
  2. 클라이언트가 /sub/message로 SUBSCRIBE 프레임을 보낸다
    전달 및 디코드된 메세지는 clientInboundChanner로 전송되고 클라이언트의 구독을 보관하는 메세지 브로커로 라우팅된다
  3. 클라이언트는 SEND 프레임을 “/pub/message”로 보낸다 “/pub”접두사는 MessageController로, 접두사가 제거 된 후 남은 “/message”는 @MessageMapping에 맞춰 라우팅된다
  4. MessageController에서 리턴된 값은 데이터(payload)와 Spring Message로 변환한다
    (이때 데이터(payload)는 /sub/message의 목적지(destination)과 리턴 값에 기반한다)
    결과 메세지는 brokerChanner에 보내지고 메세지브로커가 처리한다
  5. 메시지 브로커는 목적지(destination)과 구독주소가 일치하는 모든 구독자를 찾고, 메시지는 STOMP 프레임으로 인코딩되어 WebSocket 연결로 전송하는 clientOutboundChannel을 통해 전송한다

+ Interception

ChannelInterceptor등록을 통해 모든 메세지를 직접 가로챌 수 있다
WebSocketConfig.java에서 하단에 메서드를 추가로 작성한다
(spring5.0.4 docs에서 setInterceptors로 나와있는데 사용되지않는다고 나와서 대체)

	@Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new MyChannelInterceptor());
    }

MyChannelInterceptor클래스 파일을 따로 작성하지않고 사용할 수 있지만 따로 분리했다

public class MyChannelInterceptor implements ChannelInterceptor{

	@Override
	public Message preSend(Message<?> message, MessageChannel channel) {
		StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
		StompCommand command = accessor.getCommand();
		if (command.compareTo(StompCommand.SUBSCRIBE) == 0) {
			String destination = accessor.getDestination();
			System.out.println("구독 주소 : " + destination);
			System.out.println(message);
		} else if (command.compareTo(StompCommand.CONNECT) == 0) {
			System.out.println("사용자 연결");
		} else if (command.compareTo(StompCommand.DISCONNECT) == 0) {
			System.out.println("사용자 연결 해제");
		}
		return message;
	}
}

(이또한 extends ChannelInterceptorAdapter에서 implements ChannelInterceptor로 변경)
HeaderAccessor나 SimpMessageHeaderAccessor로 메세지 정보에 접근할 수 있다

내 프로젝트에서는 필요하다고 생각하지않아서 출력문만 확인하는 용으로 두었다
이때 command는 어떤 메세지 프레임인지 enum형태로 정의되어있다
그래서 같은지 확인하기 위해 compareTo()를 사용했다

다른예제들을 보면 Interceptor하는 방법보단,
클라이언트에서 메세지를 송신할때 “type”같은 값을 넣어줘서 메세지 컨트롤러에서 주로 처리를 했다

사실 인터셉터에서 어떻게 뭘 처리할 수 있는지, 무엇에 쓰이는 용도인지는 잘 모르겠다
preSend외에 postSend, afterSendCompletion, preReceive등 다른 메소드들도 있다

전체 소스 : Github Repository

참고

vue spring boot 웹소켓 채팅 만들기
Spring WebSocket 소개
[Spring]Springboot + websocket 채팅[1]

댓글남기기