채팅

채팅방 STOMP + REDIS의 PUB/SUB 기능 구현

전한준 2025. 6. 19. 19:08

 

<template>
    <v-container>
        <v-row>
            <v-col>
                <v-card>
                    <v-card-title class="text-center text h-5">
                            채팅방 목록
                            <div class="d-flex justify-end">
                                <v-btn color="secondary" @click="showCreateRoomModal = true ">
                                    채팅방 생성
                                </v-btn>
                            </div>
                    </v-card-title>
                    <v-card-text>
                        <v-table>
                            <thead>
                                <tr>
                                    <th>방번호</th>
                                    <th>방제목</th>
                                    <th>채팅</th>
                                </tr>
                            </thead>
                            <tbody>
                                <tr v-for="chat in chatRoomList" :key="chat.roomId">
                                    <td>{{ chat.roomId }}</td>
                                    <td>{{ chat.roomName }}</td>
                                    <td>
                                        <v-btn color="primary" @click="joinChatRoom(chat.roomId)"
                                        >참여하기</v-btn>
                                    </td>
                                </tr>
                            </tbody>
                        </v-table>
                    </v-card-text>
                </v-card>
            </v-col>
            <v-dialog v-model="showCreateRoomModal" max-width="500px">
                <v-card>
                    <v-card-title class="text-h6">
                        채팅방 생성
                    </v-card-title>
                    <v-card-text>
                        <v-text-field label="방제목" v-model="newRoomTitle"/>
                    </v-card-text>
                    <v-card-actions>
                        <v-btn color="grey" @click="showCreateRoomModal=false">
                            취소
                        </v-btn>
                        <v-btn color="primary" @click="createChatRoom"> 
                            생성
                        </v-btn>
                    </v-card-actions>
                </v-card>
            </v-dialog>

        </v-row>
    </v-container>
    
    </template>
    <script>
    import axios from 'axios';
    
    export default{
        data(){
            return {
                chatRoomList :[],
                showCreateRoomModal:false,
                newRoomTitle:"",
            }
        },
        async created(){
            // 들어오자마자 이 함수를 호출한다. 
         this.loadChatRoom();
        },
        methods:{
           async joinChatRoom(roomId){
                await axios.post(`${process.env.VUE_APP_API_BASE_URL}/chat/room/group/${roomId}/join`);
                // 화면 전환을 해주는 용도 
                this.$router.push(`/chatpage/${roomId}`);
            },
           async createChatRoom(){
            await axios.post(`${process.env.VUE_APP_API_BASE_URL}/chat/room/group/create?roomName=${this.newRoomTitle}`,null);
            this.showCreateRoomModal=false; 
            this.loadChatRoom(); 

            },
            async loadChatRoom(){
                const response=await axios.get(`${process.env.VUE_APP_API_BASE_URL}/chat/room/group/list`);
                this.chatRoomList=response.data;
            }
        }
    
    }
    
    </script>

 

 

router로 화면전화을 따로 해준다. 

API 로  기능을 다시 회고하자면 

 

 

그룹 채팅방 개설 및 참여 이전 포스트 글에서  다루었다. 

 

이전메시지 내역을 조회하자면

    public List<ChatMessageDto> getChatHistory(Long roomId){
        // 특정룸에
//        내가 해당 채팅방의 참여자가 아닐경우 에러
        ChatRoom chatRoom = chatRoomRepository.findById(roomId).orElseThrow(()-> new EntityNotFoundException("room cannot be found"));
        Member member = memberRepository.findByEmail(SecurityContextHolder.getContext().getAuthentication().getName()).orElseThrow(()->new EntityNotFoundException("member cannot be found"));
        // 해당 방의 참여자를 탐색한다.
        List<ChatParticipant> chatParticipants = chatParticipantRepository.findByChatRoom(chatRoom);
        boolean check = false;
        for(ChatParticipant c : chatParticipants){
            if(c.getMember().equals(member)){
                check = true;
            }
        }
        if(!check)throw new IllegalArgumentException("본인이 속하지 않은 채팅방입니다.");
//        특정 room에 대한 message조회
        List<ChatMessage> chatMessages = chatMessageRepository.findByChatRoomOrderByCreatedTimeAsc(chatRoom);
        List<ChatMessageDto> chatMessageDtos = new ArrayList<>();
        for(ChatMessage c : chatMessages){
            ChatMessageDto chatMessageDto = ChatMessageDto.builder()
                    .message(c.getContent())
                    .senderEmail(c.getMember().getEmail())
                    .build();
            chatMessageDtos.add(chatMessageDto);
        }
        return chatMessageDtos;
    }

 

참여자들로부터 채팅방을 찾고 참여자들이 같으면  이전 메시지를 조회할 수 있게 한다. 

보낸 사람에 대한 이메일까지 쓸 수 있다. 

 


@Component
public class StompHandler implements ChannelInterceptor {

    @Value("${jwt.secretKey}")
    private String secretKey;
    private final ChatService chatService;

    public StompHandler(ChatService chatService) {
        this.chatService = chatService;
    }

    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        final StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);

        if(StompCommand.CONNECT == accessor.getCommand()){
            System.out.println("connect요청시 토큰 유효성 검증");
            String bearerToken = accessor.getFirstNativeHeader("Authorization");
            String token = bearerToken.substring(7);
//            토큰 검증
            Jwts.parserBuilder()
                    .setSigningKey(secretKey)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
            System.out.println("토큰 검증 완료");
        }
        if(StompCommand.SUBSCRIBE == accessor.getCommand()){
            System.out.println("subscribe 검증");
            String bearerToken = accessor.getFirstNativeHeader("Authorization");
            String token = bearerToken.substring(7);
            Claims claims = Jwts.parserBuilder()
                    .setSigningKey(secretKey)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
            String email = claims.getSubject();
            // 요청 URL 을 꺼낼수 있다.  배열로 리턴되었을 떄 / 2번째를 알 수 있다.
            String roomId = accessor.getDestination().split("/")[2];
            // 참여자이면 true이고 아니면 false겠지?
            if(!chatService.isRoomPaticipant(email, Long.parseLong(roomId))){
                throw new AuthenticationServiceException("해당 room에 권한이 없습니다.");
            }
        }

        return message;
    }

}

 

STOMP 핸들러에 방에 접근 권한 여부를 설정 해주자.  SUBSCRIBE이 되어 있다면 메시지를 볼 수 있게 설정을 해준다. 

SUBSCRIBE 을 할때에 검증을 할 수 있게 해 주자.

여기 이것말고도 많다고 하셨다. 

 

웹소켓 URL만 알고 구독하는 것을 방지할 수 있습니다.

 

 

채팅 메시지 읽음에 관한 처리

크게 두가지가 있다. 

 

첫번째는 세션을 관리 ( 로직이 복잡하다.) 방에 일일히 누가 구독을 하였는지 조사 해 보는 것이다.

두번째는 우리가 화면을 끄거나 라우터로 다른 곳으로 이동을 하였을 경우 그 방에 있는 메시지를 전부 읽음 처리로 바꾸어 주는 것이다.

 

우리는 여기서 disconnect(화면 이동)으로 처리를 해 보겠다. 

//    채팅메시지 읽음처리
    // A라는 사람이 채팅방에 채팅을 다 읽었다.
    @PostMapping("/room/{roomId}/read")
    public ResponseEntity<?> messageRead(@PathVariable Long roomId){
        chatService.messageRead(roomId);
        return ResponseEntity.ok().build();
    }

 

채팅방 떠나기 기능

  public void leaveGroupChatRoom(Long roomId){
        ChatRoom chatRoom = chatRoomRepository.findById(roomId).orElseThrow(()-> new EntityNotFoundException("room cannot be found"));
        Member member = memberRepository.findByEmail(SecurityContextHolder.getContext().getAuthentication().getName()).orElseThrow(()->new EntityNotFoundException("member cannot be found"));
        if(chatRoom.getIsGroupChat().equals("N")){
            throw new IllegalArgumentException("단체 채팅방이 아닙니다.");
        }
        ChatParticipant c = chatParticipantRepository.findByChatRoomAndMember(chatRoom, member).orElseThrow(()->new EntityNotFoundException("참여자를 찾을 수 없습니다."));
        chatParticipantRepository.delete(c);

        List<ChatParticipant> chatParticipants = chatParticipantRepository.findByChatRoom(chatRoom);
        if(chatParticipants.isEmpty()){
            chatRoomRepository.delete(chatRoom);
        }
    }

 

채팅방을 떠날때 여기서는  참여자에서 도 제외를 시켜주고 방에 참가자가 아예 없다면 그 방도 삭제를 해 준다.

 

이거를 하기 위해서는 엔티티의 관계가 중요하다. 

@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
public class ChatRoom  extends BaseTimeEntity
{   @OneToMany(mappedBy = "chatRoom", cascade = CascadeType.REMOVE, orphanRemoval = true)
    private List<ChatMessage> chatMessages = new ArrayList<>();
}


@Entity
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
public class ChatMessage extends BaseTimeEntity {

    @OneToMany(mappedBy = "chatMessage", cascade = CascadeType.REMOVE, orphanRemoval = true)
    private List<ReadStatus> readStatuses = new ArrayList<>();
}

 

이렇게 ChatRoom -> ChatMessaage -> ReadStatus  부모 객체가 삭제가 되면 자식 객체도 같이 삭제가 되게 처리를 해 주어야 한다 . 

 

그 밖에도 서비스를 구상 할때 A가 채팅을 칠 때 B가 안 읽고 있으면 읽음 처리가 되는지 

채팅방 목록을 나갔을 때 읽음 처리 여부 등등 이런 것을 화면에서 테스트 해 보아야 한다. 

<template>
    <v-container>
      <v-row justify="center">
        <v-col cols="12" md="8">
          <v-card>
            <v-card-title class="text-center text-h5">
              채팅
            </v-card-title>
            <v-card-text>
              <div class="chat-box">
                <div
                  v-for="(msg, index) in messages"
                  :key="index"
                  :class="['chat-message', msg.senderEmail === senderEmail ? 'sent' : 'received']"
                >
                  <strong>{{ msg.senderEmail }}:</strong> {{ msg.message }}
                </div>
              </div>
              <v-text-field
                v-model="newMessage"
                label="메시지 입력"
                @keyup.enter="sendMessage"
              />
              <v-btn color="primary" block @click="sendMessage">전송</v-btn>
            </v-card-text>
          </v-card>
        </v-col>
      </v-row>
    </v-container>
  </template>
  
  <script>
  import SockJS from 'sockjs-client';
  import Stomp from 'webstomp-client';
  import axios from "axios";

  export default {
    data() {
      return {
        messages: [],
        newMessage: '',
        stompClient: null,
        senderEmail: null,
        roomId: null ,
      };
    },
    async created() {
      this.senderEmail = localStorage.getItem("email");
      this.roomId=this.$route.params.roomId; 
      const response=await axios.get(`${process.env.VUE_APP_API_BASE_URL}/chat/history/${this.roomId}`);
      this.messages=response.data;
      this.connectWebsocket();
    },
    //사용자가 현재 라우트에서 다른 곳으로 이동할때 호출되는함수 
    beforeRouteLeave(to, from, next) {
      this.disconnectWebSocket();
      next(); // ✅ 필수
    },
    // 화면을 완전히 꺼버렸을 때 
    beforeUnmount() {
      this.disconnectWebSocket();
    },
    methods: {
      connectWebsocket() {
        // 이미 연결되어 있으면 제외 
        if (this.stompClient && this.stompClient.connected) return;  
        const socketJs = new SockJS(`${process.env.VUE_APP_API_BASE_URL}/connect`);
        this.stompClient = Stomp.over(socketJs);
        this.token=localStorage.getItem("token");
        this.stompClient.connect( {
            Authorization: `Bearer ${this.token}`
        },
          () => {
            console.log('STOMP 연결 성공');
            // 방번호에 맞게 보냄 
            this.stompClient.subscribe(`/topic/${this.roomId}`, (message) => {
              const parsed = JSON.parse(message.body);
              this.messages.push(parsed);
              this.scrollToBottom();
              // 서버에서 subscribe로 보내겠다. 
            },{Authorization:`Bearer ${this.token}` });
          },
          (error) => {
            console.error('STOMP 연결 실패:', error);
          }
        );
      },
      sendMessage() {
        if (this.newMessage.trim() ==="") return;
        // 메시지를 json으로 바꾸어줌 
        const message = {
          senderEmail: this.senderEmail,
          message: this.newMessage
        };
        // roomId로 정보를 보냄 자바스크립트객체에서 json으로 바꾸어줌 이걸 백에서 dto로 받음 
        this.stompClient.send(`/publish/${this.roomId}`,  JSON.stringify(message));
        this.newMessage = ""
      },
      scrollToBottom() {
        this.$nextTick(() => {
          const chatBox = this.$el.querySelector('.chat-box');
          chatBox.scrollTop = chatBox.scrollHeight;
        });
      },
      async disconnectWebSocket() {
        // 지금까지 메시지는 모두 읽었다. 
        await axios.post(`${process.env.VUE_APP_API_BASE_URL}/chat/room/${this.roomId}/read`);
        if (this.stompClient && this.stompClient.connected) {
          this.stompClient.unsubscribe(`/topic/${this.roomId}`);
          this.stompClient.disconnect();
          console.log('STOMP 연결 해제');
        }
      }
    }
  };
  </script>
  
  <style>
  .chat-box {
    height: 300px;
    overflow-y: auto;
    border: 1px solid #ddd;
    margin-bottom: 10px;
  }
  .chat-message {
    margin-bottom: 10px;
  }
  .sent {
    text-align: right;
  }
  .received {
    text-align: left;
  }
  </style>

 

StompChatPage에서 지금까지 메시지는 모두 읽었다 . 처리를 위해 post를 해준다. 

 

 

실시간 숫자 변화는 (여기서는 한 번 화면을 렌더링을 해 주어야 하지만 ) SSE를 해 주어야 한다. 

 

 

 

1대 1 채팅 

<template>
    <v-container>
        <v-row>
            <v-col>
                <v-card>
                    <v-card-title class="text-center text-h5">
                        회원목록
                    </v-card-title>
                    <v-card-text>
                        <v-table>
                            <thead>
                                <tr>
                                    <th>ID</th>
                                    <th>이름</th>
                                    <th>email</th>
                                    <th>채팅</th>
                                </tr>
                            </thead>
                            <tbody>
                                <tr v-for="member in memberList" :key="member.id">
                                    <td>{{member.id}}</td>
                                    <td>{{member.name}}</td>
                                    <td>{{member.email}}</td>
                                    <td>
                                        <v-btn color="primary" @click="startChat(member.id)">채팅하기</v-btn>
                                    </td>
                                </tr>
                            </tbody>
                        </v-table>
                    </v-card-text>
                </v-card>

            </v-col>

        </v-row>

    </v-container>
</template>

<script>
import axios from 'axios';
export default{
    data(){
        return {
            memberList: []
        }
    },
    async created(){
        const response = await axios.get(`${process.env.VUE_APP_API_BASE_URL}/member/list`);
        this.memberList = response.data;
    },
    methods:{

        async startChat(otherMemberId){
            // 기존의 채팅방이 있으면 return받고, 없으면 새롭게 생성된 roomId return. 
            const response = await axios.post(`${process.env.VUE_APP_API_BASE_URL}/chat/room/private/create?otherMemberId=${otherMemberId}`);
            const roomId = response.data;
            this.$router.push(`/chatpage/${roomId}`);

        }
    }
}
</script>

 

startChat(ID) : 여기에서 ID는 상대방 ID를 이용을 해서  만약  기존의 방이 있으면 return 받고 없으면 새롭게 생성된 roomId 를 반환 받는다. 

 

    public Long getOrCreatePrivateRoom(Long otherMemberId){
        // 나의 채팅방을 찾고
        Member member = memberRepository.findByEmail(SecurityContextHolder.getContext().getAuthentication().getName()).orElseThrow(()->new EntityNotFoundException("member cannot be found"));
        Member otherMember = memberRepository.findById(otherMemberId).orElseThrow(()->new EntityNotFoundException("member cannot be found"));

//        나와 상대방이 1:1채팅에 이미 참석하고 있다면 해당 roomId return
        Optional<ChatRoom> chatRoom = chatParticipantRepository.findExistingPrivateRoom(member.getId(), otherMember.getId());
        if(chatRoom.isPresent()){
            return chatRoom.get().getId();
        }
//        만약에 1:1채팅방이 없을경우 기존 채팅방 개설
        ChatRoom newRoom = ChatRoom.builder()
                .isGroupChat("N")
                // 만든 사람의 ID 랑 상대방입장 ID를 넣어서 방이름을 만들겠다.
                .name(member.getName() + "-" + otherMember.getName())
                .build();
        chatRoomRepository.save(newRoom);
//        두사람 모두 참여자로 새롭게 추가
        addParticipantToRoom(newRoom, member);
        addParticipantToRoom(newRoom, otherMember);

        return newRoom.getId();
    }

 

나와 상대방의 ID를 가지고 방을 찾는다. 여기에 중요한 로직 

상대와 내가 방에 같이 있는 것을 어떻게 알 수 있을까?

이거는 일단 내가 들어간 방을 찾고 --1

상대방이 방에 있는 지 탐색을 하고 --2

그리고 그게 단체 채팅방이 아닌지 찾으면 된다. --3

 

@Repository
public interface ChatParticipantRepository extends JpaRepository<ChatParticipant, Long> {
    List<ChatParticipant> findByChatRoom(ChatRoom chatRoom);
    Optional<ChatParticipant> findByChatRoomAndMember(ChatRoom chatRoom, Member member);
    List<ChatParticipant> findAllByMember(Member member);

    // 단체 채팅방이 아닌경우도 추가를 한다.
    @Query("SELECT cp1.chatRoom FROM ChatParticipant cp1 JOIN ChatParticipant cp2 ON cp1.chatRoom.id = cp2.chatRoom.id WHERE cp1.member.id = :myId AND cp2.member.id = :otherMemberId AND cp1.chatRoom.isGroupChat = 'N'")
    Optional<ChatRoom> findExistingPrivateRoom(@Param("myId") Long myId, @Param("otherMemberId") Long otherMemberId);
}

 

그리고 방이 있다면 들어가고 없다면 이름까지 만들어서 방을 개설하면 된다. 

 

 

 

Redis +Pub/Sub 기능 구현하기 

 

redis 라이브러리 import

@Configuration
public class RedisConfig {
    @Value("${spring.data.redis.host}")
    private String host;

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

//    연결기본객체
    @Bean
    @Qualifier("chatPubSub")
    public RedisConnectionFactory chatPubSubFactory(){
        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
        configuration.setHostName(host);
        configuration.setPort(port);
//        redis pub/sub에서는 특정 데이터베이스에 의존적이지 않음.
//        configuration.setDatabase(0);
        return new LettuceConnectionFactory(configuration);
    }

//    publish객체
    @Bean
    @Qualifier("chatPubSub")
    // publish 객체가 여러개 있을 수 있음
//    일반적으로는  RedisTemplate<key데이터타입, value데이터타입>을 사용 위에 있는 것을 사용을 하겠다.
    public StringRedisTemplate stringRedisTemplate(@Qualifier("chatPubSub") RedisConnectionFactory redisConnectionFactory){
        return  new StringRedisTemplate(redisConnectionFactory);
    }

//    subscribe객체
    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(
            @Qualifier("chatPubSub") RedisConnectionFactory redisConnectionFactory,
            MessageListenerAdapter messageListenerAdapter
    ){
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(redisConnectionFactory);
        // 리슨(subscribe)하고 나서 수신된 메시지를 처리해주는 매서드를 넣는다. redisPubSub 도 특정 주제에 관해 발행 subscribe 한다.
        container.addMessageListener(messageListenerAdapter, new PatternTopic("chat"));
        return container;
    }

//    redis에서 수신된 메시지를 처리하는 객체 생성
    @Bean
    public MessageListenerAdapter messageListenerAdapter(RedisPubSubService redisPubSubService){
//        RedisPubSubService의 특정 메서드가 수신된 메시지를 처리할수 있도록 지정 onMessage 매서드가 나옴
        return new MessageListenerAdapter(redisPubSubService, "onMessage");

    }

 

 

application.yml

data:
  redis:
    host: localhost
    port: 6379

 

이렇게 Qualifier 로 주입 될 Bean 을 지정해준다. 

Redis 에 연결 객체를 만들어 줄때 

host와 port를 만들고 DB까지 셋팅을 해 주지 않는다. 

 

 

publish 객체를 만들때도 이렇게 연결객체 (chatPubSub) 를 주입시켜준다. 

 

subscribe 객체가 메시지를 수신 받으면 밑에 메시지 리스너가 이거를 처리를 해준다. 

/chat 이라는 채널을 통해서 

 

메시지 리스너는 Pub/Sub에 메시즈를 위임을 한다. 

여기서 onMessage 가 처리를 해준다.

 


@Service
public class RedisPubSubService implements MessageListener {

    private final StringRedisTemplate stringRedisTemplate;
    private final SimpMessageSendingOperations messageTemplate;

    public RedisPubSubService(@Qualifier("chatPubSub") StringRedisTemplate stringRedisTemplate, SimpMessageSendingOperations messageTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.messageTemplate = messageTemplate;
    }
    // 메시지 발행 객체가 필요하다.
    public void publish(String channel, String message){
        stringRedisTemplate.convertAndSend(channel, message);
    }

    @Override
    // Message 에 실질적인 메시지가 담겨있다.
//    pattern에는 topic의 이름의 패턴이 담겨있고, 이 패턴을 기반으로 다이나믹한 코딩
    public void onMessage(Message message, byte[] pattern) {
        String payload = new String(message.getBody());
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            // dto 로 바꾸어주는 형변환이 필요하다.
            ChatMessageDto chatMessageDto = objectMapper.readValue(payload, ChatMessageDto.class);
            //room 에 정보를 보내주는 역할이다.
            messageTemplate.convertAndSend("/topic/"+chatMessageDto.getRoomId(), chatMessageDto);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }
}

 

patter에는 topic 패턴을 더 설정을 해줄 수 도 있고 Redis에서 받은 메시지를 서버에게 뿌린다. 

@Controller
public class StompController {

    private final SimpMessageSendingOperations messageTemplate;
    private final ChatService chatService;
    private final RedisPubSubService pubSubService;

    public StompController(SimpMessageSendingOperations messageTemplate, ChatService chatService, RedisPubSubService pubSubService) {
        this.messageTemplate = messageTemplate;
        this.chatService = chatService;
        this.pubSubService = pubSubService;
    }
////    방법1.MessageMapping(수신)과 SenTo(topic에 메시지전달)한꺼번에 처리
//    @MessageMapping("/{roomId}") //클라이언트에서 특정 publish/roomId형태로 메시지를 발행시 MessageMapping 수신
//    @SendTo("/topic/{roomId}")  //해당 roomId에 메시지를 발행하여 구독중인 클라이언트에게 메시지 전송
////    DestinationVariable : @MessageMapping 어노테이션으로 정의된 Websocket Controller 내에서만 사용
//    public String sendMessage(@DestinationVariable Long roomId, String message){
//        System.out.println(message);
//        return  message;
//    }

//    방법2.MessageMapping어노테이션만 활용.
    @MessageMapping("/{roomId}")
    public void sendMessage(@DestinationVariable Long roomId, ChatMessageDto chatMessageReqDto) throws JsonProcessingException {
        System.out.println(chatMessageReqDto.getMessage());
        // 콘솔 출력 DB에 데이터가 저장 특정 룸에 채팅방에 채팅내용(DTO)를 저장
        chatService.saveMessage(roomId, chatMessageReqDto);
        chatMessageReqDto.setRoomId(roomId);
//        messageTemplate.convertAndSend("/topic/"+roomId, chatMessageReqDto);
        ObjectMapper objectMapper = new ObjectMapper();
        String message = objectMapper.writeValueAsString(chatMessageReqDto);

        // String 형태로 바꾸어준다.
        pubSubService.publish("chat", message);
    }
}

 

chat이라는 채널에 message를 주겠다. 

publish 한 메시지를 onMessage 가 받는다. 

   @Override
    // Message 에 실질적인 메시지가 담겨있다.
//    pattern에는 topic의 이름의 패턴이 담겨있고, 이 패턴을 기반으로 다이나믹한 코딩
    public void onMessage(Message message, byte[] pattern) {
        String payload = new String(message.getBody());
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            // dto 로 바꾸어주는 형변환이 필요하다.
            ChatMessageDto chatMessageDto = objectMapper.readValue(payload, ChatMessageDto.class);
            //room 에 정보를 보내주는 역할이다.
            messageTemplate.convertAndSend("/topic/"+chatMessageDto.getRoomId(), chatMessageDto);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

 

그리고 각각의 서버룸에 보내준다.  밑에 있는 것을 이용해서 

SimpMessageSendingOperations는 STOMP 메시지를 클라이언트로 전송하기 위한 인터페이스입니다. 일반적으로 내부 구현체는 SimpMessagingTemplate입니다.

Spring Boot WebSocket에서는 이 객체를 통해 다음과 같이 서버가 **특정 대상(구독자, 특정 유저 등)**에게 STOMP 메시지를 능동적으로(push) 보낼 수 있습니다.

 

local 에서는  front 는 300

'채팅' 카테고리의 다른 글

채팅방 ERD 설계 + 구조  (0) 2025.06.17
STOMP  (0) 2025.06.16
웹소켓 통신  (0) 2025.06.16
WebSocket 통신 (1)  (0) 2025.06.16
AWS 기본 정리(1)  (0) 2025.03.21