Dev_Henry

[Spring] 소켓통신 이용한 채팅 구현하기 5 - 채팅내용 DB저장 본문

Web/Spring

[Spring] 소켓통신 이용한 채팅 구현하기 5 - 채팅내용 DB저장

데브헨리 2023. 7. 21. 16:04
728x90

 

채팅목록 관리는 기존 사용하던 방식 그대로 Spring Data JPA와 mysql을 사용했다.

 

채팅내용을 관리하는 부분에서 어떤식으로 구현을 할지 고민을 많이했다.

처음에는 서버에 저장하지않고 클라이언트에만 저장하려 했으나, 어플이 꺼져있는 동안(소켓연결이 끊겨있는 동안) 온 메시지를 접속시 보여주려면 메시지큐를 다루어야 할것같은데 이 부분이 어렵기도하고 안정성이나 관리 측면을 생각해서 서버에 저장하려고 한다.

하지만 채팅 특성상 채팅데이터가 여러가지 형태로 올 수 있고, 삽입과 조회가 빈번하게 일어나기 때문에 RDB를 사용하면 성능이 떨어질듯 해서 사용해본적 없는 nosql을 써보기로 결정했다.

 

  • 의존성 추가
	implementation ('org.springframework.boot:spring-boot-starter-data-mongodb')
 
  • 프로퍼티 설정
spring.data.mongodb.host=${MONGODB_HOST}
spring.data.mongodb.port=${MONGODB_PORT}
spring.data.mongodb.username=${MONGODB_USER}
spring.data.mongodb.password=${MONGODB_PWD}
spring.data.mongodb.database=${MONGODB_DB}
spring.data.mongodb.authentication-database=${MONGODB_AUTH_DB}
 
  • mongoConfig ( 선택 )
@Configuration
public class MongoDBConfig {
    @Autowired
    private MongoMappingContext mongoMappingContext;

    @Bean
    public MappingMongoConverter mappingMongoConverter(
            MongoDatabaseFactory mongoDatabaseFactory,
            MongoMappingContext mongoMappingContext
    ) {
        DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDatabaseFactory);
        MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mongoMappingContext);
        converter.setTypeMapper(new DefaultMongoTypeMapper(null));
        return converter;
    }
}
 

위 설정파일은 없어도 동작에는 문제가 없지만 저장할때마다 _class값을 함께 저장한다. 없애고 싶다면 위와 같이 설정해주면 저장하지 않는다.

 


 

  • Chat
@Getter
@Setter
@Document(collation = "chat") //mongoDB
public class Chat {
    @Id
    private String id;
    private ChatDTO.MessageType type; // 메시지 타입
    private String roomId; // 방 번호
    private String sender; // 채팅을 보낸 사람
    private String message; // 메시지
    private String time; // 채팅 발송 시간간
}
 

RDB를 사용할때는 @Entity를 사용했지만 MongoDB를 사용하기 위해서는 @Document어노테이션에 컬렉션 명을 적어준다.

 

  • repository
public interface ChatRepository extends MongoRepository<Chat,String> {
    public List<Chat> findAllByRoomIdAndTimeAfter(String roomId, LocalDateTime time);
}
 

JPA와 같은 ORM은 RDB를 위한 기술이기 때문에 nosql에는 사용할 수 없다. 하지만 스프링에서는 Spring-Data-mongodb를 제공해주어 mongoDB도 편리하게 쓸 수 있게 도와준다.

findAllByRoomIdAndTimeAfter메서드의 경우 채팅방id와 시간(입장시간)으로 입장 이후에 온 메시지를 모두 불러오는 기능인데, 쿼리를 직접 적을 필요 없이 spring-data에서 제공하는 쿼리메소드 기능으로 편리하게 사용할 수 있다.

 

 

  • chatRoom
public class ChatRoom {
    @Id
    private String roomId; // 채팅방 아이디
    private String roomName; // 채팅방 이름
    private long userCount; // 채팅방 인원수

    public void upUserCount(){
        this.userCount++;
    }
    public void downUserCount(){
        this.userCount--;
    }
}
 

기존에 chatRoom 엔티티에 유저목록을 함께 가지고 있었지만 채팅방,유저 별로 각각 입장시간도 함께 저장하기 위해서 유저목록을 분리했다.

 

  • chatRoomMember
public class ChatRoomMember {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @ManyToOne
    private ChatRoom chatRoom;
    @ManyToOne
    private Member member;

    private LocalDateTime enterTime;

    public ChatRoomMember(ChatRoom chatRoom,Member member,LocalDateTime time){
        this.chatRoom = chatRoom;
        this.member = member;
        this.enterTime = time;
    }
}
 

 

  • chatRoomController
@RequestMapping("/chatroom")
public class ChatRoomController {
    private final ChatService chatService;

    // 채팅방 목록 조회
    @GetMapping("/")
    public ResponseEntity<ResultResponse> chatRoomList(){
        List<ChatRoomDTO> chatRoomDTOS = chatService.chatRoomList();
        return ResponseEntity.ok(ResultResponse.of(GET_CHATROOMLIST_SUCCESS, chatRoomDTOS));
    }

    // 채팅방 생성
    @PostMapping("/")
    public ResponseEntity<ResultResponse> createRoom(@RequestParam String name) {
        ChatRoomDTO room = chatService.createRoom(name);
        return ResponseEntity.ok(ResultResponse.of(CREATE_CHATROOM_SUCCESS,room.getRoomId()));
    }

    // 채팅방 나가기
    @DeleteMapping("/")
    public ResponseEntity<ResultResponse> leaveRoom(@RequestParam String roomId) {
        chatService.leaveRoom(roomId);
        return ResponseEntity.ok(ResultResponse.of(LEAVE_ROOM_SUCCESS));
    }


    // 채팅방 채팅내용 불러오기 (방 열기)
    @GetMapping("/chat")
    public ResponseEntity<ResultResponse> getChatList(String roomId){
        List<ChatDTO> chats = chatService.getChatList(roomId);
        return ResponseEntity.ok(ResultResponse.of(GET_CHATLIST_SUCCESS,chats));
    }


    // 채팅에 참여한 유저 리스트 반환
    @GetMapping("/userlist")
    public ResponseEntity<ResultResponse> userList(String roomId) {
        List<String> userList = chatService.roomMemberList(roomId);
        return ResponseEntity.ok(ResultResponse.of(GET_CHAT_USERLIST_SUCCESS,userList));
    }
}
 

채팅방 목록 조회,생성,퇴장 등을 만들었다. 이미 입장한 채팅방을 열때 채팅내용을 디비에서 불러올 수 있어 소켓연결이 끊긴 상태에서 온 메시지도 확인가능하도록 만들었다. (캐싱이나 기타 방식으로 저장해두고 새로운 채팅만 가져오면 더 빠르게 동작할 수 있을듯. 추후 찾아보자)

 

  • chatController
@RestController
public class ChatController {

.
.

    // /pub/chat.message.{roomId} 로 요청하면 브로커를 통해 처리
    // /exchange/chat.exchange/room.{roomId} 를 구독한 클라이언트에 메시지가 전송된다.
    @MessageMapping("chat.enter.{chatRoomId}")
    public void enterUser(@Payload ChatDTO chat, @DestinationVariable String chatRoomId) {

        // 채팅방에 유저 추가
        chatService.enterRoom(chat.getRoomId(), chat.getSender());

        chat.setTime(LocalDateTime.now());
        chat.setMessage(chat.getSender() + " 님 입장!!");
        rabbitTemplate.convertAndSend(CHAT_EXCHANGE_NAME, "room." + chatRoomId, chat);

    }

    @MessageMapping("chat.message.{chatRoomId}")
    public void sendMessage(@Payload ChatDTO chat, @DestinationVariable String chatRoomId) {
        log.info("CHAT {}", chat);
        chat.setTime(LocalDateTime.now());
        chat.setMessage(chat.getMessage());
        rabbitTemplate.convertAndSend(CHAT_EXCHANGE_NAME, "room." + chatRoomId, chat);

    }

    //기본적으로 chat.queue가 exchange에 바인딩 되어있기 때문에 모든 메시지 처리
    @RabbitListener(queues = CHAT_QUEUE_NAME)
    public void receive(ChatDTO chatDTO){
        log.info("received : " + chatDTO.getMessage());
        Chat chat = rootConfig.getMapper().map(chatDTO,Chat.class);
        chatRepository.save(chat);
    }
}
 

채팅방 입장처리는 "chat.enter.{chatRoomId}"경로로 메시지를 받았을때 처리하도록 했고,

기본으로 미리 만들어둔 큐에서 @rabbitListener를 이용해 모든 메시지를 받아서 db에 저장하도록 했다.

 

 

  • chatService
@Service
public class ChatServiceImpl implements ChatService{

.
.

    @Override
    @Transactional
    public void enterRoom(String roomId,String memberId) {
        ChatRoom chatRoom = chatRoomRepository.findById(roomId).orElseThrow(() -> new EntityNotFoundException(CHATROOM_NOT_FOUND));
        Member member = memberRepository.findByMemberId(memberId).orElseThrow();
        //Member member = accountUtil.getLoginMember();
        chatRoom.upUserCount();
        if(chatRoomMemberRepository.existsByChatRoomAndMember(chatRoom,member)){
            throw new BusinessException(ALREADY_ENTER_ROOM);
        }
        ChatRoomMember chatRoomMember = new ChatRoomMember(chatRoom,member, LocalDateTime.now());
        chatRoomMemberRepository.save(chatRoomMember);
    }

    @Override
    @Transactional
    public void leaveRoom(String roomId) {
        ChatRoom chatRoom = chatRoomRepository.findById(roomId).orElseThrow(() -> new EntityNotFoundException(CHATROOM_NOT_FOUND));
        Member member = accountUtil.getLoginMember();
        chatRoom.downUserCount();
        ChatRoomMember chatRoomMember = chatRoomMemberRepository.findByChatRoomAndMember(chatRoom,member).orElseThrow(()->new EntityNotFoundException(CHATROOM_MEMBER_NOT_FOUND));
        chatRoomMemberRepository.delete(chatRoomMember);
    }

    @Override
    public List<ChatDTO> getChatList(String roomId) {
        Member member = accountUtil.getLoginMember();
        ChatRoom chatRoom = chatRoomRepository.findById(roomId).orElseThrow(() -> new EntityNotFoundException(CHATROOM_NOT_FOUND));
        ChatRoomMember chatRoomMember = chatRoomMemberRepository.findByChatRoomAndMember(chatRoom,member).orElseThrow(()->new EntityNotFoundException(CHATROOM_MEMBER_NOT_FOUND));
        List<Chat> chats = chatRepository.findAllByRoomIdAndTimeAfter(roomId,chatRoomMember.getEnterTime());
        List<ChatDTO> chatDTOS = chats.stream().map(chat -> rootConfig.getMapper().map(chat, ChatDTO.class)).collect(Collectors.toList());
        return chatDTOS;
    }
}
 

 


결과

 

입장

DB저장 결과

 

 

728x90
반응형