Spring & SpringBoot

Springboot + jwt + web socket(STOMP) 채팅 구현

땅콩콩 2024. 2. 12. 22:13

스프링부트로 채팅을 구현하기 위해 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);
    }
}

 

위 코드에서 ChatServicesendMessage 메소드에서 messagingTemplate.convertAndSend를 사용하는 부분은 클라이언트가 아니라 서버 측에서 발생한다.

 

즉, 클라이언트가 /pub/chat/message를 통해 메시지를 서버에 보내면, 서버는 그 메시지를 받아 처리하고, 메시지가 속한 채팅방의 모든 구독자에게 /sub/chatroom/{chatRoomId} 경로를 통해 메시지를 전송하는 것이다.

 

정리하면 아래와 같다!!

 

(프론트엔드)

1. 채팅방에 참여할 때 /sub/chatroom/{chatRoomId}로 구독을 시작

2. 메시지를 보낼 때: /pub/chat/message로 메시지를 발행

 

(백엔드)

1. /pub/chat/message 경로로 들어온 메시지를 받아 처리

2. 해당 채팅방을 구독하는 모든 클라이언트에게 /sub/chatroom/{chatRoomId} 경로로 메시지를 전송