이전 게시물에서는 웹 소켓을 이용한 채팅 서버를 구현해 보았다. 그러나 웹 소켓만으로는 채팅서버에 문제가 있습니다.
@Component
public class Sessions {
private final Map<Long, Set<WebSocketSession>> sessionsByRoomId = new ConcurrentHashMap<>();
public Set<WebSocketSession> getSessionsByRoomId(Long roomId) {
return sessionsByRoomId.computeIfAbsent(roomId, k -> new HashSet<>());
}
public void removeSession(WebSocketSession session) {
sessionsByRoomId.values().forEach(sessions -> sessions.remove(session));
}
}
다음과 같이 채팅방 식별자(아이디)와 Set<WebSocketSession>
을 Map 자료구조를 통해 구현했습니다.
웹 소켓 서버가 2대 이상이라면, 메모리 기반으로 관리하는 세션 정보를 서로 알아야 합니다.
→ 즉, 다른 서버에서 생성한 세션 정보를 서로 공유해야 하는 문제점이 있습니다.
메시지 타겟을 no01로 설정해서 메시지를 보냈는데, 서버에서는 발행자의 메시지를 확인한 후 no01채널을 구독하는 모든 사용자(클라이언트)에게 메시지를 보내게 된다.
구독과 발행을 동시에 하는 대표적인 예시는 채팅 기능이다.(양방향 통신)
@Configuration
@EnableWebSocketMessageBroker
public class StompWepSocketConfig implements WebSocketMessageBrokerConfigurer {
//웹소켓 핸드셰이크 커넥션을 생성할 경로
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/stomp/chat").setAllowedOrigins("*");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/pub"); // @MessageMapping 메서드로 라우팅된다.
registry.enableSimpleBroker("/sub"); // SimpleBroker는 해당하는 경로를 SUBSCRIBE하는 Client에게 메세지를 전달하는 간단한 작업을 수행
}
}
registry.enableSimpleBroker("/sub")
설정은 이러한 내부적인 메시지 브로커를 활성화하는 것이며, 주로 작은 규모의 애플리케이션에서 사용됩니다.@Controller
@RequiredArgsConstructor
public class MessageController {
private final MessageService messageService;
// 채팅방 입장
@MessageMapping("/chat/enter/{roomId}")
@SendTo("/sub/chat/room/{roomId}")
public MessageInfo enterUser(@DestinationVariable("roomId") Long roomId, @Payload MessageCreateRequest message){
message.setMessage(message.getSender() + "님이 채팅방에 입장하였습니다.");
return messageService.saveMessage(message);
}
// 채팅방 대화
@MessageMapping("/chat/talk/{roomId}")
@SendTo("/sub/chat/room/{roomId}")
public MessageInfo talkUser(@DestinationVariable("roomId") Long roomId, @Payload MessageCreateRequest message){
return messageService.saveMessage(message);
}
// 채팅방 퇴장
@MessageMapping("/chat/exit/{roomId}")
@SendTo("/sub/chat/room/{roomId}")
public MessageInfo exitUser(@DestinationVariable("roomId") Long roomId, @Payload MessageCreateRequest message){
message.setMessage(message.getSender() + "님이 채팅방에 퇴장하였습니다.");
return messageService.saveMessage(message);
}
}
@Service
@RequiredArgsConstructor
public class MessageService {
private final RoomService roomService;
private final MessageRepository messageRepository;
private final MessageMapper messageMapper;
// 메시지 저장
public MessageInfo saveMessage(MessageCreateRequest request){
Room currentRoom = roomService.findOneRoom(request.getRoomId());
Message chatMessage = Message.builder()
.type(Type.valueOf(request.getType()))
.message(request.getMessage())
.sender(request.getSender())
.room(currentRoom)
.build();
messageRepository.save(chatMessage);
return messageMapper.mapEntityToInfo(chatMessage);
}
}
< + 나의 생각>
Web socket으로 채팅 서비스를 구현했을 때는 WebsocketSession정보를 서버 측에서 Map 자료구조를 통해 계속 가지고 있어야 했습니다. 만약 서버가 1대가 아닌 여러대인 경우에는 이 세션 정보를 다른 서버에서도 공유해야하며 데이터의 일관성을 맞춰줘야 합니다. 그러나 STOMP를 사용하였을 경우 메시지 브로커에 의해 알아서 메시지를 전달해주어 별도의 세션관리가 필요 없어졌습니다. 웹 소켓 사용시 MemberService에서 웹소켓 핸들러를 처리하는 로직이 가독성 있다고 느껴지지 않았는데 STOMP를 적용하니 코드의 가독성과 유지보수가 좋아졌다고 느껴졌습니다.
이후 Rabbitmq 혹은 Kafka를 이용하여 고성능 메시지 브로커를 접목시켜봐야겠습니다.
출처 :
[Spring Boot] WebSocket과 채팅 (3) - STOMP
Spring Websocket & STOMP