스프링부트로 채팅을 구현하기 위해 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} 경로로 메시지를 전송