티스토리 뷰
이전 포스트에서 STOMP를 이용해 웹소켓 실시간 채팅을 구현해 보았습니다.
실시간 채팅은 되지만 채팅방에 나갔다 들어오면 저장된 기록이 없어 기존 채팅 내역이 리셋됩니다.
이를 해결하기 위해 채팅 내역을 저장하고 보여주는 Service를 개발하고 채팅 성능의 효율과 속도를 높이는 과정을 여러 포스트를 거쳐 기록하려 합니다.
제가 하는게 정답이 아니니 참고만 해주세요.
먼저 이전 코드들을 MVC패턴에 맞게 리팩토링을 해봅시다.
기존 Controller
@RestController
@Slf4j
@RequiredArgsConstructor
public class MessageController {
private final MessageService messageService;
private final SimpMessageSendingOperations sendingOperations;
@MessageMapping("/message")
public void enter(SendMessage message) {
if (Message.MessageType.ENTER.equals(message.getType())) {
message.setDetailMessage(message.getSenderId()+"님이 입장하였습니다.");
//DB저장
messageService.sendMessage(Message.MessageType.ENTER,message.roomId, message.detailMessage, message.senderId);
}
//DB저장
else messageService.sendMessage(Message.MessageType.TALK,message.roomId, message.detailMessage, message.senderId);
log.info("roomID = {}", message.getRoomId());
sendingOperations.convertAndSend("/topic/chat/room/"+message.getRoomId(),message);
}
@Data
private static class SendMessage{
Message.MessageType type;
Long roomId;
String detailMessage;
Long senderId;
}
}
기존 Controller는 기능구현 성공에만 초점을 맞춰서 비지니스 로직도 섞여있고 DTO도 따로 분리안하고 코드 중복도 있고, 맘에 안듭니다.
Service로 뺄건 빼고 최종경로만 남게끔 수정합니다.
변경 후
@RestController
@Slf4j
@RequiredArgsConstructor
public class MessageController {
private final MessageService messageService;
private final SimpMessageSendingOperations sendingOperations;
@MessageMapping("/message")
public void sendMessage(MessageDto message) {
MessageDto messageDto = messageService.messageType(message);
sendingOperations.convertAndSend("/topic/chat/room/"+messageDto.getRoomId(),message);
}
}
좋네요!
이제 채팅이 오면 DB에 저장하는 Service로직을 살펴보겠습니다.
Service
@Service
@Transactional
@RequiredArgsConstructor
public class MessageService {
private final MessageRepository messageRepository;
public MessageDto messageType(MessageDto message) {
if (Message.MessageType.ENTER.equals(message.getType())) {
message.setDetailMessage(message.getSenderId()+"님이 입장하였습니다.");
}
saveMessage(message.getType(), message.getRoomId(), message.getDetailMessage(), message.getSenderId());
return message;
}
public void saveMessage(Message.MessageType type, Long roomId, String detailMessage, Long senderId) {
Message message = new Message(Message.MessageType.TALK, roomId, senderId, detailMessage);
messageRepository.save(message);
}
}
컨트롤러에서 채팅을 받으면 서비스에 채팅 정보가 넘어오고 DB에 commit하는 아주 기본적인 로직입니다.
문제점
현재 한줄의 INSERT문이 하나의 트랜잭션 단위로 묶여지게 되어 들어오는 채팅 수 만큼 트랜잭션이 생성됩니다.
문제는 채팅의 특성상 서버호출이 잦고 사용자가 많아질 수록 채팅의 양도 기하급수적으로 많아지는데 있습니다.
데이터베이스 연결의 생애주기는 아래와 같습니다.
- 데이터베이스 드라이버를 사용하여 데이터베이스 연결 열기
- 데이터를 읽고 쓰기 위해 TCP 소켓 열기
- TCP 소켓을 사용하여 데이터 통신
- 데이터베이스 연결 닫기
- TCP 소켓 닫기
위와 같이 데이터베이스 연결을 수립하고, 해제하는 과정은 비용이 많이 들어가는 작업이라 모든 요청마다 데이터베이스 커넥션을 맺으면 통신량 과부하가 올 것입니다.
해결법
해결법은 단순합니다. 요청이 오면 차례로 담아 두었다가 일정 크기가 되면 하나의 트랜잭션으로 묶어 DB로 보내면 됩니다.
이를 위해서 Queue 와 JPA 지연 쓰기를 사용하겠습니다.
순서
- 채팅이 들어오면 Message객체를 Queue에 add
- 설정한 QueueSize가 되면 Queue에서 poll
- poll 되어 나온 Message객체를 JPA 1차 캐시에 저장
- 1차 캐시에 임시로 저장된 쿼리문을 하나의 트랜잭션으로 묶어 commit
@Service
@Transactional
@RequiredArgsConstructor
public class MessageService {
private final MessageRepository messageRepository;
private static final Queue<Message> messageQueue = new LinkedList<>();
private final EntityManager em;
private static final int messageQueueSize = 5;
public MessageDto messageType(MessageDto message) {
if (Message.MessageType.ENTER.equals(message.getType())) {
message.setDetailMessage(message.getSenderId()+"님이 입장하였습니다.");
}
saveMessage(message.getType(), message.getRoomId(), message.getDetailMessage(), message.getSenderId());
return message;
}
public void saveMessage(Message.MessageType type, Long roomId, String detailMessage, Long senderId) {
Message message = new Message(Message.MessageType.TALK, roomId, senderId, detailMessage);
messageQueue.add(message);
if(messageQueue.size() == messageQueueSize) commitMessageQueue();
}
public void commitMessageQueue() {
//쓰기 지연
for (int i = 0; i < messageQueueSize; i++) {
Message message = messageQueue.poll();
em.persist(message);
}
em.flush();
}
}
Queue 의 FIFO성질에 의해 메세지의 순서가 보장되고 JPA 영속성 컨텍스트 쓰기 지연으로 한번의 커넥션으로 처리 가능합니다.
문제점
음.... 꽤나 쉽게 해결된 것 같긴 하나 이 방법은 치명적인 문제점이 있습니다.
- 서버 내 메모리에 큐를 저장하는 방식이라 메모리 용량의 한계가 있다.
- 위의 이유로 서버 하나에 저장하는 방식이라 여러 서버를 Scale-Out 할 경우 다른 서버와 공유가 불가능하다.
- 큐사이즈만큼 요청이 들어와야 DB에 저장된다.
만약 채팅을 치고 채팅방을 나갔을 때 큐가 다 채워지지 않았다면, 다시 들어올 경우 채팅방에 남아있던 유저는 기존 채팅이 정상적으로 보이고 다시 들어온 유저는 DB에 저장된 채팅만 보이고 큐에 있는 메시지를 확인 할 방법이 없어 두 유저의 화면은 불일치하게 됩니다.
사용자가 많으면 큐가 금방 쌓이니 괜찮지 않나? 라는 안일한 생각도 해보았는데 아닌 것 같습니다.
좀 더 깊은 고민을 해볼 필요가 있습니다..........;
생각 좀 하고 다음 포스트로 다시 오겠습니다!
'Spring > 채팅앱' 카테고리의 다른 글
[Spring 채팅앱 성능 개선기 4] Redis Cache 적용 (0) | 2023.07.13 |
---|---|
[Spring 채팅앱 성능 개선기 3] Cache 설계 전략 (0) | 2023.07.06 |
[Spring 채팅앱 성능개선기 2] Cache를 이용한 속도향상 (0) | 2023.07.04 |
[Spring+Stomp] Stomp를 활용한 웹소켓 구현 (0) | 2023.07.01 |
채팅 ERD 설계 (0) | 2023.06.30 |
- Total
- Today
- Yesterday
- Stomp Kafka
- spring orphan
- Flutter
- 로컬캐시
- FCM
- 게시판 채팅
- Spring RabbitMQ
- ERD설계
- Kubernetes
- ChattingApp
- authorization
- Spring WebSocket
- Spring 대댓글
- Bcypt
- Vmmem종료
- Security
- Spring 채팅
- Spring Stomp
- Authentication
- Vmmem
- Cache
- bcrypt
- 웹소켓 채팅
- Spring
- MessageBroker
- 푸시알림동작원리
- springboot
- loadbalancing
- Stomp RabbitMQ
- nativeQuery
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |