Devit 플랫폼을 개발한지는 시간이 조금 지났지만, 마감 기한에 맞추기 바빠 채팅구현에 대한 포스팅을 진행하지 못한 점이 아쉬워
복습할 겸 다시 공부해 기록을 작성하려한다.
👍🏻 STOMP 란
Simple Text Oriented Messaging Protocol 의 약자로, 텍스트 기반 메시징 전송을 효율적으로 하기 위한 프로토콜이다.
특징으로는 Rabbit MQ 와 같이 구독 발행 시스템인 pub/sub 기반으로 작동한다.
메시지에 대한 송신 수신에 대해서 명확하게 구분하며, 별도의 핸들러를 구현할 필요 없이 @MessageMapping 어노테이션을 통해 메시징에 대한 엔드포인트 처리를 진행 할 수 있다.
위 그림은 기본적인 Message Queqe 를 이용한 STOMP 로직이다.
Message Broker 는 /topic 채널에 데이터가 도착하면 가로 채, 큐에 연결된 모든 어플리케이션에 해당 메시지를 전달하는 구조다.
반대로 /topic 채널을 구독하지 않는 어플리케이션은 해당 메시지가 도착했는지, 어떤 메시지인지 확인할 수 없다.
구독과 발행을 동시에 할 수 있다는 특징이 있다.
이러한 특징을 살려 채팅 서비스를 구현하였다.
👍🏻 Spring WebSocket STOMP 구현
WebSocketConfig.class
@Slf4j
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/api/chats/ws/chat")
.setAllowedOriginPatterns("*")
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/sub");
registry.setApplicationDestinationPrefixes("/pub");
//registry.enableStompBrokerRelay("") RabbitMQ 와 같은 외부 브로커 연결
}
}
STOMP 를 활용하기 위해 @EnableWebSocketMessageBroker 어노테이션을 선언하였다.
설정 파일에서, 웹소켓 서버의 엔드포인트와 구독, 발송 프로토콜을 설정할 수 있다.
해당 코드에서는 엔드 포인트를 "/api/chats/ws/chat" 으로 정의하였다. 또한 메시지 큐 구독 경로는 "/sub/rooms/{roomId}" 형태로 정의했으며, 메시지를 발송할 때 "/pub/message" 으로 메시지를 보내야한다.
해당 경로에 대해서는 밑에 코드에서 더 자세히 설명하겠다.
Message.class
@Entity
@Getter @Setter
public class Message {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "chat_room_id")
@JsonIgnore
private ChatRoom chatRoom;
private String senderName;
@Column(nullable = false, columnDefinition = "BINARY(16)")
private UUID senderId;
private String message;
private LocalDateTime createdAt;
/* 연관관계 편의 메소드 */
public void addMessage(ChatRoom chatRoom) {
this.chatRoom = chatRoom;
chatRoom.getMessages().add(this);
}
/* 생성 메서스 */
public static Optional<Message> createMessage(UUID senderId, String senderName, ChatRoom chatRoom, String message) {
Message sampleMessage = new Message();
sampleMessage.senderName = senderName;
sampleMessage.senderId = senderId;
sampleMessage.addMessage(chatRoom);
sampleMessage.message = message;
sampleMessage.setCreatedAt(LocalDateTime.now());
return Optional.ofNullable(sampleMessage);
}
}
메시지 큐에 발행되는 데이터이자, 채팅방을 접속할 시 이전 메시지들을 불러와야 하기 때문에 저장되는 엔티티이다.
발송자와 내용 생성 날짜 등 필드값이 존재한다. 채팅룸과는 다대일 관계를 맺고 있다.
MessageController.class
@RestController
@Slf4j
@RequiredArgsConstructor
public class MessageController {
private final MessageService messageService;
@MessageMapping(value = "/message")
public void message(SendMessageDto messageDto){
log.info("Message : {} 해당 메시지를 전달합니다.", messageDto);
messageService.sendMessage(messageDto);
}
}
해당 엔트포인트 외 API 가 존재하지만, 간결함을 위해 필요한 엔드포인트만 작성하였다.
앞서 설명할 때에 발송자가 메시지를 발송할 때 "/pub/message" 엔드포인트로 요청이 들어오면 MessageMapping 어노테이션으로 인해 message() 가 실행하게 된다.
MessageService.class
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class MessageService {
private final ChatRoomRepository chatRoomRepository;
private final MessageRepository messageRepository;
private final SimpMessageSendingOperations sendingOperations;
public String sendMessage(SendMessageDto messageDto) {
Optional<ChatRoom> chatRoom = chatRoomRepository.findByUUID(messageDto.getRoomId());
Optional<Message> message = Message.createMessage(messageDto.getUserId(), messageDto.getSenderName(), chatRoom.get(), messageDto.getMessage());
messageRepository.save(message.get());
log.info("메시지 시작");
try {
sendingOperations.convertAndSend("/sub/rooms/" + messageDto.getRoomId(), message);
return "메시지 전송 완료.";
} catch (Exception e) {
return "메시지 전송 실패 : " + e.getMessage();
}
}
}
위 메소드가 실행되며, id 에 해당하는 채팅방 엔티티를 가져온다.
그 후 미리 선언한 생성메소드를 통해 채팅방과 해당 메시지를 매핑한 후 저장한다.
sendingOperations.convertAndSend() 메소드를 통해 메시지큐로 구독하고 있는 클라이언트들에게 메시지 이벤트 처리가 가능해진다.
이전에 설정한 /sub/ 엔드포인트에 채팅방마다 독립적으로 구독해 메시지를 확인할 수 있게 rooms/{roomId} 를 더해 메시지를 발송한다.
/sub/rooms/{roomId} 를 구독하고 있는 클라이언트들은 모두 해당 메시지를 확인할 수 있게된다.
해당 로직을 개발했을 당시에는 Web socket 과 STOMP 메시지 프로토콜 모두 익숙지 않고 낯설어서 많이 헤매고 삽질했던 기억이 떠오른다. 특히 개발 초기에 작성한 코드라 예외처리가 정말 엉망이다 ,, 이전 코드를 보면서 1년 사이에 많이 성장했음을 다시 한 번 깨닫는다 😪
꾸준히 성장할 수 있게끔 현재 개발하고 있는 프로젝트도 꾸준히 기록을 남기려 한다.
쉽지 않지만 화이팅 👊🏻
'개인 공부 > Devit' 카테고리의 다른 글
[Devit] #2 유저 설계 ( Token, RabbitMQ ) (1) | 2022.06.30 |
---|---|
[Devit] #1 아키텍처 설계 (0) | 2022.06.29 |