채팅목록 관리는 기존 사용하던 방식 그대로 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저장 결과
'Web > Spring' 카테고리의 다른 글
[Spring] JWT refreshToken에 대한 고민, 사용하기(Redis) (0) | 2023.07.25 |
---|---|
jwt토큰 관련 리팩토링 (0) | 2023.07.25 |
[Spring] 소켓통신 이용한 채팅 구현하기 4 - 채팅방 DB저장 (0) | 2023.07.21 |
[Spring] 소켓통신 이용한 채팅 구현하기 3 - RabbitMQ 연동 (2) | 2023.07.21 |
[Spring] 소켓통신 이용한 채팅 구현하기 2 - STOMP (0) | 2023.07.21 |