SpringBoot에서 STOMP, Websocket 이용하여 실시간 채팅 만들기

tinyeye·2023년 3월 9일
0

스프링

목록 보기
7/10

독학으로 공부한 거라서 정확하지 않을 수 있습니다.
참고만 하시길 바랍니다!

스프링부트와 웹소켓 stomp를 이용해서 제가 실시간 채팅 서비스를 구현했던 과정을 써 보려고 합니다.


STOMP란?

  • STOMP(Simple Text Oriented Messaging Protocol)은 메세징 전송을 효율적으로 하기 위해 탄생한 프로토콜입니다.
  • websocket 위에서 동작하는 문자 기반 메세징 프로토콜로써 클라이언트와 서버가 전송할 메세지의 유형, 형식, 내용들을 정의하는 매커니즘입니다.
  • 기본적으로 pub / sub 구조로 되어있어 메세지를 전송하고 메세지를 받아 처리하는 부분이 확실히 정해져 있습니다.

1. 우선은 websocket을 프로젝트에 적용시켜 줍니다.

저는 gradle을 사용해서 아래 방법으로 적용시켰습니다.

implementation 'org.springframework.boot:spring-boot-starter-websocket'

2. stomp사용에 관한 설정들을 지정할 class파일을 하나 만들어 줍니다.

  1. WebSocketMessageBrokerConfigurer를 상속 받아 웹소켓 연결 속성을 지정 해 줍니다.
  2. @Configuration으로 빈으로 등록 해 주고
  3. @EnableWebSocketMessageBroker로 웹소켓 서버를 사용한다는 설정을 해줍니다.
  4. registerStompEndpoints 메서드와 configureMessageBroker 메서드를 구현해 줍니다.
@Configuration
@EnableWebSocketMessageBroker
public class StompConfig implements WebSocketMessageBrokerConfigurer {

	
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws/chat")
                .setAllowedOriginPatterns("*")
                .withSockJS();
    }
    
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes("/pub");
        registry.enableSimpleBroker("/sub");
    }
}

구현 내용에 대한 설명은 다음과 같습니다.

registerStompEndpoints

소켓의 연결과 관련된 설정을 합니다.

  • addEndpoint("/ws/chat") : /ws/chat 이라는 해당 주소로 소켓을 연결합니다.
  • setAllowedOriginPatterns : CORS 설정을 해주는 부분입니다. 저는 "*"를 사용해서 모든 접근을 허용 했습니다.
  • withSockJS() : 일부 소켓을 지원하지 않는 브라우저에서 sockJS를 사용하게 합니다.

configureMessageBroker

Stomp 사용을 위한 Message Broker 설정을 해주는 메소드입니다.

  • setApplicationDestinationPrefixes : 메시지를 발행(pub)할 때 관련 경로를 지정 해 주는 함수입니다.
  • enableSimpleBroker : 메시지를 구독(sub)할 때 관련 경로를 지정 해 주는 함수입니다.

3. 채팅을 주고 받을 Entity 클래스를 만들어 줍니다.

@Entity
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
@Getter
@Table(name = "chat_tbl")
public class Chat {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long chatId;

    @ManyToOne
    @JoinColumn(name = "sender_member_id")
    private Member sender;

    @ManyToOne
    @JoinColumn(name = "receiver_member_id")
    private Member receiver;

    private String message;

    private LocalDateTime chatTime;

** 여기서 Member타입은 회원의 정보를 가진 엔티티 클래스 입니다.


4. DB와 소통할 Repository와 Service 클래스를 만들어 줍니다.

  • Repository
public interface ChatRepository extends JpaRepository<Chat,Long> {
	List<Chat> findAllBySenderAndReceiverOrderByChatTimeAsc(Member sender, Member receiver)
}
  • Service
@Service
@RequiredArgsConstructor
@Transactional // 오류 발생시 롤백 위해서
public class ChatService{

	private final ChatRepository chatRepository;
    
    public void chatSave(Chat chat) {
        chatRepository.save(chat);
        
        return chat;
    }
    
    public List<Chat> getChatHistory(Long chatRoomId) {
        List<Chat> chatHistory = chatRepository.findAllBySenderAndReceiverOrderByChatTimeAsc(ChatRoom.builder().chatroomId(chatRoomId).build());
        
        return chatHistory;
    }
}

5. Controller 클래스를 만들어 줍니다.

** 저는 타임리프를 사용하고 있기 때문에 model에 값을 넣고, html 파일을 반환해 주고 있습니다.

  • @MessageMapping : 설정한 url 매핑으로 클라이언트로부터 요청 메시지를 받는 주소 설정입니다.
    아까 설정파일에서 prefix를 pub으로 지정 해 놓았기 때문에 여기서는 pub을 생략합니다.
  • @DestinationVariable : MessageMapping에서는 @PathVariable대신에 @DestinationVariable을 사용합니다.
@Controller
@RequiredArgsConstructor
public class ChatController {

    private final ChatService chatService;
    private final SimpMessagingTemplate template;

    @GetMapping("/chat/{chatMember}")
    public String chatRoomDetail(@AuthenticationPrincipal Member member, @PathVariable String chatMember,Model model) {
        List<ChatDto> history = chatService.getChatHistory(roomId);
        model.addAttribute("history", history);

        return "chat/chatDetail";
    }

    @MessageMapping("/sendMessage/{senderId}")
    public void message(@RequestBody Chat chat, @DestinationVariable String senderId) {
        ChatDto receive = chatService.chatSave(chatDto);
        template.convertAndSend("/sub/chat/receive/chatroom",receive);
    }
}

public void message

  1. 클라이언트에서 pub/sendMessage/{senderId} 로 메시지를 발행 하면 컨트롤러의 이 메서드에서 처리를 합니다.
  2. 받은 메시지를 처리하고 convertAndSend를 통해서 클라이언트가 구독하고 있는 채널(/sub/chat/receive/chatroom)로 발행한 메시지를 보냅니다.

6. Client쪽 JavaScript 작성

전체 코드

<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>

$(function(){
            connectStomp();
});

let sock = new SockJS(`https://${location.host}/ws/chat`);
let ws = Stomp.over(sock);

function connectStomp(){
  
  ws.connect(headers,function(){
    ws.subscribe("/sub/chat/receive/"+roomId,function(event){
    let data = JSON.parse(event.body);
    let sender = data.senderId;
    let message = data.message;
    let time = data.chatTime;

    if(sender != myId){
      receiveMessage(sender,message,time);
    }

    });
  });
};

function sendMessage(){
  let message = $('#sendMessage').val();
  
  ws.send("/pub/sendMessage/"+myId,{},JSON.stringify({
  "senderId" : myId,
  "receiverId" : otherId,
  "message" : message,
  "chatTime" : getTimeAndSec()
  }));
;}

SockJS 설정 및 연결 주소 할당 부분

let sock = new SockJS(`https://${location.host}/ws/chat`);
let ws = Stomp.over(sock);

connectStomp()

  • 실제 웹소켓을 연결하고 채널을 구독하는 부분 입니다.
  • 구독한 채널로 발행한 값을 event에서 받습니다.
  • 이 값을 가지고 html파일에 채팅 내용을 그려주면 됩니다.
function connectStomp(){
  
  ws.connect({},function(){
    ws.subscribe("/sub/chat/receive/"+roomId,function(event){
    let data = JSON.parse(event.body);
    let sender = data.senderId;
    let message = data.message;
    let time = data.chatTime;
      
      ...
      
    });
  });
};

sendMessage()

  • 메시지를 발행하는 부분입니다.
  • .send의 첫번째 매개변수 부분에 "setApplicationDestinationPrefixes에서 지정한 주소 + @MessageMapping에서 지정한 주소"를 넣어줍니다 -> 이 주소가 발행하는 주소 입니다.
  • JSON.stringify로 JavaScript 값이나 객체를 JSON 문자열로 변환해서 서버로 보냅니다.
function sendMessage(){
  ws.send("/pub/sendMessage/"+myId,{},JSON.stringify({
  "senderId" : myId,
  "receiverId" : otherId,
  "message" : message,
  "chatTime" : getTimeAndSec()
  }));
;}
profile
백엔드 개발자를 노리며!

0개의 댓글