오늘은 가볍게 CSV파일로 이루어진 데이터를 Mysql DB에 Bulk Insert하는 과정을 다뤄보겠습니다.
현재 제가 당근마켓에서 백엔드 개발을 하고 있는 서비스 '당장모아'에서는 사용자들이 동네의 숨겨진 장소들을 새롭게 알아보고 쉽게 본인의 테마에 추가할 수 있도록 '둘러보기' 탭을 제공합니다.
둘러보기 탭에서 어떤 장소를 보여주면 좋을지 고민하던 저희 팀은 처음에는 단순히 당근마켓의 POI 서비스가 가지고 있는 동네 장소들 중 랜덤으로 N개를 보여주려고 했습니다.
하지만 생각보다 장소의 카테고리가 너무 다양했고, 매력적인 장소를 제공하자는 취지에 맞지 않는 데이터들이 너무 많이 내려왔습니다.
결국 저희는 두 번째 안으로 저희가 직접 매력적인 장소를 추가해보려고 했지만 이 또한 쉽지 않았습니다. 적어도 1000개 정도의 장소가 있어야 사용자들에게 매번 랜덤하게 새로운 장소들을 보여줄 수 있을 것이라는 계산이 나왔지만 어떤 기준으로, 어디까지 장소를 추가해야할지 감이 잘 오지 않았습니다.
그러던 중 저희 팀 디자이너 분께서 당근마켓의 '동네생활' 탭에서 유저들이 한 번이라도 언급한 장소라면 매력적이라고 생각할 수 있지 않을까라고 아이디어를 내주셨고 관련 부서에서 약 1000개 정도의 데이터를 CSV파일로 얻을 수 있었습니다.
처음에 데이터가 온 줄 모른채로 다른 작업에 집중하고 있던 저는 디자이너 분께서 제가 만든 추천 장소 추가 어드민으로 해당 CSV파일에 있는 장소명을 하나하나 추가하고 있는 것을 발견했고, 스크립트를 짜면 쉽게 DB에 넣을 수 있을 것 같다고 생각했습니다.
이왕 해보는거 단순히 반복문으로 1000개의 쿼리를 보내지말고 멘토님들이 한 번쯤 경험해보면 좋다고 말씀해주신 Bulk Insert를 시도해보기로 했습니다.
Bulk Insert의 핵심은 간단합니다.
INSERT INTO <테이블 이름> (칼럼1 이름, 칼럼2 이름) VALUES ?
다음 쿼리를 통해 ?에 넣고자 하는 값들을 배열로 넣어주기만 하면 됩니다.
Bulk Insert가 매번 쿼리를 날리는 것보다 속도가 빠른 이유는 한 번의 쿼리 전후로 이루어지는 작업들을 한 번만(혹은 줄여주기) 때문입니다.
핵심은 간단하고 이제 ?에 넣어줄 배열만 만들면 되니 빠르게 node.js로 스크립트를 짰습니다.
그런데 다 짜고나서 생각해보니 중요한 점을 놓친 것을 알게 되었습니다.
저희 서비스의 추천장소 테이블에는 placeId 뿐만 아니라 지역별 조회를 위해 해당 POI의 regionId도 칼럼으로 가지고 있습니다.
그런데 이 placeId를 통해 한 번에 regionId를 가져올 수 있는 방법이 없었습니다. (서로 다른 부서에서 담당하고 있어 다른 서비스로 운영되기 때문에)
약 1000개의 데이터를 두 번의 HTTP 리퀘스트를 거쳐 가공해야했고 단순히 node.js로 반복문을 돌린다면 비동기처리로 인해 한 번에 1000번 이상의 리퀘스트가 두 개의 서버로 날아갈 것이 예상되었습니다.
거의 공격에 다름없는 리퀘스트가 예상된다고 두 부서 개발자분께 문의했더니 조금씩 나눠서 보내주실 것을 요청하셨습니다.
몇 번에 나눠 리퀘스트를 나눠야할지도 애매하고 Bulk로 데이터를 가져오는 API도 있었지만 몇 개까지 한 번에 받아올 수 있는지도 애매했던 저는 SetInterval함수를 이용해서 시간이 조금 걸리더라도 초당 1번씩 리퀘스트를 보내서 INSERT 문에 넣어줄 배열을 완성하고 한 번에 집어넣는 방식을 택하기로 결정했습니다.
const mysql = require('mysql');
const path = require('path');
const fs = require('fs');
const axios = require('axios').default;
const conn = mysql.createConnection({
host: '',
port: 3306,
database: '',
user: '',
password: '',
})
const sql = "INSERT INTO recommend_place (placeId, regionId) VALUES ?";
const values = [];
const file = fs.readFileSync('./place_list.csv', {encoding: 'utf-8'});
const rows = file.split("\n");
const result = [];
for (let rowIndex in rows) {
if(rowIndex == 0) continue;
const row = rows[rowIndex].split(",");
const placeId = row[0];
result.push(placeId)
}
index = 0;
const err_arr = [];
const interval = setInterval(function() {
console.log(index);
const id = result[index];
if (id) {
axios.get('' + id)
.then(res => {
const poi_id = res.data.id;
const coordinates = res.data.coordinates;
axios.get('', {
params: {
lat: coordinates.latitude,
lng: coordinates.longitude
},
headers: {
"X-Api-Key": ""
}
})
.then(response => {
const regionId = response.data.data.regions[0].id;
const value = [poi_id, regionId];
values.push(value);
index++;
})
.catch(e => {
console.log(e)
})
}).catch(e => {
console.log(e)
err_arr.push(id);
index++;
})
}
if (index === result.length){
clearInterval(interval);
conn.query(sql, [values], function(err) {
console.log(err);
conn.end();
})
console.log(err_arr);
}
}, 1000)
완성된 코드입니다.(빠르게 문제를 해결하기 위해 짠 코드라서 틀린 부분이나 왜 이걸 이렇게? 싶은 부분이 많을 수 있습니다. 댓글로 알려주시면 정말 감사하겠습니다.)
지금까지 빠르게 INSERT하기 위해 짰지만 마이크로서비스 환경에서 다른 서버에 부하를 주지 않기 위해 결국은 시간을 포기한 Bulk Insert 경험담이었습니다.
읽어주셔서 감사합니다.