최근에 라인 알림센터의 마이그레이션에 관련된 스토리에 대 한 글을 읽었다. 라인 서비스의 알림 기능을 담당하는 팀이 올해 초 메인 스토리지를 레디스에서 몽고디비로 마이그레이션하게 된 이유와 배경에 대해서 자세하게 서술되어 있는데, 나도 현재 알림 기능을 담당하는 개발자이다보니 더 흥미가 생겨서 읽었던 것 같다.
https://engineering.linecorp.com/ko/blog/LINE-integrated-notification-center-from-redis-to-mongodb/
라인의 경우 서비스 고도화와 이용량 증가에 따른 redis의 효율성 문제, 비용 이슈로 mongoDB로 변경하게 되었다고 한다. 수많은 DB 중 mongoDB를 선택한 이유로는 첫 번 째, 알림의 데이터 형태가 문서형 데이터베이스(document database)에 적합했고 두 번째 이유로는 여러 개의 인덱스(secondary index), 복합 인덱스(compund index), TTL(time to live)인덱스 등 지원하는 인덱스 기능들이 풍부하기 때문이라고 한다. 이 외에도 고가용성, 확장성 등 팀의 자원을 고려했을 때 몽고디비가 적합했다고 한다. (세컨더리 인덱스, 컴파운드 인덱스, TTL 인덱스가 뭐지.... 공부할 부분 생겼다 ㄱㅇㄷ)
그럼, 우리 팀은 왜 MongoDB를 채택했을까? 팀 선배님에 의하면 우리 서비스 같은 경우 스타트업이나 투자자, 뉴스, 토론 등 검색하고 탐색할 수 있는 기능과 데이터가 매우 다양하고 매우 복잡하기 때문에 RDBMS처럼 정규화를 거친 데이터베이스의 경우 매번 발생하는 join은 성능면에서 비효율적이라고 했다. 비정규화로 스키마를 디자인하여 join하지 않아서 구현 복잡도를 낮췄고 복잡한 트랜잭션을 구현하거나 실수 발생에 대한 가능성을 낮출 수 있기 때문에 몽고디비를 사용한다고 했다.
여하튼 나는 현재 몽고디비를 사용해야 하는 개발자이기 때문에 당연하게 몽고디비에 대해서 잘 알고 잘 사용할 줄 알아야 한다. 다행히 정글에서 팀프로젝트를 할 때 몽고디비를 공부하고 사용한 경험이 있기 때문에 현재 업무에 적응하는데 도움이 되었지만, 그 지식의 깊이가 얕기 때문에 이를 더 학습하고자 하는 욕심이 생겼다. 물론 욕심이 없더라도 일을 잘하기 위해서 결국 알아야 하는 것들이지만...!!
그래서 지금 학습하고자 하는 부분은 몽고디비 처음부터!! 라고 하려고 했지만... 우선적으로 현재 내가 업무에서 사용하고 있는 기능들을 먼저 정리해보려고 한다(물론 공부하다가 기본이 버러지같으면 다시 처음부터 차근차근 정리할 것이다. 가자 태초마을로...!)
Aggregation
Aggregation operations process multiple documents and return computed results.
aggregation은 여러 문서를 처리하고 계산된 결과를 리턴한다. aggregation은 다음과 같은 상황에서 사용할 수 있다.
- 여러 문서의 값을 함께 그룹화
- 단일의 결과를 반환하기 위해 그룹화된 데이터 작업 수행
- 시간 경과에 따른 데이터 분석
Aggregation Pipelines
어그리게이션 파이프라인은 도큐먼트를 수행하는 하나 이상의 스테이지로 구성되어 있다. 각각의 스테이지들은 그 안에 있는 문서들을 수행한다. 예를 들어 어떤 하나의 스테이지에서 문서를 필터링하고, 그룹화하고, 값을 계산할 수 있다. 이렇게 하나의 스테이지에서 도출된 도큐먼트는 다음 스테이지로 넘겨진다. 또한 어그리게이션 파이프라인은 문서 그룹에 대한 결과를 반환할 수 있다. 예시로 합계, 평균, 최대값 및 최소값을 반환할 수 있다. 공식 문서에 나온 예시를 보자.
db.orders.aggregate( [
{ $match: { status: "urgent" } },
{ $group: { _id: "$productName", sumQuantity: { $sum: "$quantity" } } }
] )
$match 스테이지에서 status가 "urgent"인 도큐먼트들을 필터링하고, 다음 $group 스테이지에서 productName필드들을 그룹화한 후, 각 productName의 quantity의 합을 계산하여 sumQuantity 필드로 리턴한다.
Map-Reduce
❗️MongoDB 5.0 버전부터는 Map-Reduce 기능이 더이상 사용되지 않고, 이 기능은 Aggregation Pipeline으로 사용할 수 있다고 한다. 그 이유로는 map-reduce보다 aggregation pipeline이 성능과 사용성에 있어서 더 효율적이기 때문이라고 한다. 현재 우리 팀은 아직 4.4 버전을 이용하고 있지만, 실제 이 기능을 사용하고 있지 않기 때문에 스킵!
Mongoose Aggregate Methods
MongoDB의 ODM인 Mongoose에서 편의성을 위해 다양한 프로토타입 메소드들을 지원한다. 현재 우리팀의 코드에서 자주 사용되고 있는 메소드들 위주로 정리 고고고고고
$addFields
Adds new fields to documents. $addFields outputs documents that contain all existing fields from the input documents and newly added fields.
addFields는 파라미터로 객체 필드를 받는다. 파라미터로 받은 객체를 도큐먼트 안에 필드로 추가하는 메소드다. 예시를 한 번 만들어봤다.
db.Sales.aggregate([
{ $addFields: { lastPayment: { $last: "$paymentHistory" } } },
{
$match: {
"paymentMethod": "TOSSPAYMENTS",
},
},
{ $unset: ["lastPayment"] },
]);
Sales라는 콜렉션이 있고, 그 안에 paymentHistory라는 배열 형식의 결제 히스토리를 담는 필드와 paymentMethod라는 결제 방식을 담은 스트링 형식의 필드가 있다고 하자. 이때 $addFields를 통해 lastPayment라는 필드를 생성할 수 있고, 이때 이 값은 $last를 통해 paymentHistory의 가장 마지막 값을 가지도록 설정한다. 그 다음 $match스테이지에서 paymentMethod가 토스페이먼트인 것을 필터링하고, 마지막으로 $unset을 통해 생성했던 새 필드를 다시 없앴다.
다른 예시로는 a, b, c라는 필드를 가지고 있고, 세 값의 합인 d라는 필드를 생성하고 싶을 때 다음과 같이 코드를 작성할 수 있다.
db.<collection>.aggregate([{$addFields:{"d":{$sum:[a,b,c]}}])
$match
Filters the documents to pass only the documents that match the specified condition(s) to the next pipeline stage.
{ $match: { <query> } }
$match는 작성한 쿼리(객체 형식)에서 일치하는 도큐먼트만 반환하는 메소드다.
{ "_id" : ObjectId("512bc95fe835e68f199c8686"), "author" : "dave", "score" : 80, "views" : 100 }
{ "_id" : ObjectId("512bc962e835e68f199c8687"), "author" : "dave", "score" : 85, "views" : 521 }
{ "_id" : ObjectId("55f5a192d4bede9ac365b257"), "author" : "ahn", "score" : 60, "views" : 1000 }
{ "_id" : ObjectId("55f5a192d4bede9ac365b258"), "author" : "li", "score" : 55, "views" : 5000 }
{ "_id" : ObjectId("55f5a1d3d4bede9ac365b259"), "author" : "annT", "score" : 60, "views" : 50 }
{ "_id" : ObjectId("55f5a1d3d4bede9ac365b25a"), "author" : "li", "score" : 94, "views" : 999 }
{ "_id" : ObjectId("55f5a1d3d4bede9ac365b25b"), "author" : "ty", "score" : 95, "views" : 1000 }
articles라는 콜렉션이 있는데 만약 dave라는 저자의 도큐먼트만 반환하고 싶으면 다음과 같이 작성하면 된다.
db.articles.aggregate(
[ { $match : { author : "dave" } } ]
);
결과:
{ "_id" : ObjectId("512bc95fe835e68f199c8686"), "author" : "dave", "score" : 80, "views" : 100 }
{ "_id" : ObjectId("512bc962e835e68f199c8687"), "author" : "dave", "score" : 85, "views" : 521 }
$project
Passes along the documents with the requested fields to the next stage in the pipeline. The specified fields can be existing fields from the input documents or newly computed fields.
* ref: https://www.mongodb.com/docs/v4.4/reference/operator/aggregation/project/
{ $project: { <specification(s)> } }
$project는 필드를 포함하거나 _id값 제외, 새로운 필드 추가 및 기존의 필드 값을 재설정할 수 있는 기능을 한다. 뭔가 다양하게 활용할 수 있을 것 같은 느낌이 든다..
필드를 포함시키거나 제외할 때는 0(false)나 1(true)값을 부여하여 설정할 수 있다. 참고로 _id는 true가 디폴트이기 때문에, 만약 이 값을 제외하고 싶으면 _id:0 을 명시해야 한다.
// ...
{$project: { _id: 0, price: 1 } },
// ...
또한 project로 새로운 필드를 추가할 수 있지만, 만약 추가해야 하는 상황이 온다면 $addFields를 사용하는 것이 더 간단하다고 한다.
공식 문서의 예시로 사용 방법을 이해해보자. books라는 콜렉션이 있고, 아래와 같은 도큐먼트가 있다.
{
"_id" : 1,
title: "abc123",
isbn: "0001122223334",
author: { last: "zzz", first: "aaa" },
copies: 5
}
만약 이런 코드를 작성한다면 결과는 다음과 같을 것이다.
// aggregete $project 사용
db.books.aggregate( [ { $project : { title : 1 , author : 1 } } ] )
// 결과
{ "_id" : 1, "title" : "abc123", "author" : { "last" : "zzz", "first" : "aaa" } }
$limit
Limits the number of documents passed to the next stage in the pipeline.
$limit takes a positive integer that specifies the maximum number of documents to pass along.
{ $limit: <positive integer> }
$sort
The $sort modifier orders the elements of an array during a $push operation.
$sort는 4.4버전 이전까지는 도큐먼트 내에 있는 배열만 정렬할 수 있었는데, 현재는 도큐먼트가 아닌 배열도 정렬할 수 있다고 한다.
댓글