채팅방 이름에 따라 페이지네이션을 구분하는 로직을 작성하는 과정에서 고민이 발생했습니다.
과연 채팅이 어마무시하게 증가한다면 단일 컬랙션에서 채팅방 이름과 ID 값으로 커서 기반 페이지네이션을 적용하는 것이 합당할까? 다른 채팅방의 데이터 정보까지 하나의 컬랙션에서 다 저장하는 것이 옳은 방안인가?
에 대한 고민이 있었습니다.
기존 채팅 데이터 저장 로직은 아래와 같았습니다.
단일 컬랙션에 채팅 데이터를 저장하는 방식을 활용하다보니 채팅방에 들어가 이전 채팅을 조회하는 과정에서 데이터의 양이 많아질수록 쿼리가 지연될 수 있다는 문제점을 확인할 수 있었습니다.
결국 채팅을 저장하는 과정에서 단일 컬랙션을 사용한 점이 문제인 것 같다는 생각이 주를 이뤘고, 채팅방 별로 컬랙션을 분리하는 방식을 채택했습니다.
ChatMessage
기존 ChatMessage
는 채팅방 ID를 저장하기 위한 String chatRoomId
필드가 포함되어 있었지만 컬랙션을 분리함에 따라 필드를 삭제했습니다.
@Getter
@Document
public class ChatMessage {
@Setter
private Long id;
private ChatMessageType messageType;
private Long senderId;
private String message;
private LocalDateTime createdAt;
@Builder
private ChatMessage(Long id, ChatMessageType messageType, Long senderId, String message) {
this.id = id;
this.messageType = messageType;
this.senderId = senderId;
this.message = message;
this.createdAt = LocalDateTime.now();
}
}
ChatRepository
MongoTemplate
을 활용해서 채팅 데이터를 저장 및 조회하는 역할을 맞고 있습니다.createCollection()
메서드를 통해 새로운 채팅방이 생성된 경우 컬랙션을 함께 생성해줍니다.findAllByCollection()
메서드를 통해 컬랙션별 채팅 데이터에 대한 페이지네이션을 적용하고 이에 따른 결과를 반환해줍니다.findLastChatByCollection()
메서드를 통해 채팅방의 가장 최근 채팅을 확인할 수 있습니다.@Repository
@RequiredArgsConstructor
public class ChatRepository {
private final MongoTemplate mongoTemplate;
public void createCollection(String collectionName) {
if (!mongoTemplate.collectionExists(collectionName)) {
mongoTemplate.createCollection(collectionName);
}
}
public ChatMessage save(ChatMessage chatMessage, String collectionName) {
return mongoTemplate.save(chatMessage, collectionName);
}
public CursorPaginationResult<ChatDataApiRes> findAllByCollection(String collectionName,
CursorPaginationInfoReq pageable) {
Query query = new Query();
query.limit(pageable.getPageSize()).addCriteria(cursorIdCondition(pageable.getCursorId()))
.with(Sort.by(Sort.Order.desc("createdAt")));
var result = mongoTemplate.find(query, ChatMessage.class, collectionName).stream()
.map(ChatDataApiRes::from).toList();
return CursorPaginationResult.fromDataWithHasNext(result, pageable.getPageSize(),
hasNext(pageable.getCursorId(), pageable.getPageSize(), collectionName));
}
public Optional<ChatMessage> findLastChatByCollection(String collectionName) {
Query query = new Query();
query.limit(1).with(Sort.by(Sort.Order.desc("createdAt")));
return Optional.ofNullable(mongoTemplate.findOne(query, ChatMessage.class, collectionName));
}
private Criteria cursorIdCondition(Long cursorId) {
return cursorId != null ? Criteria.where("id").lt(cursorId) : Criteria.where("id").gt(0L);
}
private boolean hasNext(Long cursorId, int pageSize, String collectionName) {
return cursorId == null ? mongoTemplate.estimatedCount(collectionName) > pageSize
: mongoTemplate.count(new Query(Criteria.where("id").lt(cursorId)), ChatMessage.class,
collectionName) > pageSize;
}
}
채팅 데이터 저장 로직은 아래와 같이 변경되었습니다.
기존 단일 컬랙션을 사용한 경우와 개별 컬랙션을 사용한 방식의 성능 비교를 진행했을 때, 5000개의 더미 채팅 데이터 기준 160 ms 에서 → 130 ms로 약 18%의 성능 향상이 이루어졌습니다.