채팅을 보내려는 유저를 A라고 하고, 채팅을 받는 유저를 B라고 가정해보자.
나는 유저 A가 유저 B에게 채팅을 보낼 때 여러개의 상황에 맞게 분기 처리가 되어서, 채팅방에 신규 입장시 환영 메세지가 출력되고, 이미 입장을 했던 유저라면 일반적인 메세지를 전송하는 로직을 생각해보았다.
첫번째로 유저 A와 유저 B가 처음으로 채팅을 하는 상황이다.
이 경우에는 유저 A와 유저 B간의 채팅방이 존재하지 않기 때문에, 채팅시 채팅방을 생성할 필요가 존재한다.
또한 환영 메세지를 유저 B에게 보내주어야 한다.
두번째로 유저 A와 유저 B가 기존에 채팅을 했어서 채팅방이 존재하는 경우이다.
이 경우에는 유저 A와 유저 B간의 채팅방이 이미 존재하기 때문에, 채팅시 채팅방을 생성할 필요가 없다.
이 경우 환영 메세지를 보내줄 필요 없이, 메세지만 브로커를 통해 유저 B에게 전달하면 된다.
내가 어떻게 위의 기능을 구현했는지 코드와 함께 더 자세히 알아보도록 하자.
해당 글은 Spring Message + Stomp를 기본적으로 인지하고 있다는 전제하에 작성되었다.
ChatRoom.class
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Builder
public class ChatRoom extends CommonEntity {
private String roomName;
private String lastReceivedChat;
@ElementCollection
@CollectionTable(
name = "visitedNames",
joinColumns = @JoinColumn(name = "chatRoom_id")
)
@Builder.Default
private List<String> visitedNames = new ArrayList<>();
public void setRandomRoomName(){
this.roomName = UUID.randomUUID().toString();
}
public void updateLastRecievedChat(String lastReceivedChat){
this.lastReceivedChat = lastReceivedChat;
}
}
visiteNames
ArrayList 필드와 같은 경우에는 해당 채팅방에 한번이라도 접근한 유저를 저장하는 ArrayList이다.roomName
같은 경우에는 UUID를 이용해서, 랜덤으로 값을 생성해준다.ChatMessage.class
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Builder
public class ChatMessage extends CommonEntity {
public enum MessageType {
ENTER, TALK
}
private MessageType type;
private String roomName;
private String senderName;
private String message;
public static ChatMessage to(ChatMessageRequestDTO requestDTO){
ChatMessage chatMessage = ChatMessage.builder()
.message(requestDTO.getMessage())
.senderName(requestDTO.getSenderName())
.roomName(requestDTO.getRoomName())
.build();
return chatMessage;
}
public void updateSenderName(String username){
this.senderName = username;
}
public void updateStatus(MessageType type){ this.type = type; }
public void updateRoomName(String roomName){
this.roomName = roomName;
}
}
MessageType type
필드는 입장 메세지와 일반 채팅을 구분하기 위한 Enum 필드이다.Repository 같은 경우에는 일반적인 쿼리만 있기에, 비즈니스 로직부터 알아보기로 하자.
MessageService.class
@Service
@Transactional
@RequiredArgsConstructor
public class MessageService {
private final MessageRepository messageRepository;
private final ChatRoomRespository roomRepository;
@Transactional
public ChatMessageReponseDTO.ChatDTO saveMessage(ChatMessageRequestDTO messageDTO, SimpMessageHeaderAccessor headerAccessor) {
ChatMessage message = ChatMessage.to(messageDTO);
String userName = Objects.requireNonNull(headerAccessor.getUser()).getName();
// 해당 roomName의 room이 있는지
ChatRoom chatRoom = findRoom(message.getRoomName());
// 있다면 message 저장
if (chatRoom != null) {
// 재입장인지 아닌지 분기 처리
for (String s : chatRoom.getVisitedNames()) {
// 재입장이라면
if (s.equals(userName)) {
return ChatMessageReponseDTO.from(ifUserReEnter(chatRoom, message, userName));
}
}
// 재입장이 아니더라도
return ChatMessageReponseDTO.from(ifUserNotReEnterExistRoom(chatRoom, message, userName));
}
// 맨 처음 입장이라면 -> Room 생성 후 메시지 생성
return ChatMessageReponseDTO.from(ifUserFirstEnter(message, userName));
}
// 재입장이라면
private ChatMessage ifUserReEnter(ChatRoom chatRoom, ChatMessage message, String userName){
chatRoom.updateLastRecievedChat(message.getMessage());
message.updateStatus(ChatMessage.MessageType.TALK);
message.updateSenderName(userName);
message.updateRoomName(chatRoom.getRoomName());
return messageRepository.save(message);
}
// 재입장은 아니되 이미 방이 존재할 때
private ChatMessage ifUserNotReEnterExistRoom(ChatRoom chatRoom, ChatMessage message, String userName){
chatRoom.getVisitedNames().add(userName);
chatRoom.updateLastRecievedChat(message.getMessage());
message.updateStatus(ChatMessage.MessageType.ENTER);
message.updateSenderName(userName);
message.updateRoomName(chatRoom.getRoomName());
return messageRepository.save(message);
}
// 맨 처음 입장이라면
private ChatMessage ifUserFirstEnter(ChatMessage message, String userName){
ChatRoom chatRoomOP = createRoom(message);
chatRoomOP.getVisitedNames().add(userName);
chatRoomOP.setRandomRoomName();
chatRoomOP.updateLastRecievedChat(message.getMessage());
message.updateStatus(ChatMessage.MessageType.ENTER);
message.updateSenderName(userName);
message.updateRoomName(chatRoomOP.getRoomName());
return messageRepository.save(message);
}
// Room 생성
private ChatRoom createRoom(ChatMessage message){
ChatRoom chatRoom =
ChatRoom.builder()
.lastReceivedChat(message.getMessage()).build();
return roomRepository.save(chatRoom);
}
// Room select 쿼리
private ChatRoom findRoom(String roomName) {
Optional<ChatRoom> room = roomRepository.findByRoomName(roomName);
return room.orElse(null);
}
}
createRoom
메서드를 이용해서 Room을 생성 및 Message를 저장해준다.MessageController.class
@RestController
@RequiredArgsConstructor
@Slf4j
public class MessageController {
private final SimpMessagingTemplate template;
private final MessageService messageService;
@MessageMapping("/chat/message")
public void enter(ChatMessageRequestDTO messageDTO, SimpMessageHeaderAccessor headerAccessor) {
ChatMessageReponseDTO.ChatDTO chatMessage = messageService.saveMessage(messageDTO, headerAccessor);
if (Objects.equals(chatMessage.getType(), "ENTER")) {
template.convertAndSend("/sub/chat/room/" + chatMessage.getRoomName(), chatMessage.getSenderName() + "님이 채팅방에 입장하셨습니다.");
}
if (Objects.equals(chatMessage.getType(), "TALK")) {
template.convertAndSend("/sub/chat/room/" + chatMessage.getRoomName(), chatMessage);
}
}
}
convertAndSend()
이다. 나머지 로직은 해당 메세지를 어떻게 처리하고, 가공 및 저장해야할 지에 대한 로직이다.@MessageMapping(”/chat/message”)
어노테이션을 통해서 해당 enter
메서드로 매핑시켜준다.messageService.saveMessage()
메서드를 통해 Message를 가공 및 최초 입장 및 일반 채팅 여부를 저장해서 DTO 객체에 저장한다.
글 잘 봤습니다.