본문 바로가기

백엔드 개발/WebSocket

SpringBoot WebSocket Chatting Server 만들기 3단계 - 여러 채팅 서버 간의 메세지 공유 (by Redis Pub/sub)

https://www.daddyprogrammer.org/post/4731/spring-websocket-chatting-server-redis-pub-sub/

0.목차

1. 개요 
2. 전개도, 전체적인 흐름 파악
3. 코드 분석 
4. 프로젝트 진행하면서 있었던 에러 해결 

1. 개요 

이번 프로젝트에서는 Redis라는 인메모리형 DB를 활용하여 2번까지 진행했던 STOMP와 WebSocket만으로 구현한  Chatting Server 프로젝트를 더 고도화 하려고 한다. 

 어? 이미 하나의 서버에서 서로 메세지를 주고 받는 것은 충분히 되는데 왜 Redis를 장착해야 할까요?

그렇다면, 먼저 Redis를 왜 장착해야 하는지에 대해서 설명하겠다.

ⓐ STOMP와 WebSocket만을 쓰는 Chatting Server의 문제점 

⑴ 지금까지 만든 프로젝트는 서버가 꺼질 때마다 채팅방 정보들이 전부 사라진다. 

당연한 말이다. 지금까지의 프로젝트에서 우리는 DB에 채팅방 정보들을 저장하지 않고, HashMap에 저장했다. 따라서 서버가 꺼지면 해당 정보들은 모두 사라진다. 이를 방지 하기 위해, Redis라는 인메모리형 DB를 사용할 예정이다. 

 

⑴ - ⒜ 어? 근데 인메모리형 DB이면 해당 채팅방 정보도 DB 서버가 꺼지면 다 사라지지 않나요?? 

그것도 맞는 말이다. 그런데 생각해보면, 대화가 끝나고 모두가 방을 나가면 사라지는 채팅방의 정보를 DB에 저장할 필요가 있을까? 카카오톡 단톡방이나, 토크온 같은 플랫폼을 생각해보자. 만약 모든 채팅방을 디스크에 있는 DB에 저장한다면 쓸모 없는 데이터들이 디스크에 쌓여갈 것이다. 사람들이 다 나가거나, 더 이상 사용하지 않는 채팅방도 하루에 수 백, 수 천개가 생길 것이기 때문이다. 이러한 경우 Redis처럼 인메모리형 DB가 더 유용할 수 있다. Redis를 사용해야 하는 이유 첫번째는 백엔드 서비스 서버와 DB 서버가 분리된다는 점이다. 따라서 서비스 서버가 꺼져도, Redis 서버가 살아있다면 채팅방 정보들은 사라지지 않는다. 따라서 데이터가 휘발되는 정도를 낮출 수 있다. 두 번째로 데이터의 중요도를 나눠서 MySQL같은 디스크 기반 서버와 Redis같은 인메모리형 서버에 나눠 저장한다면, 데이터의 경중에 따라 효율적인 정보 저장 또한 가능하다. (하지만 만약 DB를 Redis 하나만 쓴다면, Redis 서버가 꺼졌을 경우 데이터 유실을 어떻게 대처할 것인지에 대한 플랜B를 꼭 세워놔야 하겠다.) 

⑵ 지금까지 만든 프로젝트는 채팅 서버가 여러 개일 경우, 서버 간에 채팅방을  공유할 수 없다. 

지금까지 만든 프로젝트의 Topic은 Topic이 발행된 서버에서만 유효하다. 따라서 A라는 서버를 통해 서비스를 사용하는 사용자는 B라는 서버에 발행된 Topic(우리로 치면 채팅방)을 볼 수도 없고, 구독할 수도 없다. 메이플스토리 서버를 떠올리면 편하겠다. 제니스 서버 캐릭터는 절대 스카니아 서버 캐릭터와 대화할 수 없다. 아예 다른 세계이기 때문이다. 우리가 만든 채팅서버도 마찬가지다. 따라서 이 문제를 해결하려면, 모든 서버가 공통으로 사용할 수 있는 pub/sub 시스템을 구축하는 것이 필요하겠다. 

해당 문제도 Redis를 사용하면 해결할 수 있다. 아까 1번에서 Redis를 써야하는 이유 중 하나로 백앤드 서버와 DB 서버의 분리를 들었다. 이렇게 백앤드 서버와 DB 서버를 분리한다면, 여러 개의 백엔드 서버가 하나의 DB 서버를 공유 사용하도록 할 수 있다. 이렇게 되면, 공통 PUB/SUB 시스템만 구축된다면 하나의 서버에서 만들어진 Topic을 다른 서버들에서도 접근하여 구독하는게 가능해진다! 

  마침 Redis의 경우, 이러한 공통 PUB/SUB 시스템 또한 구축되어있다. (그래서 더더욱 redis 사용을 추천한다.)  따라서 Redis를 사용한 범 서버적 웹 소켓 통신은 다음과 같은 그림으로 진행된다. 

2. 전개도 전체적인 흐름 파악 

ChatRoomController는 채팅방 생성 및 Redis에 등록, 채팅방 리스트와 특정 방 반환, 채팅방 리스트 화면과 채팅방 하나의 Detail 화면으로 이동을 담당한다. 여기서는 채팅방 생성 시 Redis에 Topic으로 등록하는 일 외에 특별히 따로 일을 하지 않는다. 
반면 Chat Controller는 본격적으로 Pub/Sub에 관한 일을 한다. 채팅방 Detail에 들어온 사용자가 메세지를 쳐서 입력하면, 비로소 해당 사용자를 해당 Topic의 구독자 및 발행자로서 등록하고, 자신이 보낸 메세지 및 같은 Topic (채팅방)에 들어온 사람들의 메세지를 볼 수 있도록 하는 것이다. 

3. 코드 분석 

ⓐ EmbeddedRedisConfig

package org.websocket.demo.config;


import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import redis.embedded.RedisServer;

// 로컬 환경에서만 실행합니다.
@Profile("local")
@Configuration
public class EmbeddedRedisConfig {

    @Value("${spring.data.redis.port}")
    private int redisPort;

    private RedisServer redisServer;

    @PostConstruct
    public void redisServer() {
        redisServer = RedisServer.builder()
                .port(redisPort)
                .setting("maxmemory 128M")
                .build();
    }

    @PreDestroy
    public void stopRedis() {
        if(redisServer != null){
            redisServer.stop();
        }
    }
}

해당 EmbeddedRedisConfig는 SpringBoot에 내장된 Redis를 사용하기 위한 설정 클래스이다. Redis에 대해 쉽게 배우기 위한 지금에만 사용하는 Config로 본격적인 Redis를 사용할 때는 사용하지 않을 클래스이다. 해당 클래스에서는 @PostConstruct와 @PreDestory란 어노테이션을 사용한다. @PostConstruct는 객체 초기화 이후 딱 한번만 매소드를 실행 시킬 때 사용하는 어노테이션이다. 해당 어노테이션이 쓰인 RedisServer()라는 함수는 내장 RedisServer가 돌아갈 포트와 최대 용량을 설정하여 RedisServer를 실행시키는 함수이다. 반대로 @PreDestroy이는 해당 Bean 객체가 소멸되기전에 해야할 일에 대해 정의한 어노테이션이다. 우리는 서버가 꺼지기 전에 RedisServer를 아예 '삭제'하지 않고 잠시 '멈춘'다. 따라서 컴퓨터가 꺼졌다가 다시 킬 경우에도 RedisServer에 저장한 내용들이 보인다. 

ⓑ RedisConfig

@Configuration
public class RedisConfig {

    // redis 내장 pub/sub 기능을 사용하기 위해 Message Listener 설정을 추가
    @Bean
    public RedisMessageListenerContainer redisMessageListener(RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        return  container;
    }

    // 우리가 만든 어플리케이션에서 사용할 RedisTemplate 설정, 직렬화 어케 할 건지, RestTemplate 연결에는 어떤 팩토리를 쓰는지
    // RedisTemplate: Redis 데이터 베이스와 상호작용하기위한 설정을 담은 클래스

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory){
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class));

        return redisTemplate;
    }
}

본격적인 Redis 설정 파일이다. Redis의 내장 Pub/Sub 시스템을 사용하기 위해서는 'RedisMessageListenerContainer'의 객체가 필요하다. 해당 객체는 특정 Topic으로 발행되는 메세지를 Topic 구독자들이 읽어서 볼 수 있도록 하는 객체이다. 이때 RedisMessageListenerContainer는 connectionFactory라는 녀석을 수입하여 자신의 멤버 변수로 저장한다. connectionFactory란,  Redis DB와의 연결을 어떻게 할 것인지 설정하는 클래스이다. 해당 클래스는 두 가지 자식 클래스가 있는데, 해당 클래스들은 비동기 연결을 지원하는지, 리액티브 프로그래밍을 수월하게 하는 매소드를 지원하는지 등의 차이가 있다고 한다. 내장Redis에서는 어떤 ConnectionFactory를 사용하는지가 미리 정해져 있는 듯 하다. 이것은 나중에 더 자세히 알아보겠다. 
  RedisTemplate란 Redis DB와의 상호작용(CRUD)을 위한 설정을 담은 클래스이다. 여기서는 어떤 ConnectionFactory를 사용할지, Key와 Value의 역직렬화는 어떻게 할 것인지에 대한 설정을 해줘야 한다. 

 

ⓒ WebSockConfig 

@Configuration
@EnableWebSocketMessageBroker // 웹소켓에서 STOMP를 활성화 하기 위한 어노테이션
public class WebSockConfig implements WebSocketMessageBrokerConfigurer {


    // 메세지 송 수신에 대한 설정을 등록한 매소드이다.
    // 메세지 수신은 sub/room/방 번호로 해당 방 번호로 온 메세지만 수신하도록 설정
    // 메세지 발신의 경우에는 pub/room 으로 보내고 방 번호는 responseBody 속 메타데이터로 저장한다.
    @Override
    public void configureMessageBroker (MessageBrokerRegistry config){

        //메세지를 발행하는 요청의 접두사는 /pub가 되도록 설정
        config.setApplicationDestinationPrefixes("/pub");

        //메세지를 구독하는 요청의 접두사는 /sub가 되도록 설정
        config.enableSimpleBroker("/sub");

    }


    // Stomp 소켓을 쓸 주소를 특정한다.
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry){

        // stomp websocket으로 연결하는 주소의 endpoint는 /ws-stomp로 설정
        // 따라서 전체 주소는 ws://localhost:8080/ws-stomp
        registry.addEndpoint("/ws-stomp").setAllowedOriginPatterns("*")
                .withSockJS();
    }

}

 

configureMessageBroker는 메세지를 '발행하는 통로'와 '구독하는 통로'가 무엇인지 설정하는 매소드이다. 
registerStompEndPoints란 STOMP 웹소켓이 존재할 주소를 특정하는 매소드이다. 해당 매소드에서 우리는 setAllowedOriginPattern(*)를 통해 CORS 문제를 피하고 어떤 브라우저도 해당 주소로 접속할수 있도록 한다. 

 

ⓓ ChatController


@RequiredArgsConstructor
@Controller
public class ChatController {

    private final RedisPublisher redisPublisher;
    private final ChatRoomRepository chatRoomRepository;

    // @MessageMapping == 메세지가 WebSocket으로 발행되는 경우 밑의 매소드가 실행된다.
    @MessageMapping("/chat/message")
    public void message (ChatMessage message) {
    
    	// 클라이언트가 채팅방 입장 시 채팅방(TOPIC)에서 대화가 가능하도록 Topic 리스너를 설정하는 enterChatRoom 메서드를 세팅
        if(ChatMessage.MessageType.ENTER.equals(message.getType())){
            chatRoomRepository.enterChatRoom(message.getRoomId());
            message.setMessage(message.getSender() + "님이 입장하셨습니다.");
        }


        //메세지를 redis Publisher를 통해 발행 -> RedisDB에 저장된 해당 TOPIC으로 메세지를 보냄
        redisPublisher.publish(chatRoomRepository.getTopic(message.getRoomId()), message);
    }
}

Chat 컨트롤러는 Publish와 SubScribe라는 메세지 시스템에 대해서 다루는 컨트롤러이다. 먼저 메세지가 발행되면, 그 메세지가 Enter 타입일 경우, 해당 방으로 입장하는 절차를 먼저 거치고, 메세지 내용물을 '~~님이 입장하셨습니다로 채운다.' 입장 절차는 chatRoomRepository에서 더 자세히 다루겠다. 그 후 메세지의 방번호에 해당하는 Topic으로 메세지가 발행된다. TALK 타입일 경우, 해당 메세지를 바로 발행한다. 

 

ⓔ ChatRoomController

@RequiredArgsConstructor
@Controller
@RequestMapping ("/chat")
public class ChatRoomController {

    private final ChatRoomRepository chatRoomRepository;

    // 1) 채팅 리스트 화면 반환
    @GetMapping("/room")
    public String rooms (Model model) {
        return "/chat/room";
    }

    // 2) 채팅방 생성 -> 하나의 Topic을 생성 (RestAPI )
    @PostMapping("/room")
    @ResponseBody
    public ChatRoom createRoom(@RequestParam String name){
        return chatRoomRepository.createChatRoom(name);
    }



    // 3) 모든 채팅방 목록 반환 (RestAPI)
    @GetMapping("/rooms")
    @ResponseBody
    public List<ChatRoom> room() {
        return chatRoomRepository.findAllRoom();
    }


    // 4) 채팅방의 입장 ->  해당 토픽을 구독한다는 뜻
    @GetMapping("/room/enter/{roomId}")
    public String roomDetail (Model model, @PathVariable String roomId) {
        model.addAttribute("roomId", roomId);
        return "/chat/roomdetail";
    }


    // 5) 특정 채팅방 조회
    @GetMapping("/room/{roomId}")
    @ResponseBody
    public ChatRoom roomInfo(@PathVariable String roomId) {
        return chatRoomRepository.findRoomById(roomId);
    }

}

ChatRoom Controller는 채팅방 화면 이동, 특정 혹은 전체 채팅방 조회, 채팅방 개설 버튼을 누를 시, 채팅방 생성 로직으로 이동하는 것을 담당하고 있다. 

 

ⓕ ChatMessage 

import lombok.Getter;
import lombok.Setter;

// 채팅 메세지를 주고받기 위한 DTO
@Getter
@Setter
public class ChatMessage {

    // 메시지 타입: 입장, 채팅 -> 서버에서 클라이언트의 행동에 따라 클라이언트에게 전달할 메세지들을 선언한 것
    public enum MessageType {
        ENTER, TALK
    }
    
    private MessageType type;   // 메세지 타입
    private String roomId;      // 방 번호
    private String sender;      // 메세지 보낸 사람
    private String message;     // 메세지

}

채팅 메세지의 DTO이다. ENUM 값으로 입장인지, 대화인지가 나뉘어지고, 방 번호, 보낸 이, 메세지 내용물을 담고 있다. 

 

ⓖ ChatRoom 

// Redis에 저장되는 객체들은 Serializable이 가능해야 하므로, Serializable을 참조하도록 선언하고
// serialVersionUID를 세팅해준다.

@Getter
@Setter
public class ChatRoom implements Serializable {

    private static final long serialVersionUID = 6494678977089006639L;

    // 채팅방의 방 번호와 이름
    private String roomId;
    private String name;


    // 밑에 내용은 클래스 매소드로서 클래스 생성부터 존재하는 매소드이고, 하나의 chatRoom을 만들어서 반환한다.
    public static ChatRoom create(String name) {
       ChatRoom chatRoom = new ChatRoom();
       chatRoom.roomId = UUID.randomUUID().toString();
       chatRoom.name = name;

       return chatRoom;
    }
}

채팅방 자체에 대한 DTO이다. 채팅방 번호와 이름을 가지고 있다. create란 매소드는 채팅방 이름을 받으면, UUID를 이용해 채팅방을 생성하는 매소드이다. 하지만 여기서 생성한 채팅방은 아직 Redis DB에 저장하지 않았음으로 의미가 없다. 후에 Redis DB에 저장하는 절차 또한 거쳐줘야 한다. 

 

ⓗ RedisPublisher

@RequiredArgsConstructor
@Service
public class RedisPublisher {

    private final RedisTemplate<String, Object> redisTemplate;

    public void publish(ChannelTopic topic, ChatMessage message) {

        redisTemplate.convertAndSend(topic.getTopic(), message);
    }

}

 

채팅방으로 메세지를 발행하는 로직을 담은 클래스이다. 아까 위에서 배웠다시피, RedisTemplate 클래스는 Redis DB와의 CRUD를 하기 위한 설정을 하는 클래스이다. 해당 클래스를 이용해 RedisDB에 저장되어있는 Topic으로 메세지 메세지를 보낸다. 

 

ⓘ Redis Subscriber

@Slf4j
@RequiredArgsConstructor
@Service
public class RedisSubscriber implements MessageListener {

    private final ObjectMapper objectMapper;
    private final RedisTemplate redisTemplate;
    private final SimpMessageSendingOperations messagingTemplate;


    // Redis에서 메세지가 발행되면, 대기하고 있던 onMessage가 해당 메세지를 낚아채서 처리한다.
    @Override
    public void onMessage(Message message, byte[] pattern) {
        try{
            // redis에서 발행된 데이터를 받아 역직렬화 (낚아챔)
            String publishMessage = (String) redisTemplate.getStringSerializer().deserialize(message.getBody());
            // ChatMessage 객체로 맵핑
            ChatMessage roomMessage = objectMapper.readValue(publishMessage, ChatMessage.class);

            // Websocket 구독자에게 채팅 메세지를 Send
            messagingTemplate.convertAndSend("/sub/chat/room/" + roomMessage.getRoomId(), roomMessage);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }
}

Subscriber는 발행되어서 TOPIC으로 온 메세지를 어떻게 저장할지에 대하여 나타낸 클래스이다. 이때 MessageListener라는 인터페이스를 구현한다. 해당 인터페이스의 OnMessage라는 매소드는 메세지를 받아서 어떻게 처리할지에 대해서 나타내는 클래스이다. 해당 매소드 구현 사항을 살펴보면, 메세지를 RedisTemplate 객체로부터 받고, 역직렬화한다. 다시 역직렬화한 내용을 ChatRoom DTO에 맞게 직렬화하여 객체를 생성한다. 

            messagingTemplate.convertAndSend("/sub/chat/room/" + roomMessage.getRoomId(), roomMessage);

해당 부분은 해당 방 번호를 구독하고 있는 사용자들에게 메세지를 보내는 부분이다. (해당 방 번호의 방에 입장 하여 있는 상태가 구독하고 있는 상태이다.)

ⓙ ChatRoomRepository

@RequiredArgsConstructor
@Repository
public class ChatRoomRepository {
    // 채팅방에 발행되는 메세지를 처리할 Listener
    private final RedisMessageListenerContainer redisMessageListener;

    // 구독 처리 서비스
    private final RedisSubscriber redisSubscriber;

    // Redis
    private static final String CHAT_ROOMS = "CHAT_ROOM";
    private final RedisTemplate<String, Object> redisTemplate;


    // chatRoom이란 이름으로 저장된 모든 HashMap들
    private HashOperations<String, String, ChatRoom> opsHashChatRoom;

    // 채팅방의 대화 메세지를 발행하기 위한  redis topic의 정보
    // 서버 별로 채팅방에 매치되는 topic 정보를 Map에 넣어 roomId로 찾을 수 있도록 한다.
    private Map<String, ChannelTopic> topics;

    @PostConstruct
    private void init() {
        opsHashChatRoom = redisTemplate.opsForHash();
        topics = new HashMap<>();
    }


    public List<ChatRoom> findAllRoom() {
        return opsHashChatRoom.values(CHAT_ROOMS);
    }

    public ChatRoom findRoomById (String id) {
        return opsHashChatRoom.get(CHAT_ROOMS, id);
    }

    // 채팅방 생성: 서버 간 채팅방 공유를 위해 redis hash에 저장한다.

    public ChatRoom createChatRoom(String name) {
        // 채팅방을 만들고
        ChatRoom chatRoom = ChatRoom.create(name);

        //Redis Hash에 저장
        opsHashChatRoom.put(CHAT_ROOMS, chatRoom.getRoomId(), chatRoom);

        // 그 후 만든 ChatRoom을 반환
        return chatRoom;
    }

    // 채팅방 입장: redis에 topic을 만들고 pub/sub 통신을 하기 위해 리스너를 설정한다.

    public void enterChatRoom(String roomId) {
        // 입장해야할 채팅방 (topic)을 얻어온다.
        ChannelTopic topic = topics.get(roomId);

        if(topic == null)
            topic = new ChannelTopic(roomId);

        // 해당 Topic으로 들어온 메세지는 어떻게 처리할 것인지에 대해 명세한 redisSubScriber를 Listener에 등록
        redisMessageListener.addMessageListener(redisSubscriber, topic);
        topics.put(roomId, topic);
    }

    public ChannelTopic getTopic(String roomId) {
        return topics.get(roomId);
    }

먼저 

    private HashOperations<String, String, ChatRoom> opsHashChatRoom;

인자가 3개나 들어가는 HashOperations에 대해서 궁금할 것이다. 해당 부분은 <해당 HashMap의 이름, Key, Value>로 이루어진 Redis의 자료구조이다. 

앞에 나온 채팅방을 조회하는 로직은 저번 포스팅과 내용이 같으므로 채팅방을 생성하고, 채팅방에 입장하는 로직에 대해 다루어보겠다. 
먼저 채팅방 생성로직은 이름을 받아서 채팅방을 만들고 HashOperations에 등록한다. 그 뒤에 ChatRoom을 반환한다. 

 

채팅방 입장 로직은 먼저 Topics라는 서버 단에서 채팅방을 저장하고 있는 HashMap에서 해당 방번호에 해당하는 Topic 객체가 있는지 먼저 확인한다. 만약 없다면, ChannelTopic이라는 생성자를 이용하여 Topic을 생성하고 Topics라는 곳에 넣는다. 또한 Redis Subscriber를 이용해 해당 Topic에 Listener를 등록한다. 해당 Topic으로 뭔가 발행되면 Topic 구독자는 그것을 읽을 수 있도록 하는 것이다. 

 

ⓚ 프론트 엔드 코드 분석

해당 프로젝트에 대한 완벽한 이해를 위해서는 프로그램의 한 축인 프론트엔드 부분에 대한 이해도 필수적인 것 같다.  프론트엔드 파일은 딱 두 개로 채팅방 리스트 화면을 담당하는 Room.ftl 화면과 채팅방 속에 들어갔을 때 화면인 RoomDetail 화면이 있다. 하나씩 천천히 알아보자. 

 

⑴ Room.ftl

옛날 vue로 되어있는데, HTML 부분 빼고 Method 부분만 살펴보자. HTML은 직관적으로 이해가 되고, Method 부분이 핵심이기 때문이다. 다른 내용들은 밑의 깃허브를 참고해주면 좋겠다.

        methods: {
            findAllRoom: function() {
                axios.get('/chat/rooms').then(response => { this.chatrooms = response.data; });
            },
            createRoom: function() {
                if("" === this.room_name) {
                    alert("방 제목을 입력해 주십시요.");
                    return;
                } else {
                    var params = new URLSearchParams();
                    params.append("name",this.room_name);
                    axios.post('/chat/room', params)
                        .then(
                            response => {
                                alert(response.data.name+"방 개설에 성공하였습니다.")
                                this.room_name = '';
                                this.findAllRoom();
                            }
                        )
                        .catch( response => { alert("채팅방 개설에 실패하였습니다."); } );
                }
            },
            enterRoom: function(roomId) {
                var sender = prompt('대화명을 입력해 주세요.');
                localStorage.setItem('wschat.sender',sender);
                localStorage.setItem('wschat.roomId',roomId);
                location.href="/chat/room/enter/"+roomId;
            }
        }

1. findALLRoom은 Get 요청을 통해 JSON 형태로 모든 ChatRoom 객체를 반환 받는다. 그리고 반환받은 JSON 값들을 chatRooms라는 배열에 넣어서 화면에 보여준다. 

2. createRoom Function은 채팅방 제목을 입력하지 않았다면 경고문을 띄어서 돌려보내고, 제대로 적었다면, /chat/room으로 해당 방 제목을 실어서 Post 요청을 한다. 그리고, 제대로 POST 요청에 성공했다면, 다시 findAllRoom()을 해서 채팅방의 리스트를 새로 받아서 화면에 띄운다.

3. EnterRoom은 사용자가 고른 방의 방제목과 대화명으로 입력한 내용을 인수로 받아서 localStroage에 저장한다. 그리고 /chat/room/enter+ 방 번호로 화면 이동을 한다. location.href를 쓴 것을 보니, SPA를 지켜서 코딩한 것은 아닌 듯 하다...

 

⑵ roomDetail.ftl

// 변수 생성 및 초기화-------------------------------------------------------------------
	var vm = new Vue({
        el: '#app',
        data: {
            roomId: '',
            room: {},
            sender: '',
            message: '',
            messages: []
        },
        created() {
            this.roomId = localStorage.getItem('wschat.roomId');
            this.sender = localStorage.getItem('wschat.sender');
            this.findRoom();
        },
        
        // AXIOS -----------------------------------------------------------------
        methods: {
            findRoom: function() {
                axios.get('/chat/room/'+this.roomId).then(response => { this.room = response.data; });
            },
            sendMessage: function() {
                ws.send("/pub/chat/message", {}, JSON.stringify({type:'TALK', roomId:this.roomId, sender:this.sender, message:this.message}));
                this.message = '';
            },
            recvMessage: function(recv) {

                this.messages.unshift({"type":recv.type,"sender":recv.type=='ENTER'?'[알림]':recv.sender,"message":recv.message})
            }
        }
    });
    // pub/sub event ---------------------------------------------------------------------
    ws.connect({}, function(frame) {
        ws.subscribe("/sub/chat/room/"+vm.$data.roomId, function(message) {
            var recv = JSON.parse(message.body);

            vm.recvMessage(recv);
        });
        ws.send("/pub/chat/message", {}, JSON.stringify({type:'ENTER', roomId:vm.$data.roomId, sender:vm.$data.sender}));
    }, function(error) {
        alert("error "+error);
    });

먼저 들어오자 마자 create() 함수를 진행해서 localStroage에 넣었던 ID와 sender를 현재 페이지의 roomId와 sender에 넣는다.  

(1) Method 부분 

  • findRoom 함수를 통해서 현재 사용자가 들어간 방의 정보를 받는다. 정보를 받아서 room이란 변수에 저장한다. 
  • sendMessage : 드디어 pub/chat/message라는 주소를 만났다!! 여기서는 Message에 대한 발행을 담당한다. 사용자가 친 메세지 내용, 방번호, 보낸 이, 보낸 메세지 타입 : TALK으로 해서 해당 주소로 보낸다. 접두사가 pub이므로 백엔드로직에서는 해당 내용을 발행으로 알아먹을 것이다.
  • recvMessage: .unshift 함수는 배열 맨 앞에 해당 원소를 추가하고, 새로운 길이를 반환하는 함수이다. 누가 들어오면 바로 EnterType을 메세지를 만들어서 Messages라는 프론트 Level의 메세지를 전부 저장하는 배열에 저장한다. 만약 MessageType이 Enter가 아니면 그냥 있는 해당 내용으로 메세지 객체를 만들어서 메세지 배열에 넣는다. 

(2) WebSocket Connect 부분 

  • 프론트의 WebSocket 통신에 대해 잘은 모르겠지만, (먼저 백엔드 공부 끝내면 공부해야겠다.) 해당 페이지 랜더링 시 바로 실행하는 코드인 것 같다. 먼저 
    var sock = new SockJS("/ws-stomp");
    var ws = Stomp.over(sock);

해당 코드를 통해 백엔드에서 설정했던 소켓 통신 주소로 목표 지점을 지정한다.  그 후 

위의 ws.connect 부분을 실행한다. 

  • ws.subscribe를 통해서 "/sub/chat/room" + 현재 들어온 방의 방번호로 구독한다. 이렇게 되면 해당 방의 TOPIC으로 발행되는 메세지를 받을 수 있게 된다. 여기서는 메세지를 받아서 객체로 Parsing 하고  위에 Message 배열에 값을 넣고 관리하는 recvMessage 함수에 객체를 넣는다. 
  • ws.send는 pub/chat/message를 이용해서 방에 입장한 사람의 정보와 알림을 바로 socket으로 보내도록 한다. ENTER TYPE을 send하는 것은 누가 해당 방에 들어와서 roomDetail 페이지가 실행된 시점만 한다. 나머지 TALK 타입 메세지는 위의 sendMessage 함수로 버튼 클릭 시 ws.send 되도록 만든다. 

4. 프로젝트 진행하면서 생긴 에러들에 대하여 

해당 부분은 검색으로도 볼 수 있도록 다른 포스팅에서 작성하여 링크를 올리도록 하겠다. 

 

EmbededRedis 설정 오류 : https://dalcheonroadhead.tistory.com/373

 

 

해당 부분 코딩한 깃허브 바로 가기 : https://github.com/dalcheonroadhead/WebSocketBEPractice/tree/ThirdStep