웹 소켓 -> STOMP로 변경한 채팅 서버

박준수·2023년 7월 3일
0

[토이프로젝트]

목록 보기
2/5
post-thumbnail

이전 게시물에서는 웹 소켓을 이용한 채팅 서버를 구현해 보았다. 그러나 웹 소켓만으로는 채팅서버에 문제가 있습니다.

웹 소켓에 대한 문제점

@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대 이상이라면, 메모리 기반으로 관리하는 세션 정보를 서로 알아야 합니다.
→ 즉, 다른 서버에서 생성한 세션 정보를 서로 공유해야 하는 문제점이 있습니다.

STOP란

  • 메세징 전송을 효율적으로 하기 위한 프로토콜로 pub/sub 기반으로 동작한다.
  • 메시지를 송신, 수신에 대한 처리가 명확하게 정의할 수 있다.
  • WebsocketHandler를 직접 구현할 필요 없이, @MessageMapping 같은 어노테이션을 사용해서, 메시지 발행 시 엔드포인트를 별도로 분리해서 관리할 수 있다.

PUB/SUB(발행/구독)

  • pub / sub란 메세지를 공급하는 주체와 소비하는 주체를 분리해 제공하는 메세징 방법이다.
  • 메시지 타겟을 no01로 설정해서 메시지를 보냈는데, 서버에서는 발행자의 메시지를 확인한 후 no01채널을 구독하는 모든 사용자(클라이언트)에게 메시지를 보내게 된다.

  • 구독과 발행을 동시에 하는 대표적인 예시는 채팅 기능이다.(양방향 통신)

채팅방에서의 pub/sub 컨셉

  • 채팅방 생성 : pub / sub 구현을 위한 Topic이 생성됨
  • 채팅방 입장 : Topic 구독
  • 채팅방에서 메시지를 송수신 : 해당 Topic으로 메세지를 송신(pub), 메세지를 수신(sub)

Web Socket → STOMP 변경

@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에게 메세지를 전달하는 간단한 작업을 수행
    }
}
  • 웹 소켓에서 STOMP 프로토콜로 Config를 변경해 주었습니다.
  • registerStompEndpoints를 오버라이드하여 웹 소켓 핸드셰이크 커넥션을 생성할 경로를 지정해줍니다.
  • configureMessageBroker에서 setApplicationDestinationPrefixes(”/pub”) 메서드는 공급자, enableSimpleBroker(”/sub”) 메서드는 해당하는 경로를 구독하는 클라이언트에게 메세지를 전달하는 역할을 해줍니다.
    • 스프링에서는 간단한 메시지 전달 요구사항을 충족하기 위해 내부적으로 간단한 메시지 브로커를 제공하는데 registry.enableSimpleBroker("/sub") 설정은 이러한 내부적인 메시지 브로커를 활성화하는 것이며, 주로 작은 규모의 애플리케이션에서 사용됩니다.
    • RabbitMQ나 Kafka는 더 복잡하고 확장성 있는 메시지 브로커 시스템으로 선택될 수 있습니다.
      • 메시지 브로커란? : 메시지 브로커는 메시지 기반 시스템에서 메시지를 중개하고 전달하는 소프트웨어 또는 서비스입니다.

MessageController

@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);
    }
}
  • MessageController에서는 @MessageMapping("/chat/enter/{roomId}", @SendTo("/sub/chat/room/{roomId}") 어노테이션을 통해 pub/sub 경로를 지정해 줄 수 있습니다.
  • 위의 config에 언급했듯이 “pub/chat/enter/{roomId}”로 발행 요청을 하면 “sub/chat/room/{roomId}”로 메시지가 전달이 됩니다.
  • 저는 MessageController를 입장, 대화(chatting), 퇴장으로 나뉘어 작성하였습니다.

MessageService

@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);
    }
}
  • MessageService에서 Message엔티티에 메시지 정보를 저장하도록 로직을 구현해 보았습니다.

메시지 확인

  • apic 이라는 툴을 사용하여 STOMP 메시지를 송/수신할 수 있습니다.
  • 데이터베이스에 채팅 메시지의 데이터가 저장되는 것을 확인 할 수 있었습니다.

Web socket에서 STOMP로 바뀌고 이점

< + 나의 생각>
Web socket으로 채팅 서비스를 구현했을 때는 WebsocketSession정보를 서버 측에서 Map 자료구조를 통해 계속 가지고 있어야 했습니다. 만약 서버가 1대가 아닌 여러대인 경우에는 이 세션 정보를 다른 서버에서도 공유해야하며 데이터의 일관성을 맞춰줘야 합니다. 그러나 STOMP를 사용하였을 경우 메시지 브로커에 의해 알아서 메시지를 전달해주어 별도의 세션관리가 필요 없어졌습니다. 웹 소켓 사용시 MemberService에서 웹소켓 핸들러를 처리하는 로직이 가독성 있다고 느껴지지 않았는데 STOMP를 적용하니 코드의 가독성과 유지보수가 좋아졌다고 느껴졌습니다.
이후 Rabbitmq 혹은 Kafka를 이용하여 고성능 메시지 브로커를 접목시켜봐야겠습니다.

출처 :
[Spring Boot] WebSocket과 채팅 (3) - STOMP
Spring Websocket & STOMP

profile
방구석개발자

0개의 댓글