목차
배경 자사 서비스 중에 공공데이터 사이트에서 국민연금 open api 를 사용하고 있었습니다.
그러던 중 api측에서 과도한 api 요청으로 부하가 발생하여 국민연금의 2015~2018년 데이터를 제공하지 않겠다고 공지했습니다.
저희는 기업을 대상으로 서비스를 운영 중이었는데, 더 이상 데이터를 제공받지 못하게 되었습니다. 그래서 국민연금 파일 데이터 제공받아 파일에 있는 국민연금 정보를 DB에 저장하는 작업을 하게 되었습니다.
(파일 데이터로 전환한 또 다른 이유는 api 서버에 여러 문제가 많습니다. 응답 속도도 느릴 뿐더러 서버에서 block 을 피하기 위해 프로그래밍한 것도 너무 짜증이 났습니다…)
2015~2020년 기간의 파일 데이터를 DB 에 저장 작업을 마치고 나서 최종 데이터의 용량, document 수 등을 확인했습니다. DB collection 용량은 약 3GB, Document 수는 90만개가 넘었습니다.
저는 방대한 데이터를 사용한 적이 없어서 어떻게 데이터를 다룰지 고민 했습니다. 문뜩 생각난 indexing 이란 것을 들어만 봤었는데, 어떻게 사용하는지에 대해서는 전혀 알지 못했기 이참에 공부하여 정리 해봤습니다.
Index란 Index 라는 단어는 익숙 합니다. 목차라고 하며 책의 앞쪽 부분에 원하는 내용을 쉽고 빠르게 찾기 위해 나열된 소주제와 페이지 수를 정리해 놓은 것입니다.
만약 목차가 없는 사전에서 ‘정보’라는 단어를 찾는다면 어떻게 단어를 찾아야 할까요?
생각만 해도 끔찍한데 그래도 생각해봅시다… 개발자들은 끔찍한걸 경험해봐야 더 효율적으로 생각 하게됩니다.
책의 ‘ㄱ’ 부터 시작해서 ‘ㅈ’을 찾은 다음에는 ‘ㅏ’ 부터 시작해서 ‘ㅓ’를 계속해서 찾아가야 하는 엄청난 불편함이 생깁니다.
그래서 DB에도 이러한 개념이 있는 것이겠죠. 감사한 일입니다…
Sample DB 설치 Indexing 을 알아 보기전 실습을 하기 위한 sampleDB 를 준비합니다. 이 DB는 국민연금을 납부한 전국의 사업장 정보를 갖고 있습니다. (약 40MB, 73만개의 document)
실습 전에 mongoDB 를 설치합니다.
Sample DB 다운로드 하고 압축을 풀어줍니다.
Command 을 열어서 아래와 같이 진행합니다.
1 2 3 4 5 6 7 8 9 10 11 12 $ mongorestore sampleDB 2020 -01 -31 T23:31 :31.770 +0900 preparing collections to restore from 2020 -01 -31 T23:31 :31.771 +0900 reading metadata for sample.NationalPension from sampleDB/sample/NationalPension.metadata.json2020 -01 -31 T23:31 :31.838 +0900 restoring sample.NationalPension from sampleDB/sample/NationalPension.bson2020 -01 -31 T23:31 :34.768 +0900 [##########..............] sample.NationalPension 70.8 MB/155 MB (45.7 %)2020 -01 -31 T23:31 :37.768 +0900 [######################..] sample.NationalPension 146 MB/155 MB (94.0 %)2020 -01 -31 T23:31 :38.141 +0900 [########################] sample.NationalPension 155 MB/155 MB (100.0 %)2020 -01 -31 T23:31 :38.142 +0900 restoring indexes for collection sample.NationalPension from metadata2020 -01 -31 T23:31 :40.756 +0900 finished restoring sample.NationalPension (731815 documents)2020 -01 -31 T23:31 :40.756 +0900 done
참고로 DB 를 미리 생성하지 않아도 자동으로 DB 를 생성합니다.
1) 복구된 데이터 확인하기
1 2 3 4 5 6 7 8 9 10 11 12 $ mongo > use sample switched to db sample > db .NationalPension.count () 731815 > db .NationalPension.stats(1024*1024) ... 출력 생략
2) Collection 설명
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 > Object.keys(db.NationalPension .findOne ()) [ "_id" , "adptDt" , "wkplNm" , "bzowrRgstNo" , "wkplJnngStcd" , "sido" , "sigungu" , "_created_at" , "_updated_at" ] > db.NationalPension .distinct ("bzowrRgstNo" ).length 8673
Index 유형 indexing 하는 방법은 여러 가지가 있습니다. 그 중 제가 적용한 방법에 대해서 살펴 보겠습니다.
Single field index 1개의 field 만 indexing 합니다. 적용 전, 임의의 쿼리를 실행해 봅시다.
1) 적용 전, 쿼리 실행 결과 상세 정보
totalDocsExamined: 731,815개
executionTimeMillis: 325ms
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 > db.NationalPension.find({ bzowrRgstNo: "105879" }).explain('executionStats' ) { "queryPlanner" : { "plannerVersion" : 1 , "namespace" : "sample.NationalPension" , "indexFilterSet" : false , "parsedQuery" : { "bzowrRgstNo" : { "$eq " : "105879" } }, "winningPlan" : { "stage" : "COLLSCAN" , "filter" : { "bzowrRgstNo" : { "$eq " : "105879" } }, "direction" : "forward" }, "rejectedPlans" : [ ] }, "executionStats" : { "executionSuccess" : true , "nReturned" : 290 , "executionTimeMillis" : 325 , "totalKeysExamined" : 0 , "totalDocsExamined" : 731815 , "executionStages" : { ... }, }, "serverInfo" : { ... }, "ok" : 1 }
2) Indexing 적용하기
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 1 은 오름차순, -1 내림차순을 의미합니다. > db.NationalPension.createIndex({ bzowrRgstNo: 1 }); { "createdCollectionAutomatically" : false , "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1 } // collection 에 적용된 모든 index 확인 > db.NationalPension.getIndexes() // 자동으로 _id 로만 indexing 적용된 상태 [ { "v" : 2, "key" : { "_id" : 1 }, "name" : "_id _", " ns" : " sample.NationalPension" } ]
3) 적용 후, 쿼리 실행 결과 상세 정보
totalDocsExamined: 290개
executionTimeMillis: 1ms
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 > db.NationalPension.find ({ bzowrRgstNo: "105879" }).explain('executionStats' ) { "queryPlanner" : { "plannerVersion" : 1 , "namespace" : "sample.NationalPension" , "indexFilterSet" : false , "parsedQuery" : { "bzowrRgstNo" : { "$eq" : "105879" } }, "winningPlan" : { "stage" : "FETCH" , "inputStage" : { "stage" : "IXSCAN" , "keyPattern" : { "bzowrRgstNo" : 1 }, "indexName" : "bzowrRgstNo_1" , "isMultiKey" : false , "multiKeyPaths" : { "bzowrRgstNo" : [ ] }, "isUnique" : false , "isSparse" : false , "isPartial" : false , "indexVersion" : 2 , "direction" : "forward" , "indexBounds" : { "bzowrRgstNo" : [ "[\" 105879 \", \" 105879 \"]" ] } } }, "rejectedPlans" : [ ] }, "executionStats" : { "executionSuccess" : true , "nReturned" : 290 , "executionTimeMillis" : 1 , "totalKeysExamined" : 290 , "totalDocsExamined" : 290 , "executionStages" : { ... } }, "serverInfo" : { ... }, "ok" : 1 }
상세 정보 중에 가장 중요한 정보만 비교하면 다음과 같습니다.
totalDocsExamined: document 검사 개수가 731815 -> 290
executionTimeMillis: 325ms -> 1ms
Document 검색 개수가 대폭 줄었으며 실행 시간이 무려 325% 증가하였습니다.
마치며 indexing 을 접하고나니 전혀 어렵지 않았고 조금의 설정만으로도 서비스 성능에 만족스러운 결과를 얻었습니다. 아직은 맛보기에 불과하지만 이후에 indexing 을 더 효율적으로 사용하는 방법을 찾아서 적용하려고 합니다.
참고