최근 우리 자사 서비스에 알림 기능을 구현하는 일을 계속 하고 있다. 알림 관련 데이터를 CRUD하는 api를 작성했었고, 지금은 리팩토링을 하고 있다.
그중 가장 관심을 가지고 있는 이슈는 CRUD를 할 때, 노드 내에서 처리하는 것이 아니라, 몽고디비의 쿼리 연산을 이용하는 것이다.
예시로 사용자가 알림을 삭제할 때, 처음 작성했던 방식은 노드에서 데이터를 바꾸고 그 이후 DB를 바꾸는 방식이었다.
// findOne을 통해 해당 데이터를 조회하고, 노드에서 수정 후, save()를 통해 변경 사항을 db에 저장
// ...
const notificationInfo = await Notification.findOne({ userId });
notifications.forEach(notification => {
notificationInfo.listOfNotifications.splice(notificationInfo.listOfNotifications.indexOf(notification._id), 1);
});
return notificationInfo.save();
// ...
몽고디비의 쿼리 연산자를 통해 다음과 같이 리팩토링 했다.
// ...
return await Notification.updateOne({ userId }, { $pull: { listOfNotifications: { _id: { $in: notifications } } } });
// ...
(해당 로직은 도큐먼트를 삭제하는 것이 아니라, 도큐먼트 내에 Array 필드 중 하나의 원소를 없애는 작업이기 때문에 deleteOne보다는 updateOne이 더 적합하다고 판단함)
여기서 두 코드의 차이는 update()를 통해 특정 필드만 수정하느냐, 아니면 save()를 통해 하나의 도큐먼트를 수정하느냐의 차이다.
save()같은 경우, 해당 도큐먼트의 _id를 기준으로 수정된 사항을 반영하여 그 도큐먼트 데이터를 수정한다. 덮어쓰기라고 하면 쉽게 이해될 듯 하다(만약 _id가 없다면 insert 기능을 하여 데이터를 저장). 즉 save()는 도큐먼트 단위로 데이터를 변경한다.
반면에 update의 쿼리 연산을 이용하게 되면, 해당 필드만 수정하게 된다. 그럼 두 방식 중 어떤 방법이 더 좋은 방법일까? 좋은 방법이라고 하면 성능 측면에서 자원을 덜 쓰고 더 빨리, 더 많이 처리할 수 있는 방식일텐데.. 저 두 메소드만 비교해봤을 때에는 필드만 바꾸는 update 방식이 더 효율적일 것 같지만, 직접 모니터링하면서 비교해보지 않는 이상은 확실히 말을 못 하겠다. 다만 디비의 연산자를 사용하지 않는다면, 노드에서도 데이터 처리하는 로직이 실행되고, 가공된 데이터를 다시 디비에 저장하는 작업이 이중으로 실행되기 때문에 디비에서 한 번에 하는 것이 훨씬 효율적일 것이라고 생각한다.
아무튼 쿼리 연산으로 리팩토링하는 이유는 사수님도 코드 리뷰할 때 쿼리 연산으로 하면 더 좋을 것 같다는 의견도 주셨고, 나 역시 몽고디비를 더 잘 활용하기 위해서 많이 사용해보고 싶다는 학습적인 의지도 있어서이다.
리팩토링하는 과정에서 사용했던 기능들 정리 고고곡ㄱㄱㄱ
Array 필드 수정하기
이번 리팩토링 작업을 하면서 가장 날 괴롭혔던 부분은 array 필드를 어떻게 수정하냐는 것이었다. 그리고 아주 일반적이면서도 중요한 진리를 다시 한 번 더 깨달았다. 바로 공식 문서를 세심하게 찾아보고 읽을 것...
https://www.mongodb.com/docs/v4.4/tutorial/update-documents/
$set
{ $set: { <field1>: <value1>, ... } }
$set 연산자는 어떤 필드가 가지고 있는 특정한 값을 수정할 때 사용된다. 만약 그 필드에 $set으로 수정하고자 하는 값이 없다면 그 특정한 값을 가진 필드를 새로 추가한다.
$[<identifier>]
{ <update operator>: { "<array>.$[<identifier>]" : value } },
{ arrayFilters: [ { <identifier>: <condition> } ] }
이 연산자는 업데이트 연산 시, arrayFilters 조건과 일치하는 배열의 원소를 식별하는 기능을 한다.
arrayFilters
arrayFilters는 배열 필드에 있는 원소를 수정하는데 사용되는 조건문이다.
위의 연산 기능들을 사용한다면, 도큐먼트 안에 있는 배열 필드의 특정한 값을 수정할 수 있다.
// ...
return await Notification.updateOne(
{ userId },
{
$set: { "notifications.$[element].isRead": true, "notifications.$[element].confirmedAt": new Date() },
},
{ arrayFilters: [{ "element._id": { $in: [1, 2, 3, 4] } }] }
);
// ...
위의 코드를 본다면, userId의 값을 가진 도큐먼트를 찾은 후, $set을 통해 notifications라는 배열 필드의 isRead값을 true로 변경하고, comfirmedAt이라는 값에 new Date()를 넣는다. 그리고 arrayFilters에서 $[element]를 통해 _id값이 [1, 2, 3, 4]와 일치하는 원소들만 수정한다.
$pull
{ $pull: { <field1>: <value|condition>, <field2>: <value|condition>, ... } }
$pull 연산자는 해당 도큐먼트의 배열 필드 내에 특정한 값을 제거할 때 사용된다. 예시는 맨 위에 알림 삭제 코드에 있다.
$push
{ $push: { <field1>: <value1>, ... } }
이 연산자는 해당 도큐먼트의 배열 필드 내에 특정한 값을 추가하는 기능이다. 만약 이 필드가 배열이 아니라면 이 연산은 진행되지 않는다. 만약 추가하는 값이 배열이라면, 이 배열이 하나의 원소로 추가된다. 만약 [1, 2, 3, 4] 라는 기존 배열이 있고, $push 연산을 통해 [5, 6, 7]을 추가한다면 [1, 2, 3, 4, [5, 6, 7]] 이렇게 된다. 만약 이를 [1, 2, 3, 4, 5, 6, 7]로 하고 싶다면 $each 연산자를 사용하면 된다. 또한 $sort 연산자를 통해 원하는 값으로 정렬할 수도 있다.
// ...
return await Notification.updateOne(
{ userId },
{ $push: { notifications: { $each: [5, 6, 7], $sort: { notifiedAt: -1 } } } }
);
// ...
'개발자 도전기 > [STUDY] DATABASE' 카테고리의 다른 글
mongoDB | node.js | mongoose | populate()로 여러 컬렉션 사용하기 (0) | 2023.01.10 |
---|---|
mongoDB | mongoose | 업무에서 사용하는 기능들 정리(v4.4 기준) - Transactions (0) | 2022.10.31 |
mongoDB | mongoose | 업무에서 사용하는 기능들 정리(v4.4 기준) - Aggregation (0) | 2022.10.25 |
댓글