ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • WebSocket 사용해서 react와 함께 채팅구현하기 (Stomp사용하기)
    Spring 2021. 9. 5. 17:30

     

     

    nodejs에서 socketio를 이용해서 클라이언트와 서버의 채팅을 구현한다면, spring에는 stompjs가 존재한다. 

     

    STOMP

    stomp는 websocket와 같이 양방향(클라이언트-서버)네트워크 프로토콜로 HTTP에서 모델링되는 프레임기반의 프로토콜이다. spring에서 stomp를 사용한다면, spring websocket 어플리케이션은 Stomp Broker로 작동하게 된다. websocket에서는 text나 binary데이터를 전송하면서 추가적인 정보(예를들면, 어디로 route하고 어떻게 처리할지)의 부재로 추가코드작성이 불가피해진다. 이를 해결하기위한 서브프로토콜이 stomp이다. stomp덕분에 CONNECT,  SUBSCRIBE, UNSUBSCRIBE, ACK, SEND와 같은 웹소켓 프레임을 통해서 클라이언트들과 broker들이 서로 다른 언어로 메세지를 주고받을 수 있게 되었다. 이 stomp의 요청 프레임 안에는 header가 포함되어, 어디에서 이 메세지를 수신할 지에 대한 정보가 담겨있다. 

     

     

    출처 구글

     

    메세지를 전달하는 대상, 즉 특정대상 혹은 다수의 대상인지에 따라서 사용되는 채널이 달라지게 된다. 

     

     

    보기를 보면서 진행해보자. 

    (프로젝트내에 사용된 것을 가지고 작성하므로, 생략된 부분이 존재할 수 있다)

     

     

    Server(Spring)

    먼저, intellij에서 스프링부트에 다음과 같이 dependency를 추가해준다. 

    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-websocket</artifactId>
    		</dependency>

     

    WebSocketConfig 클래스를 만들어주고 다음과 같이 작성해준다. 

    import org.springframework.context.annotation.Configuration;
    import org.springframework.messaging.simp.config.MessageBrokerRegistry;
    import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
    import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
    import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
    
    @Configuration
    @EnableWebSocketMessageBroker
    public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
        @Override
        public void registerStompEndpoints(StompEndpointRegistry registry) {
            registry.addEndpoint("/chat").setAllowedOrigins("http://localhost:3000").withSockJS();
        }
    
        @Override
        public void configureMessageBroker(MessageBrokerRegistry registry) {
    
            registry.enableSimpleBroker("/topic","/queue");
            registry.setApplicationDestinationPrefixes("/app");
    
        }
    }

     

    여기서 /topic은 1:N의 일대다의 구독방식을 가지고 있고, /queue는 1:1구독방식으로 일대일 메세지 전달을 할때 사용된다. 

    즉 다수에게 메세지를 보낼때는 '/topic/주소', 특정대상에게 메세지를 보낼 때는 '/queue/주소'의 방식을 택하게 된다. 

     

    /app은 메세지를 보내는 prefix로 작동하며 클라이언트->서버로 메세지를 보낼때는 다음과 같은 방식을 통하게 된다. 

    client.send(`/app/chat/보낼주소`,{},JSON.stringify(보낼데이터))

     

    먼저 특정 대화방 conversation 과 대화방이 가지고 있는 특정 message엔티티를 작성해주자. 

    conversation와 유저는 서로 다수의 대상을 가질 수 있으므로 다대다의 관계를 가지며,  하나의 대화방당 여러개의 메세지를 가질 수 있고 메세지는 하나의 대화방아이디만을 가질 수 있으므로 일대다의 관계를 가진다. 

    import lombok.Getter;
    import lombok.Setter;
    
    import javax.persistence.*;
    import java.util.*;
    
    @Entity
    @Table(name = "conversation")
    @Getter
    @Setter
    public class Conversation extends BaseTimeEntity {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "conversation_id")
        private Integer id;
    
        @OneToMany(mappedBy = "conversation" , cascade = CascadeType.ALL, orphanRemoval = true)
        private List<Message> messages = new ArrayList<>();
    
        private String text;
    
        @Column(name = "image_name")
        private String imageName;
    
        @Column(name = "image_url")
        private String imageUrl;
    
    
        @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
        @JoinTable(
                name = "conversation_user",
                joinColumns = @JoinColumn(name = "conversation_id"),
                inverseJoinColumns = @JoinColumn(name = "user_id")
        )
        private Set<User> users = new HashSet<>();
    
        public Conversation() {
        }
    
        public void addUser(User user){
            this.users.add(user);
        }
    
    }

     

    message는 메세지 하나당 하나의 작성한 사람을 가지고, 유저는 다수의 메세지를 가질 수 있으므로 일대다의 관계를 가진다. 

    import lombok.Getter;
    import lombok.Setter;
    
    import javax.persistence.*;
    import java.util.Date;
    
    @Entity
    @Table(name = "message")
    @Getter
    @Setter
    public class Message extends BaseTimeEntity {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "message_id")
        private Integer id;
    
        private String content;
    
        @Column(name = "image_name")
        private String imageName;
    
        @Column(name = "image_url")
        private String imageUrl;
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "conversation_id")
        private Conversation conversation;
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "user_id")
        private User User;
    
        public Message() {
        }
    
        public Message(String content, Conversation conversation, com.yunhalee.withEmployee.entity.User user) {
            this.content = content;
            this.conversation = conversation;
            User = user;
        }
    
        public Message(String content, String imageName, String imageUrl, Conversation conversation, com.yunhalee.withEmployee.entity.User user) {
            this.content = content;
            this.imageName = imageName;
            this.imageUrl = imageUrl;
            this.conversation = conversation;
            User = user;
        }
    
    }

     

    user엔티티는 다음과 같다. (유저쪽에서 유저가 작성한 메세지만을 조회할 일이 없으므로, 메세지연관관계를 작성해주지 않았다)

    import lombok.Getter;
    import lombok.Setter;
    
    import javax.persistence.*;
    import java.util.ArrayList;
    import java.util.HashSet;
    import java.util.List;
    import java.util.Set;
    
    @Entity
    @Table(name="user")
    @Getter
    @Setter
    public class User {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name="user_id")
        private Integer id;
    
        @Column(name = "name", nullable = false, length = 40)
        private String name;
    
        @Column(name = "email", nullable = false, unique = true)
        private String email;
    
        @Column(name = "password", nullable = false)
        private String password;
    
        @Column(name = "description")
        private String description;
    
        @Column(name="image_name")
        private String imageName;
    
        @Column(name = "image_url")
        private String imageUrl;
    
        @ManyToMany(mappedBy = "users", cascade = CascadeType.ALL)
        private Set<Conversation> conversations = new HashSet<>();
    
    
        public User(){
            super();
        }
    
        public User(String name, String email, String password) {
            this.name = name;
            this.email = email;
            this.password = password;
        }

     

     

    SocketController클래스를 만들어주고 다음과 같이 작성해준다. (메세지 DTO는 따로 작성하지 않겠다)

     

    userList라는 해시셋을 이용해서 로그인한 유저의 아이디 리스트를 작성해주었다.  client에서 join을 할 때마다, 유저 리스트에 유저의 정보를 추가해서 로그인한 유저의 정보를 알수 있도록 작성하였다. 

    또, 특정된 대상에게 메세지를 보내기 위해서 유저의 아이디를 이용하였는데, SimpMessagingTemplate을 이용해서 convertAndSend를 통해서 특정아이디를 이용한 주소로 메세지를 보내주었다. 

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.messaging.handler.annotation.DestinationVariable;
    import org.springframework.messaging.handler.annotation.MessageMapping;
    import org.springframework.messaging.handler.annotation.Payload;
    import org.springframework.messaging.simp.SimpMessagingTemplate;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.HashSet;
    import java.util.Set;
    
    @RestController
    public class SocketController {
    
    
        private static Set<Integer> userList = new HashSet<>();
    
        @Autowired
        private SimpMessagingTemplate simpMessagingTemplate;
    
        @MessageMapping("/chat/{id}")
        public void sendMessage(@Payload MessageDTO messageDTO, @DestinationVariable Integer id){
            this.simpMessagingTemplate.convertAndSend("/queue/addChatToClient/"+id,messageDTO);
        }
    
        @MessageMapping("/join")
        public void joinUser(@Payload Integer userId){
            userList.add(userId);
            userList.forEach(user-> System.out.println(user));
        }
    }

     

     

    React

    이제 클라이언트에서도 websocket stomp를 사용하여 서버와 소통할 수 있도록 해보자. npm i sockjs-client, stompjs를 실행해준다. 

    여기서 서버는 8080의 포트, 클라이언트는 3000의 포트를 사용하는데, 프록시를 이용해서 cors 오류를 방지해준다. 

     

    다음과 같이 서버와 연결하고, 메세지를 주고받을 수 있도록 해준다.  서버의 주소는 8080이고, 서버의 endpoint로 'chat'으로 선택해주었기 때문에 다음과 같이 작성해준다. 

      var sock = new SockJS('http://localhost:8080/chat')
      let client = Stomp.over(sock);

     

     

    이제 연결을 통해서 메세지를 주고받도록 하자. 

        // Connect
        useEffect(() => {
            client.connect({}, () =>{
                console.log('Connected : ' + auth.user.id)
                client.send("/app/join", {},JSON.stringify(auth.user.id))
    
                // Create Message
                
                client.send(`/app/chat/${(메세지받을대상)user.id}`,{},JSON.stringify(res.data))
    
                client.subscribe('/queue/addChatToClient/'+auth.user.id, function(messageDTO){
                    const messagedto = JSON.parse(messageDTO.body)
                })
    
            })  
            return () => client.disconnect();
    
        }, [client, auth.user.id, dispatch])

    여기서 client.send("/app/join",{}, JSON.stringify(auth.user.id))를 통해서 유저가 로그인하면(접속하면) 접속한 유저의 아이디를 서버로 보내서 접속한 유저의 정보를 받게 된다. 

     

    특정 대상에게 보내기 위해서 메세지를 받을 대상에게 메세지를 전달하면, 서버는 아이디를 받고 아이디를 가진 유저에게 다시 메세지 내용을 보낸다. 

     

    클라이언트(메세지와 받을사람아이디를 같이 전송) -> 서버(메세지를 받을 사람에게 전달) -> 클라이언트(받을사람이 메세지 수신)

     

     

    모두에게 보낸다면 "/topic/주소-"를 통해서 특정대상이아닌 모든이가 수신하도록 주소를 작성해주면 된다. 

     

     

     

     

     

     

     

    (websocket학습을 위해서 참고한 블로그)

    https://dev-gorany.tistory.com/235

     

    [Spring Boot] WebSocket과 채팅 (3) - STOMP

    [Spring Boot] WebSocket과 채팅 (2) - SockJS [Spring Boot] WebSocket과 채팅 (1) 일전에 WebSocket(웹소켓)과 SockJS를 사용해 Spring 프레임워크 환경에서 간단한 하나의 채팅방을 구현해본 적이 있다. [Sprin..

    dev-gorany.tistory.com

    http://it-archives.com/222147127689/

     

    [SpringBoot] 스프링부트 웹소켓(WebSocket) 예제 – 흑곰의 유익한 블로그 2호점

    [SpringBoot] 스프링부트 웹소켓(WebSocket) 예제 1. 스프링부트 웹 프로젝트 생성 우선 스프링부트 웹 프로젝트가 있어야 한다. 새로 시작하려면 아래 포스트를 참고하면 된다. https://blog.naver.com/bb_/2221

    it-archives.com

    https://tjdans.tistory.com/25

     

    Spring Boot - WebSocket & JWT & Spring Security 토큰 인증

    개발 환경 : Spring Boot, Maven, Web Socket ( Sock JS ), Spring Secuirty , Json Web Token, Vue jwt 인증 헤더 : 'Authorization' JWT 인증 기반의 프로젝트에서의 인증 JWT를 기반으로 하는 제 프로젝트의 인..

    tjdans.tistory.com

     

     

     

     

     

     

Designed by Tistory.