Spring boot와 MongoDB를 이용해 채팅방을 구현하는데,
채팅방 목록을 구하는 API를 작성하려고 했다.
아래와 같은 정보 (채팅방 번호, 마지막 채팅 메세지, 안 읽은 메세지, 마지막 채팅시간) 을 가져오려고 한다.
근데, MongoDB가 처음이라 일단 만들면서 배우는거라 여기 깨지고 알아보는데 sum에 대한건 별로 없어서 고생 좀 해서 메모를 해본다!
우선 sum에 대한 공식문서는 아래에서 볼 수 있다.
https://www.mongodb.com/docs/v4.2/reference/operator/aggregation/sum/
$sum (aggregation) — MongoDB Manual
Array Operand In the $group stage, if the expression resolves to an array, $sum treats the operand as a non-numerical value. In the other supported stages: With a single expression as its operand, if the expression resolves to an array, $sum traverses into
www.mongodb.com
명령어 자체에서 볼 수 있듯이 sum은 결과를 더해서 반환해준다.
안 읽은 메세지를 처리하기 위해서는 ChatMessage에서 읽지 않고, 받은 회원의 번호가 '나'이고, isRead(읽음유무)가 false여야 한다.
sum에서도 표현식(expression)을 사용할 수 있기 때문에 조건문을 사용할 수있다.
조건문은 $cond를 통해 사용할 수 있다.
{
$cond: {
if: <boolean-expression>, then: <true-case>, else: <false-case>
}
}
// 또는
{
$cond: [
<boolean-expression>, <true-case>, <false-case>
]
}
cond는 조건문이 참일 때 <true-case>를 반환하고 거짓일 때 <false-case>를 반환한다.
아래는 Stage 5의 전문이다.
{
_id: "$_id",
participations: {
$last: "$participations",
},
messages: {
$last: "$messages",
},
notReadCount: {
$sum: {
$cond: [
{
$and: [
{
$eq: ["$messages.receiverId", 5],
},
{
$eq: ["$messages.isRead", false],
},
],
},
1,
0,
],
},
},
}
읽어보면 조건문으로
메세지 수신자가 5이고, 메시지가 읽지 않음이면 1을 반환하고, 아니면 0을 반환한다.
그걸 sum을 돌리면 결과는 다음과 같다.
채팅방에서 채팅메시지 정보를 뽑기 위해 사용한 Aggregation의 모든 이미지이다.
위의 Stage 1~5를 Spring boot로 사용해보자.
1. MongoDB의 Id는 ObjectId 형식이다. lookup을 할 때 String과 ObjectId는 형식이 다르기 때문에 참조키로 사용할 수 없기 때문에 변환해야 한다.
AddFieldsOperation objectRoomIdToString = AddFieldsOperation.builder().addField(roomId)
.withValue(
ConvertOperators.ToString.toString("$" + roomId)
).build();
2. MatchOperation은 일치하는 항목을 선택할 때 사용한다. 채팅방이 "삭제 또는 차단"되지 않아야 하기 때문에 일치하는 것을 정해준다.
// and 괄호 안에는 컬럼명이 들어가야한다. enum으로 관리하고, 따로 변수로 빼놨기 때문에 변수로 사용한다.
// 원래는 String 형식의 칼럼명을 작성한다. is는 eq과 같다.
MatchOperation roomMatch = Aggregation.match(
new Criteria()
.and(participation).is(userId)
.and(status).is(ChatStatus.NORMAL.name())
);
3. ChatRoom과 ChatMessage를 lookup(SQL에서의 Join) 하기 위해 정해준다.
// lookup("조인할 컬렉션명", "기준컬렉션의 ID", "조인할 컬렉션에서의 참조키ID")
LookupOperation lookupOperation = Aggregation.lookup(
MessageColumn.COLLECTION_NAME.getWord(),
RoomColumn.ID.getWord(),
MessageColumn.ROOMID.getWord(),
JOIN_AS
);
4. 채팅방 1, 메세지 N 이기 때문에 배열 형식을 하나하나로 풀기 위해 사용한다.
// path는 lookup한 것의 이름을 적으면 되고, noArrayIndex()는 메세지가 없을 때에 빈 배열을 보여줄거냐라는 거다.
// 보여주기 싫으면 skipNullAndEmptyArrays()를 사용하면 됨
UnwindOperation unwindOperation = UnwindOperation.UnwindOperationBuilder
.newBuilder().path("$"+ JOIN_AS)
.noArrayIndex().preserveNullAndEmptyArrays();
5. unwind로 푼 것들을 묶기 위해서 사용한다.
/**
last는 가장 마지막의 것을 가져오게 된다. 나는 마지막 메세지를 찾고 싶어 last를 사용.
sum안에 $cond는 아래와 같은 식으로 줄 수 있다.
Spring boot starter MongoDB 2.7.9 버전을 사용할 때에는 이렇게 사용한다.
*/
GroupOperation roomGrouping = group(roomId)
.last(participation).as(participation)
.last(JOIN_AS).as("lastMessage")
.sum(
ConditionalOperators.Cond.newBuilder().when(
Criteria.where(JOIN_AS + "." + isRead).is(false)
.and(JOIN_AS + "." + MessageColumn.RECEIVERID.getWord()).is(userId)
).then(1)
.otherwise(0)
).as("notReadCount");
6. 메세지가 가장 최근에 이뤄진 채팅방을 맨 위로 올리기 위해서 마지막 메세지의 생성시간 역순으로 조회한다.
SortOperation latestSort = Aggregation.sort(Sort.Direction.DESC, "lastMessage." + createdAt);
마지막으로 해당 메서드의 전문을 첨부해보겠다!
// 채팅방 목록 Aggregation을 반환하는 메서드
private Aggregation makeRoomSearchAggregation(Long userId) {
String JOIN_AS = "messages";
String roomId = RoomColumn.ID.getWord();
String participation = RoomColumn.PARTICIPATIONS.getWord();
String status = RoomColumn.STATUS.getWord();
String createdAt = MessageColumn.CREATEDAT.getWord();
String isRead = MessageColumn.ISREAD.getWord();
AddFieldsOperation objectRoomIdToString = AddFieldsOperation.builder().addField(roomId)
.withValue(
ConvertOperators.ToString.toString("$" + roomId)
).build();
MatchOperation roomMatch = Aggregation.match(
new Criteria()
.and(participation).is(userId)
.and(status).is(ChatStatus.NORMAL.name())
);
LookupOperation lookupOperation = Aggregation.lookup(
MessageColumn.COLLECTION_NAME.getWord(),
RoomColumn.ID.getWord(),
MessageColumn.ROOMID.getWord(),
JOIN_AS
);
UnwindOperation unwindOperation = UnwindOperation.UnwindOperationBuilder
.newBuilder().path("$"+ JOIN_AS)
.noArrayIndex().preserveNullAndEmptyArrays();
GroupOperation roomGrouping = group(roomId)
.last(participation).as(participation)
.last(JOIN_AS).as("lastMessage")
.sum(
ConditionalOperators.Cond.newBuilder().when(
Criteria.where(JOIN_AS + "." + isRead).is(false)
.and(JOIN_AS + "." + MessageColumn.RECEIVERID.getWord()).is(userId)
).then(1)
.otherwise(0)
).as("notReadCount");
SortOperation latestSort = Aggregation.sort(Sort.Direction.DESC, "lastMessage." + createdAt);
return Aggregation.newAggregation(
objectRoomIdToString, roomMatch, lookupOperation
, unwindOperation, roomGrouping, latestSort
);
}
@Override
@Transactional(readOnly = true)
public List<ChatRoomResDto> getList(Long userId) {
List<ChatRoomResDto> resDtos = new ArrayList<>();
// 사실상 이 makeRoomSearchAggregation()과 아래부분만 보면 된다
Aggregation roomSearchAggregation = makeRoomSearchAggregation(userId);
List<ChatRoomFindDto> roomFindDtos =
mongoTemplate.aggregate(
roomSearchAggregation, // Aggregation 조건
RoomColumn.COLLECTION_NAME.getWord(), // 기준 컬레션 이름
ChatRoomFindDto.class // 반환받을 클래스
).getMappedResults();
// ... 후략
}
'Spring' 카테고리의 다른 글
Spring에서 인터페이스를 사용하는 프로그래밍(programming to interface)이 좋은 이유 (0) | 2022.09.04 |
---|---|
Spring Reactive Data Persistence - 리액트 데이터 퍼시스턴스 (0) | 2022.04.09 |
Spring에서 REST (0) | 2022.04.02 |
JPA(Java Persistence Api) 사용하기 (0) | 2022.03.26 |
Spring Boot 폼 입력 유효성 검사(Form Validation ) (0) | 2022.03.24 |