hwarrk / hwarrk-back

0 stars 1 forks source link

채팅 기능 구현을 위한 내용 교환 #31

Open suzhanlee opened 3 hours ago

suzhanlee commented 3 hours ago

우선 맨 처음에는 websocket을 생으로 사용하는게 아니라 spring with stomp를 사용해 message가 동일 규격으로 오도록 만들기 위해 stomp를 사용했습니다.

+--------------+ +-------------------+ +----------------+ 클라이언트 <-----> WebSocket 서버 <-----> Spring Security +--------------+ +-------------------+ +----------------+
                                                         +---------+
                                                         | 메시지  |
                                                         | 핸들러  |
                                                         +---------+
                                                            |
                                                            |
                                                        +--------+
                                                        | RabbitMQ |
                                                        +--------+
                                                            |
                                                            |
                                                    +----------------+
                                                    |   구독자(사용자) |
                                                    +----------------+

우선 위와 같은 형식으로 만들어봤습니다.

websocket server 생성하기

 @Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic"); 
        config.setApplicationDestinationPrefixes("/app"); 
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws").withSockJS(); // STOMP 엔드포인트 등록
    }
}
image

topic 으로 보내면 simpleBroker가 내용을 처리하게 되는데, 이를 rabbitMQ로 바꿔서 실행할 수 있습니다. app 의 경우 simpAnnotationMessageBroke 에 보내 채팅의 전송과 받기를 분리할 수 있으나 추후 채팅 내용에 대한 데이터 분석이나 채팅 내용에 대한 검열이 필요 없다면 사용하지 않아도 될 것 같네요

@Controller
public class WebSocketController {

    @MessageMapping("/send")
    @SendTo("/topic/messages")
    public String sendMessage(String message) throws Exception {
        // 메시지를 처리하는 로직 (예: 데이터베이스에 저장 등)
        return message; // 수신자에게 메시지 전송
    }
}

여기서 MessageMapping 를 사용해 messagehandler가 메시지를 받아서 topic path를 기반으로 보내게 해 메시지를 바로 topic의 브로커(rabbitMQ)에게 보내 sub에게 보내게 만들었습니다.

@Document(collection = "user_rooms")
public class UserRoom {
    @Id
    private String id;
    private String userId;
    private String roomId;

    // 생성자, Getter, Setter
}

room 의 경우 위와 같이 만들어 사용자가 어떤 방에 참여했는지 알 수 있도록 했습니다.

public class Message {
    private String roomId;
    private String sender;
    private String content;
    private LocalDateTime timestamp;

    // 생성자, Getter, Setter
}

message도 nosql에 저장하도록 하면 될 것 같네요

@RestController
@RequestMapping("/chat")
public class MessageHistoryController {

    private final MongoTemplate mongoTemplate;

    public MessageHistoryController(MongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
    }

    @GetMapping("/history/{roomId}")
    public List<Message> getChatHistory(@PathVariable String roomId) {
        Query query = new Query();
        query.addCriteria(Criteria.where("roomId").is(roomId));
        query.with(Sort.by(Sort.Direction.ASC, "timestamp"));  
        return mongoTemplate.find(query, Message.class, "messages");
    }
}

이런 식을 하면 메시지 내역도 가져올 수 있을 것 같아요

@RestController
@RequestMapping("/chat")
public class UserRoomController {

    private final UserRoomRepository userRoomRepository;

    public UserRoomController(UserRoomRepository userRoomRepository) {
        this.userRoomRepository = userRoomRepository;
    }

    @PostMapping("/room/{roomId}/join")
    public ResponseEntity<?> joinRoom(@PathVariable String roomId, @RequestBody String userId) {
        if (!userRoomRepository.findByUserIdAndRoomId(userId, roomId).isPresent()) {
            UserRoom userRoom = new UserRoom();
            userRoom.setRoomId(roomId);
            userRoom.setUserId(userId);
            userRoomRepository.save(userRoom);
        }
        return ResponseEntity.ok("User joined the room");
    }

    @GetMapping("/room/{roomId}/participants")
    public List<String> getParticipants(@PathVariable String roomId) {
        List<UserRoom> userRooms = userRoomRepository.findByRoomId(roomId);
        return userRooms.stream().map(UserRoom::getUserId).collect(Collectors.toList());
    }
}

이러면 방에 사용자가 있는지도 볼 수 있어 보이네요

@PostMapping("/room/{roomId}/leave")
public ResponseEntity<?> leaveRoom(@PathVariable String roomId, @RequestBody String userId) {
    userRoomRepository.findByUserIdAndRoomId(userId, roomId)
        .ifPresent(userRoom -> userRoomRepository.delete(userRoom));
    return ResponseEntity.ok("User left the room");
}

삭제도 가능해보입니다.

이를 통해 방 만들기, 방 삭제, 내 방 목록 조회... 모두 가능해보이고 실제 채팅을 보내면 방에 포함된 사람들이 pub-sub 패턴으로 해당 방으로 알람이 갈테니 이를 event로 react에서 받아서 숫자(빨간 숫자-> 메시지 갯수)를 표기 해주는게 가능해 보이네요! -> React는 RabbitMQ를 통해 전송된 알림을 이벤트로 받아 UI에 채팅 알림 숫자를 표시!

kafka 대신 rabbitMQ를 사용한 이유는 rabbitMQ가 낮은 레이턴시를 가지는데 비해 kafka의 경우, 대규모 스트리밍 작업에 특화되어 있지만, 로깅이나 추가적인 채팅에 대한 분석이 필요한게 아니라면 사용자에게는 낮은 레이턴시가 더 좋아 보여 이렇게 적용해봤습니다.

lsh2613 commented 3 hours ago

엄청 빨리 정리하셨군요... simpleBroker 대신 rabbitMQ를 사용하여 외부 메시지 브로커를 사용한다는 말씀이신 거죠? 추가로 message에 isRead 컬럼을 통해 읽음 표시(0, 1), 안 읽은 대화 갯수 표시 등을 구할 수 있을 것 같아요

suzhanlee commented 3 hours ago

오 isRead 방식 되게 좋은거 같네요 넵 외부 브로커로 생각하고 있었습니다.