Springboot + jwt + web socket(STOMP) 채팅 구현
스프링부트로 채팅을 구현하기 위해 STOMP 프로토콜을 사용해보기로 했다.
구현을 위해 공부하는 과정에서 헷갈렸던 부분이 있어 적어둔다.
STOMP(Simple Text Oriented Messaging Protocol)란 http와 호환되는 양방향 통신을 제공하기 위한 프로토콜이다.
@RequiredArgsConstructor
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final StompHandler stompHandler;
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/sub");
// 메세지 구독 요청 -> sub/chatroom/1
config.setApplicationDestinationPrefixes("/pub");
// 메세지 발행 요청 -> /pub/chat/message
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-stomp").setAllowedOrigins("*");
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(stompHandler);
}
}
보다시피 STOMP프로토콜을 사용하는 웹소켓 통신에서 클라이언트가 메시지를 발행하고, 구독하는 경로가 다르다.
여기서 내가 헷갈렸던 부분은 클라이언트가 서버로 메시지를 보낼 때 사용하는 '/pub'경로와
클라이언트가 메시지를 받을 때 서버가 클라이언트에게 보내는 '/sub' 경로이다.
예를 들어 /pub/chat/message 경로를 사용해서 클라이언트가 서버로 메시지를 보내면,
sub/chatroom/{chatRoomId} 경로를 통해서 서버로부터 특정 채팅방의 메시지를 받는다.
@RequiredArgsConstructor
@RestController
@RequestMapping("/chat")
public class ChatController {
private final ChatService chatService;
//컨트롤러 메소드에서 Authentication 객체를 매개변수로 받을때, Spring Security는 SecurityContextHolder에서 이 객체를 자동으로 주입해줌
@PostMapping("/message")
public ResponseEntity<?> sendMessage(@RequestBody MessageDto messageDTO, Authentication authentication) {
String email = authentication.getName(); //userDetails를 기준으로 정보를 가져온다!
chatService.sendMessage(messageDTO, email);
return ResponseEntity.ok().build();
}
}
@RequiredArgsConstructor
@Service
@Transactional
public class ChatService {
private final MessageRepository messageRepository;
private final ChatRoomRepository chatRoomRepository;
private final MemberRepository memberRepository;
private final SimpMessagingTemplate messagingTemplate;
public void sendMessage(MessageDto messageDto, String email) {
Member sender = memberRepository.findByEmail(email)
.orElseThrow(() -> new NotFoundMemberException("사용자를 찾을 수 없습니다."));
ChatRoom chatRoom = chatRoomRepository.findById(messageDto.getChatRoomId())
.orElseThrow(() -> new NotFoundChatRoomException("채팅방을 찾을 수 없습니다."));
Message message = new Message();
message.setSender(sender);
message.setChatRoom(chatRoom);
message.setContent(messageDto.getContent());
message.setCreatedTime(LocalDateTime.now());
messageRepository.save(message);
messagingTemplate.convertAndSend("/sub/chatroom/" + chatRoom.getId(), message);
}
}
위 코드에서 ChatService의 sendMessage 메소드에서 messagingTemplate.convertAndSend를 사용하는 부분은 클라이언트가 아니라 서버 측에서 발생한다.
즉, 클라이언트가 /pub/chat/message를 통해 메시지를 서버에 보내면, 서버는 그 메시지를 받아 처리하고, 메시지가 속한 채팅방의 모든 구독자에게 /sub/chatroom/{chatRoomId} 경로를 통해 메시지를 전송하는 것이다.
정리하면 아래와 같다!!
(프론트엔드)
1. 채팅방에 참여할 때 /sub/chatroom/{chatRoomId}로 구독을 시작
2. 메시지를 보낼 때: /pub/chat/message로 메시지를 발행
(백엔드)
1. /pub/chat/message 경로로 들어온 메시지를 받아 처리
2. 해당 채팅방을 구독하는 모든 클라이언트에게 /sub/chatroom/{chatRoomId} 경로로 메시지를 전송