보라코딩

SpringBoot + React 채팅 구현 (웹소켓, stomp, redis) 본문

코딩/Spring

SpringBoot + React 채팅 구현 (웹소켓, stomp, redis)

new 보라 2023. 11. 20. 22:10
WebSocketConfig (WebSocket, Stomp 설정)
implementation 'org.springframework.boot:spring-boot-starter-websocket'
@Configuration // Config로 지정
@EnableWebSocketMessageBroker // 웹소켓 사용하는 브로커 활성화
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config){
        // 클라이언트에게 topic으로 시작하는 것에 대한 구독 기능 제공
        config.enableSimpleBroker("/sub");
        // 클라이언트에서 서버로 메시지 보낼 때 app 경로 사용하도록 설정
        config.setApplicationDestinationPrefixes("/pub");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry){
        // ws 엔드포인트 등록하고 모든 도메인에서 접근 허용, SockJS 사용해서 브라우저가 웹소켓 지원하지 않을때 폴백 옵션 활성화(다른 수단으로 통신)
        registry.addEndpoint("/ws").setAllowedOriginPatterns("*").withSockJS();
    }
}

 

 

 

RedisConfig (Redis 설정)

 

Redis 설치 필요

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

 

application.properties에 설정 필요

spring.redis.host=localhost 
spring.redis.port=6379

 

 

Spring Data Redis 는 RedisTemplate와 RedisRepository 이용해서 Redis에 접근

 

 

- RedisTemplate : 스프링 프레임워크에서 제공하는 Redis와 상호작용하는 도구. Redis와 통신 담당

- Key-Value 형태로 저장하며 객체를 직렬화해서 저장하고, Jackson 라이브러리 사용해서 JSON형식으로 객체 직렬화

- Jackson 라이브러리 : Java 객체를 JSON 형식으로 직렬화, JSON을 다시 Java 객체로 역직렬화

 

 

@Configuration
public class RedisConfig {

    private final Logger log = LoggerFactory.getLogger(getClass());

    @Bean
    public RedisTemplate<String, ChattingMessage> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // RedisTemplate 빈을 생성하는 메서드
        RedisTemplate<String, ChattingMessage> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        // Key Serializer를 StringRedisSerializer로 설정
        template.setKeySerializer(new StringRedisSerializer());

        // Value Serializer를 Jackson2JsonRedisSerializer로 설정
        Jackson2JsonRedisSerializer<ChattingMessage> jsonSerializer = new Jackson2JsonRedisSerializer<>(ChattingMessage.class);
        template.setValueSerializer(jsonSerializer);

        // Hash Key Serializer를 StringRedisSerializer로 설정
        template.setHashKeySerializer(new StringRedisSerializer());

        // Hash Value Serializer를 Jackson2JsonRedisSerializer로 설정
        template.setHashValueSerializer(jsonSerializer);

        // 로깅: RedisTemplate이 성공적으로 구성되었음을 로그로 출력
        log.info("RedisTemplate configured successfully");

        return template;
    }
}

 

 

 

 

 

 

ChatService (Redis 저장 및 조회 Service)

 

 

- MESSAGE_KEY : 모든 채팅방의 메시지를 redis에 저장할 때 사용되는 키 (고유한 식별자)

   => 각 채팅방의 키는 messages:535 이런 식으로 저장됨

 

- redisTemplate.opsForList().rightPush(roomKey, message);

   : Redis의 리스트는 여러 개의 요소를 순서대로 저장할 수 있는 데이터 구조.

     각 요소는 인덱스를 가짐. 리스트의 왼쪽 끝이나 오른쪽 끝에 새로운 요소를 추가 가능

     rightPush 메서드는 리스트의 오른쪽 끝에 새로운 요소를 추가

 

 

@Service
public class ChatService {

    @Autowired
    private RedisTemplate<String, ChattingMessage> redisTemplate;
    private final Logger log = LoggerFactory.getLogger(getClass());

    // Redis에 저장할 메시지의 키
    private static final String MESSAGE_KEY = "messages";

    // 메시지 저장 메서드
    public void saveMessage(ChattingMessage message){
        log.info("ChatService_saveMessage : " + message);

        // 각 채팅방에 대한 별도의 키 생성
        String roomKey = MESSAGE_KEY + ":" + message.getRoomNo();

        // 해당 채팅방의 메시지 목록에 새로운 메시지 추가
        redisTemplate.opsForList().rightPush(roomKey, message);
    }

    // 전체 채팅방의 메시지 목록 조회 메서드
    public List<ChattingMessage> getMessages() {
        log.info("ChatService_getMessages");

        Long size = redisTemplate.opsForList().size(MESSAGE_KEY);
        if (size != null) {
            return redisTemplate.opsForList().range(MESSAGE_KEY, 0, size - 1);
        }
        return Collections.emptyList();
    }

    // 특정 채팅방의 메시지 목록 조회 메서드
    public List<ChattingMessage> getMessagesByRoom(String roomNo) {
        log.info("ChatService_getMessagesByRoom : "+ roomNo);
        String roomKey = MESSAGE_KEY + ":" + roomNo;

        Long size = redisTemplate.opsForList().size(roomKey);
        log.info("Room key: {}, Message count: {}", roomKey, size);

        if (size != null) {
            List<ChattingMessage> messages = redisTemplate.opsForList().range(roomKey, 0, size - 1);
            log.info("Messages retrieved: {}", messages);
            return messages;
        }
        log.info("No messages found.");
        return Collections.emptyList();
    }
}

 

 

 

 

 

WebSocketController

 

 

- SimpleMessagingTemplate : STOMP(메시징 위한 간단, 유연한 프로토콜로 websocket 위에서 동작)를 지원

   - 역할 : convertAndSend 메서드 사용하여 메시지 전송

 

 

@RestController
public class WebSocketController {

    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;

    @Autowired
    private ChatService chatService;

    private final Logger log = LoggerFactory.getLogger(getClass());

    // 클라이언트에서 특정 채팅방의 메시지 조회
    @GetMapping("/api/messages/{roomNo}")
    public List<ChattingMessage> getMessagesByRoom(@PathVariable String roomNo) {
        log.info("Received request to get messages for room: {}", roomNo);

        return chatService.getMessagesByRoom(roomNo);
    }


    // WebSocket에서 사용하는 메시지 매핑 : 채팅방 입장 메시지 처리                 
    @MessageMapping("/chat/join/{roomNo}")
    public void join(@DestinationVariable String roomNo, @Payload ChattingMessage chatDto) {
        log.info("User '{}' joined the room '{}'", chatDto.getUser(), roomNo);

        // 채팅방 입장 메시지 저장
        chatService.saveMessage(chatDto);

        // 채팅방 입장 메시지를 해당 채팅방의 구독자에게 전송
        chatDto.setMessage(chatDto.getUser() + "님이 입장하셨습니다.");
        simpMessagingTemplate.convertAndSend("/sub/chat/join/" + roomNo, chatDto);
    }

     // WebSocket에서 사용하는 메시지 매핑 : 채팅방 메시지 전송 처리
    @MessageMapping("/chat/message/{roomNo}")
    public void sendMessage(@DestinationVariable String roomNo, @Payload ChattingMessage chatDto) {
        log.info("User '{}' sent a message in the room '{}': '{}'", chatDto.getUser(), roomNo, chatDto.getMessage());

        // 채팅방 메시지 저장
        chatService.saveMessage(chatDto);

        // 메시지가 ~로 시작한다면 해당 채팅방의 모든 구독자에게 전송
        if (chatDto.getMessage().startsWith("님이 입장하셨습니다.")) {
            simpMessagingTemplate.convertAndSend("/sub/chat/room/" + roomNo, chatDto);
        }
    }
}

 

 

 

 

React
 

연결

const socket = new SockJS("http://localhost:8080/ws");
const stompClient = Stomp.over(socket);
  stompClient.connect({}, () => {
        stompClient.subscribe("/sub/chat/room/" + room, (message) => {
 
        });
 
 
입장 메세지 전송
        stompClient.send(
          "/pub/chat/join/" + room,
          {},
          JSON.stringify({
            message: user + "입장",
            user: user,
            roomNo: room,
            timeStamp: currentTime,
          })
        );
 

 

메세지 전송

    if (stompClientRef.current && currentMessage.trim() !== "") {
      console.log("stompClient true!");
      stompClientRef.current.send(
        "/pub/chat/message/" + room,
        {},
        JSON.stringify({
          message: currentMessage,
          user: user,
          roomNo: room,
          timeStamp: currentTime,
        })
      );

 

 

 

레디스에서 데이터 가져오기

      axios
        .get("/api/messages/" + room)
        .then((response) => {
          console.log(response.data);
          setMessageHistory(response.data);
        })
        .catch((error) => {
          console.error("Error fetching messages:", error);
        });