안녕하세요! 드림어스컴퍼니 인프라개발팀 Jackson 입니다.
FLO는 개인화 서비스를 제공하게 되면서 늘어나는 데이터 볼륨과 부하량을 처리하고자 NoSQL인 MongoDB를 도입하였는데요. RDBMS인 Oracle부터 성능을 위한 Redis 등 여러 저장소 중에서도
FLO 서비스에 MongoDB를 도입한 이유와 이를 통해 어떤 경험을 했는지 등을 이번 컨텐츠를 통해
공유하고자 합니다.
고민사항
MongoDB 도입 전 저희가 겪었던 고민점들을 말씀드리겠습니다.
- DB 인프라의 한계점
간략한 FLO의 DB형상입니다. 저희는 크게 서비스에서 쓰는 FLODB와 각종 로그가 쌓이는 LogDB,
음원 데이터를 관리하는 MetadataDB가 있습니다. 각 서버는 물리 장비이며 HA로 관리하고 있습니다.
이런 저희 DB서버에는 여러 제약이 있었습니다.
데이터 볼륨 증가로 인한 부담
부하분산의 한계(Write 분산의 어려움)
테이블 스키마 변경 어려움
이러한 이유로 저희는 기존 RDBMS가 아닌 다른 솔루션을 찾아야 했습니다.
- 데이터 볼륨 증가
Musicmate → FLO로 브랜드를 변경하고 경쟁력 있는 서비스를 제공하며 이용자 수는 많이 증가하였습니다. 작년대비 몇 배정도 늘었으니... DB가 받는 부하와 쌓이는 데이터는 점점 늘어갔습니다. 그리고 차트를 중심에서 개인화 서비스로 변경되면서 데이터의 양은 폭발적으로 증가하게 되었습니다.
- 부하분산의 한계
MySQL에서 읽기에 대한 부하분산은 slave를 늘리면 되지만 쓰기에 대한 부하는 오로지 Master 1대로 해결해야 합니다. Multi Master 방안도 있지만 데이터 sync 등 안정성 문제로 선택할 수 없었습니다.
- 테이블 스키마 변경 어려움
DB 담당자라면 대용량 테이블의 alter작업은 큰 고민거리입니다. table rebuild가 필요한 alter작업은 metadata lock으로 인한 쿼리 지연이 발생할 수 있기 때문입니다.
더불어 순간 급증하는 disk IO와 스키마 변경 대상 테이블만큼의 공간도 필요하니 여러가지 고려를 해야 합니다.
왜 MongoDB인가?
위의 고민들을 해결하고자 솔루션을 찾아보았는데요. 저희는 성능, 유연성, data store로써의 역할, 트랜잭션 등 저희 서비스에 어울리는 DB를 찾아보았습니다.
- Redis
성능적으로는 in-memory db로 훌륭합니다. key-value기반으로 다양한 데이터 타입을 지원하여 저희 메타를 담을 수 있으며 database model로 실제 파일로도 저장할 수 있어 후보군으로 정했었습니다.
그러나 OLTP 서비스의 특성 상 다양한 필드 필터링 조건을 scan command로 해결하기에는 제한이 크고 특정 문서가 변경되었을 때 관련된 문서군 전체에 대한 갱신 작업이 용이치 않아 서비스의 main database로 사용하기에는 부적절 하다고 판단했습니다.
- ES(Elastic Search)
엘라스틱 서치는 검색엔진으로써 그 성능은 이미 널리 알려져 있습니다. 기본적으로 클러스터링을 지원해 HA, 샤딩등을 별도의 구현없이 활용 가능하며 DSL을 지원하기 때문에 Lucene 쿼리 문법을 알지 못해도 쿼리문을 작성하는데 크게 어렵지 않습니다. 또한 문서의 포맷이 json기반이라는 점에서 도입을 고려했었는데요.
저희가 제공하고자 하는 서비스는 검색과 같이 하나의 문서를 사용자의 다양한 질의 내용에 의해 찾는 것이 아니라 특정 문서가 정해지면 그 문서를 추출하기 위한 필드 조건이 고정되므로 Index를 DB 처럼 고정적으로 생성하여 활용할 수 있는 DB가 더 적합하다고 판단 되었습니다.
ES는 특정 필드에 RDB처럼 Index를 생성하는 구조가 아닌 분석기를 통해 색인 방식이 결정되기 때문에 Index 관리를 DBA가 정적으로 할 수는 없습니다.
그리고 트랜잭션이 Optimistic Locking 메커니즘으로만 구현할 수 있으므로 트랜잭션이 필요한 요구사항이 생기면 구현 난이도가 상당하다는 이슈도 있습니다.
따라서 data 접근측면과 트랜잭션 보장, 동시성 등 DB로서의 역할에는 조금 어렵다고 판단했습니다.
- MongoDB
마지막으로 고려했던 MongoDB는 data store로써의 역할은 충분하고 다양한 인덱스, shard, 이중화 등 저희의 고민점을 충분히 해결해줄 수 있을 것이라 판단했습니다. 다만 mongo query language(MQL)의 생소함과 예전에 알려진 단점 (주로 MMAPv1에 대한)이 조금 망설이게 하였지만 충분한 고민 끝에 도입을 결정하였습니다.
MongoDB의 특징
Mongo가 가진 장점은 널리 있지만 어느 부분이 저희에게 도움이 됐는지 말씀드리겠습니다.
- Flexible schema
스트리밍 서비스를 제공하는 FLO는 기본적으로 음악 데이터를 취급합니다.
음악 데이터는 정형화되지 않고 제공하는 유통사마다 그 규격이 달라 정규화여 관리하고 있습니다. 그래서 하나의 정보를 나타내기 위해서는 많은 테이블을 left join을 하여 가져와야 합니다.
그리고 개인화 데이터가 저장된 테이블은 계속해서 커지고 있으며 이런 테이블의 형상 변경(alter)는 위에서 말씀드렸듯 큰 이슈가 됩니다.
이런 부분에서 mongo는 schema변경에 자유롭습니다.
- HA가 가능합니다.
replicaset을 제공하며 HA(High Availability)를 쉽게 구성할 수 있습니다.
MySQL은 기본적으로 Replication을 지원하지만 HA는 Third party 제품을 사용해야 합니다. MongoDB는 이를 정식 지원 관리해주니 신뢰가 있습니다.
- shard가 가능합니다.
data를 shard하기 위해선 여러 조건이 필요합니다. shard key, node 관리 등 신경 써야 할 것이 많죠
하지만 MongoDB는 이미 shard를 위한 아키텍처가 설계되어 있으며 그저 사용하기만 하면 됩니다.
데이터를 고르게 분배하기 위한 hash shard도 있으니 균등한 데이터 분배에 대한 고민을 안 해도 됩니다.
새 node를 추가해도 쉽게 데이터를 분산시킬 수 있습니다. 자체 로직으로 데이터 분산이 이루어 집니다.
- Secondary Index
RDBMS와 마찬가지로 Secondary 인덱스를 지원합니다. 이 점으로 데이터를 중복 관리할 필요 없도록 해줍니다. 특히 정합성이 중요한 음원데이터의 경우 Secondary Index 지원은 큰 장점입니다.
말씀드린 부분 이외에도 array 타입, collection 압축 등 다양한 기능을 사용하고 있습니다.
MongoDB 기본 용어
MongoDB를 도입한 배경과 그 특징들에 대해 간략히 설명 드렸는데요. 앞서 collection, document 등 RDBMS에서 사용하지 않는 용어들이 있습니다. 그 부분을 간략하게 설명 드리겠습니다.
MongoDB와 RDBMS의 용어 비교표입니다.
- RDBMS vs MongoDB
이렇듯 기본적으로 RDBMS와 비슷해 보이지만 Join의 경우 Embed Document로 구성하거나 aggregate pipeline의 $lookup 함수를 사용할 수 있습니다.
- 인스턴스 종류
Mongod - 데이터를 저장하는 인스턴스입니다. 여러 개의 replicaset으로 구성 가능합니다. 일반적인 DB인스턴스라고 생각하시면 됩니다.
Mongos - Router기능을 하는 인스턴스입니다. 데이터 조회 요청이 오면 Config 서버를 참고하여 어느 node에 있는지 확인하고 해당 Mongod로 접근합니다. shard 구성 시 필수 노드입니다.
Config – Shard된 데이터에 대한 메타데이터와 그 외 설정과 관련된 데이터가 저장된 서버입니다. Mongod로 되어있습니다. 중요한 메타데이터 서버이므로 replicaset으로 구성합니다.
FLO 구성안
저희가 채택한 MongoDB Cluster 구조입니다.
저희가 이렇게 구성한 몇 가지 이유에 대해 말씀드리겠습니다.
config 서버는 shard를 관리하는 데이터를 가지고 있으므로 HA를 구성하여 안정성을 확보해야 합니다.
mongos를 config 서버에 올린 이유는 local 통신으로 네트워크 안정성 및 성능 향상을 취하기 위해 같은 서버에 올렸습니다.
config서버를 보호하기 위해 mongos로 붙는 connection수는 driver 단에서 제한을 둡니다.
shard cluster 개수는 데이터 볼륨, 부하수준을 측정하여 산정하였습니다.
성능이 중요한 mongod는 PM으로 구성하였고 config, mongos는 VM입니다.
이 외에도 mongos를 각 애플리케이션 서버로컬에 배포하는 방법도 있으며 mongos, config, mongod를 각각 서버에 띄우는 방법도있습니다.
Modeling
몽고 아키텍처를 구성하고 나서 저희를 가장 고민하게 만든 과제는 모델링이었습니다.
pain-point로 잡고 있는 성능과 볼륨을 잡기위해선 모델링이 중요한데 어떻게 해야 할지 고민이었습니다.
우선 모델링 설명은 음원 데이터를 기준으로 설명 드리겠습니다.
모델링 기법은 공식 다큐먼트에 설명 되어있지만 저희 도메인 기반으로 세 가지로 나눠 설명 드리겠습니다.
- Full Embed Model
{
"_id" : ObjectId("5e13540a7c1cd06c3ea596e9"),
"track_id" : NumberLong(1),
"track_nm" : "track title",
"disk_id" : 1,
.
.
.
"create_dtime" : ISODate("2019-03-29T12:44:26.000Z"),
"album" : {
"album_id" : NumberLong(1),
"title" : "album title",
"release_ymd" : “20200505",
"disc_cnt" : 1,
.
.
.
}
먼저 full embed 모델입니다. 위 샘플은 곡 데이터 예시입니다.
음원 메타의 특성을 이해하기 쉽게 설명 드리자면 크게 가수, 앨범, 곡 세 가지로 구성되어 있습니다.
여기서 가수는 여러 앨범을 가지고 앨범은 여러 곡을 가집니다. 그래서 Full embed 모델에서는 곡 아래에 앨범, 가수 정보를 embed하고 앨범에는 가수를 embed하여 관리합니다.
json 샘플을 보시면 트랙의 정보와 트랙이 포함되는 앨범의 정보를 갖고 있습니다. json이 너무 길어서 조금 잘랐는데요. 여기에 곡의 아티스트의 데모 정보도 전부 embed 되어있다고 보시면 됩니다.
이 모델은 애플리케이션 서버에서 한번의 호출로 많은 정보를 가져올 수 있기 때문에 API는 단순한 호출이라는 이점이 있습니다. 다만 document 사이즈가 커지게 되며 각 메타에 갱신이 발생하면 이와 연관된 모든 데이터가 변경되어야 하므로 갱신 작업에 부담이 있습니다. 그리고 다른 collection과 데이터 중복이 있습니다. 여기서 말씀드리는 갱신에 대한 부담은 하나의 데이터가 여러 collection에 종속적이라는 의미입니다. 예를 들어 하나의 아티스트가 변경되면 아티스트와 그 아티스트가 가진 앨범, 트랙을 전부 갱신해야 한다는 의미입니다.
- Reference Embed Model
{
"_id" : ObjectId("5e13540a7c1cd06c3ea596e9"),
"track_id" : NumberLong(3001),
"track_nm" : "track title",
"disk_id" : 1,
.
.
.
"create_dtime" : ISODate("2019-03-29T12:44:26.000Z"),
"album" : NumberLong(2001),
"artist_list" : [
NumberLong(1001),
NumberLong(1002),
NumberLong(1003)
]
.
.
.
}
이 모델은 참조해야 할 key만 가지는 모델입니다. 샘플 보시 듯 곡의 앨범은 앨범ID만 가지고 곡의 아티스트 리스트는 아티스트ID만 가집니다.
이 모델의 특징은 다큐먼트가 Full에 비해 가볍고 갱신에 대한 부담이 없습니다. 다만 애플리케이션은 참조하는 대상만큼 쿼리 호출을 하게 됩니다. 하나의 곡을 나타내기 위해 곡, 아티스트, 앨범 collection 3번 호출해야 합니다. Mongo가 단순 query 호출의 성능이 뛰어나다고 해도 3번의 호출은 아무래도 큰 성능을 내긴 어려울 것이라고 생각했습니다.
저희는 고민했습니다.
짧은 API call로 latency를 줄일 수 있지 않을까?, working set이 커져 cache eviction이 자주 일어나지 않을까?, 갱신의 부담, 데이터 중복 등 고민한 결과 저희는 둘 다 만족시킬 수 있는 Subset pattern을 찾았습니다.
- Subset pattern
이 패턴의 기원은 working set의 크기가 커 메모리에 부담을 주게 되므로 사용하지 않는 정보는 메인 셋에서 제거하고 다른 subset으로 옮겨 최적화한 모델링입니다. Subset pattern 패턴은 mongo blog에서 참고하였는데요.
예를 들어 상품 collection에 리뷰가 embed되어있는데 그 리뷰가 계속 늘어나게 되면 한 상품의 document size는 계속 늘어나게 됩니다.
그래서 이를 해결하고자 사용자의 경험과 access pattern을 바탕으로 최근 10개의 리뷰만 메인 상품 collection에 두고 review collection을 새로 만들어 기존 리뷰들을 담아 전체 리뷰 조회 요청을 처리했다고 말합니다.
그래서 저희는 이 패턴을 참고하여 API 서버 access pattern을 분석하여 필요한 정보만 embed시켰습니다.
{
"_id" : ObjectId("5e13540a7c1cd06c3ea59704"),
"track_id" : NumberLong(3001),
"track_nm" : "track title",
"disk_id" : 1,
"album" : {
"album_id" : NumberLong(2001),
"title" : "album title“,
"image" : "http://www...",
},
"artist_list" : [
{
"artist_id" : NumberLong(1001),
"artist_nm" : "artist name"
}
]
}
예를 들면 하나의 곡을 호출할 때 필요한 정보는 앨범 명, 앨범커버 이미지, 아티스트 명입니다.
Full embed와 비교하여 굳이 앨범, 아티스트의 다른 정보를 담을 필요 없으니 document 사이즈가 줄어들고 애플리케이션 호출도 간단합니다. 다만 API access pattern에 따른 형상 관리가 필요합니다.
저희가 이 모델을 채택하게 된 이유는 API call을 짧게 가져가고 비교적 가벼운 document로 성능 향상을 할 수 있다고 생각했습니다.
그리고 메타데이터 특성 상 형상이 자주 변경되지 않기 때문에 access pattern도 크게 변경되지 않을 것이라 생각하여 관리의 부담에 비중을 크게 두지 않았습니다.
하나의 document 사이즈가 크다면 이로 인한 조회, 갱신은 메모리 eviction을 일으키고 hot data를 많이 올릴 수 없어 disk 부하, 성능 저하를 발생시킵니다.
저희는 semi embed model의 document size가 wired tiger cache 사이즈와 hot data 비율을 고려했을 때 합리적이라고 판단했고 그 기준을 계산했기 때문에 적합한 모델이라고 생각했습니다.
실제로 document array에 one to many라 해도 document size가 커질 수 있는 경우엔 reference embed 모델로 진행하였으며 one to million으로 들어가는 경우에는 Embed 하지 않는 모델로 반영했습니다.
Migration
이렇게 고안한 형상에 데이터 마이그레이션은 중요한 작업입니다. 저희가 진행했던 마이그레이션 방법은 두 가지입니다.
- $lookup 마이그레이션
첫 째로 저희가 마이그레이션 시도 방법은 aggregate pipe line의 lookup 함수를 이용하는 것이었습니다.
서비스를 중단하고 마이그레이션 대상 table의 csv를 추출하고 mongoimport로 입력한 뒤 aggregate pipeline으로 collection을 생성하는 시나리오였습니다.
독특하게 보일 수 있지만 이렇게 한 가장 큰 이유는 MQL에 익숙해지기 위함이었습니다. MQL은 SQL에서 넘어가기 어려워하는 허들이지만 반드시 넘어야할 과제 이므로 다른 방법보다 이 방법을 먼저 고민했습니다. 그리고 기존 RDBMS처럼 쿼리로 collection을 만들어 낼 수 있어 접근성이 좋았으며 마지막으로 lookup의 성능을 단순히 맹신하였기 때문입니다.
하지만 테스트 결과는 안 좋았습니다. 앨범 100만 데이터 마이그레이션 시간이 2시간 넘게 소요되었으며 lookup해야 할 collection이 많아질 수록 시간은 더욱 더 느렸습니다. 비록 aggregate에 대한 기본지식을 좀 쌓긴 했지만 마이그레이션 성능에는 좋지 못하다고 판단했습니다.
저희가 첫 번째 마이그레이션 테스트 이후로 느낀 점은 우리가 마이그레이션을 위한 많은 시간을 할애할 수 있다면 굳이 csv를 내리고 올릴 필요 없이 바로 MongoDB로 넣으면 될 것이라고 생각했습니다.
그러기 위해서는 긴 서비스 중단이 아닌 증분 처리 마이그레이션이 필요했습니다.
- 증분처리 마이그레이션
그렇게 진행하기 위한 두 번째 마이그레이션 로직은 다음과 같습니다.
MongoSyphon을 이용한 실시간 마이그레이션 로직을 계획했습니다.
사실 저는 개발에 서툴기에 직접 마이그레이션 툴을 개발하기엔 조금 역부족이었습니다. 그래서 오픈소스를 찾던 중 MongoSyphon을 알게 되었는데요. 이 툴은 java로 만들진 오픈소스로 간단한 template을 작성하면 MySQL에서 관련 테이블을 참조해 json형태로 mongo로 바로 넣을 수 있습니다.
그래서 처리 방법은 증분 처리를 위한 trigger를 먼저 배포하고 마이그레이션을 진행하여 초기 데이터를 MongoDB에 넣고 잠깐의 서비스 중단 시 변경분을 반영하는 것이었습니다. 그 결과로 안정적으로 마이그레이션을 마칠 수 있었습니다.
웬만하면 사용하지 말아야 할 것들
마지막으로 도입 전 겪은 경험에 대해 몇 가지 말씀드리겠습니다.
- MQL에서 javascript 사용
저희는 서비스오픈 전 성능 테스트를 진행합니다. 시나리오는 평시와 같은 부하를 넣고 문제가 없으면 서비스 장애가 날 때까지 단계별로 부하를 줘서 임계치를 산정하는 테스트를 진행합니다. 그런데 WAS, DB서버 장애가 날 상황의 부하 상태가 아니었음에도 일정 수준 이상으로 TPS가 오르지 않았습니다. 이 때 발견한 slow query는 아래와 같았습니다.
db.track.find({
'album.album_id': 2457,
'status_yn': 'Y',
'album.status_yn': 'Y',
'$where': function() { var now = new Date(); return (this.start_dtime <= now && now <= this.end_dtime) && (this.album.start_dtime <= now && now <= this.album.end_dtime)}
});
## index list
track_id_1
album.album_id_1
.
.
.
track collection에 ‘album.album_id’의 인덱스가 있음에도 slow query로 되었으며 인덱스의 카디널리티(Cardinality)도 높은 인덱스였습니다. 저희는 원인을 찾던 중 다음의 문서를 발견하였습니다.
“Considerations
·Do not use global variables.
$where evaluates JavaScript and cannot take advantage of indexes. Therefore, query performance improves when you express your query using the standard MongoDB operators (e.g., $gt, $in).
In general, you should use $where only when you can’t express your query using another operator. If you must use $where, try to include at least one other standard query operator to filter the result set. Using $where alone requires a collection scan.
Using normal non-$where query statements provides the following performance advantages:
MongoDB 공식 다큐먼트에서 발췌한 내용으로 MQL안에서 javascript사용은 기본 쿼리로 표현할 방법이 없을 때 사용하라고 나옵니다. 그리고 인덱스가 있는 필드라도 사용할 수 없다고 나오는데요. 위에서 언급한 쿼리는 인덱스 스캔이 가능했음에도 불구하고 Javascript는 mongo에서 evaluation단계가 있기 때문에 일반적인 표현보다는 성능 저하를 가져올 수 있다고 합니다. 그리하여 해당 쿼리는 다음과 같이 튜닝하였습니다.
db.track.find({
'album.album_id': 457,
'status_yn': 'Y',
'album.status_yn': 'Y',
'start_dtime': { "$lte" : { "$date" : "2020-05-06T04:35:43.406Z" } } ,
'end_dtime': { "$gte" : { "$date" : "2020-05-06T04:35:43.406Z" } },
'album.start_dtime': { "$lte" : { "$date" : "2020-05-06T04:35:43.406Z" } } ,
'album.end_dtime': { "$gte" : { "$date" : "2020-05-06T04:35:43.406Z" } }
});
튜닝 후 db부하는 감소하였으며 임계치로 현재 대비 3배 이상의 부하를 소화하는 결과로 테스트를 마칠 수 있었습니다.
- db.collection.count()
마이그레이션 후 전체 건수, 데이터 타입, 코드 값 distinct 등 여러 쿼리를 통해 검증합니다. 이 때 저희가 사용한 검수 쿼리로는 다음과 같은데요.
db.member_playlist.count();
db.member_playlist.aggregate([
{$unwind:"$track_list"},
{$group:{
_id:null,
cnt: {$sum:1}
}}
]);
db member_playlist.find({"playlist_id":{$not:{$type:"long"}}});
db.member_playlist.find({"playlist_title":{$exists:true}}).count();
db.member_playlist.distinct("playlist_type");
여기서 member_playlist는 회원들이 가지고 있는 마이플레이리스트 collection입니다. 마이그레이션 후 전체 건수를 검증하는데 그 때 당시 데이터가 100만이라면 마이그레이션 후 데이터는 120만으로 나왔었습니다.
db.member_playlist.count()
-> 1,200,324
db.member_playlist.aggregate([
{$project:
{
_id:0
}
},
{$out:"member_playlist_back"}
])
db.member_playlist_back.count()
-> 1,002,114
한가지 더 의아한 사실은 데이터가 늘어났기 때문에 중간 유입이 있는 것인지 의심하여 데이터를 백업을 aggregate pipe line의 $out스테이지로 collection을 만들었는데요. 그 collection의 전체 건수는 마이그레이션 대상 건수와 일치했습니다.
또 다시 공식 document를 찾아보지 않을 수 없었습니다. 거기서 발견한 사실은 다음과 같습니다.
“IMPORTANT
Avoid using the db.collection.count() method without a query predicate since without the query predicate, the method returns results based on the collection’s metadata, which may result in an approximate count. In particular,
- On a sharded cluster, the resulting count will not correctly filter out
- After an unclean shutdown, the count may be incorrect.
For counts based on collection metadata, see also collStats pipeline stage with the count option. ”
전체 count 쿼리는 기본적으로 통계정보를 참고하여 반환하는데 마이그레이션 시 move chunk로 인한 orphan document가 아직 삭제되지 않은 상태에서 return된 것으로 확인했습니다. 그래서 만약 마이그레이션이나 move chunk가 일어난 상황에서 전체 count를 정확하게 뽑고자 한다면 aggregate를 사용하거나 인덱스 스캔을 통한 count 쿼리를 작성해야 합니다.
db.member_playlist.aggregate([
{$group:{
_id:null,
cnt: {$sum:1}
}}
]);
or
db.member_playlist.count({"character_no":{$gt:0}});
-> 5,762,114
마치며
몽고 도입 초기에 강력한 성능에 대한 맹신으로 호기롭게 도전했지만 예상치 못한 문제들을 여럿 마주쳤습니다.
MQL에 사용한 javascript쿼리로 인한 성능저하와 collection 전체 count의 통계 값 리턴 등 이 있었습니다. 이런 경험들을 바탕으로 저희는 무엇을 사용하기 전 꼭 document를 읽어보고 사이드 이팩트에 대해 조사 해야겠다는 교훈을 얻었습니다.
그리고 modeling에 대하여... 도메인 지식, API access pattern, 잘 알려진 modeling 기법, 볼륨 메트릭스 등을 참고하여 결과로 나온 저희 모델링을 기반으로 최적의 환경을 구성하여 현재 최고 워크로드 대비 3배 이상의 퍼포먼스를 낼 수 있었습니다.
그리고 DB는 공유 자원임을 항상 기억하여 개발, DB 어느 한쪽으로만 치우치지 않는 선택을 하는 것이 중요하다는 사실을 다시 한번 배웠습니다.
마이그레이션에서는 비록 안 좋은 결과로 끝났던 lookup 마이그레이션 실패로 얻은 교훈을 바탕으로 실시간 mongoSyphon기반 마이그레이션을 계획할 수 있었습니다.
그 결과로 안정적으로 마이그레이션을 완료할 수 있었습니다. 그리고 솔루션을 자체적으로 만들 수 있다면 좋겠지만 그럴 여력이 안된다면 좋은 오픈소스를 찾아보는 것도 좋을 것 같습니다.
그럼에도 저희의 이런 경험과 노력으로 저희가 고민했던 pain-point들을 해결할 수 있었습니다.
데이터 볼륨은 shard와 collection 압축으로 약 50% 절약할 수 있었고 유연한 스키마는 스키마에 대한 부담을 줄여주었습니다. 무거운 쿼리는 간단한 호출로, write/read 부하분산은 shard로 해결하였습니다.
추후에는 Version up과 Change stream, TTL, Capped collection 등 더 다양한 기능을 추가하여 서비스의 안정성을 도모하도록 노력해보려 합니다. 이상 MongoDB 도입기에 대한 블로그를 마칩니다.
읽어주셔서 감사합니다!
👍👍👍